fixed language logic items
This commit is contained in:
parent
fbfe85f225
commit
d1f0b3c3d6
155 changed files with 2091 additions and 2057 deletions
|
|
@ -18,7 +18,7 @@ export interface AttributeDefinition {
|
|||
description?: string;
|
||||
required?: boolean;
|
||||
default?: any;
|
||||
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
||||
options?: Array<{ value: string | number; label: string }> | string;
|
||||
validation?: any;
|
||||
ui?: any;
|
||||
readonly?: boolean;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export interface Prompt {
|
|||
|
||||
export interface AttributeOption {
|
||||
value: string | number;
|
||||
label: string | { [key: string]: string };
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface AttributeDefinition {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export interface AttributeDefinition {
|
|||
description?: string;
|
||||
required?: boolean;
|
||||
default?: any;
|
||||
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
||||
options?: Array<{ value: string | number; label: string }> | string;
|
||||
validation?: any;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
<button
|
||||
className={`${styles.iconButton} ${styles.danger}`}
|
||||
onClick={() => onDelete(rule.id)}
|
||||
title={t('delete rule')}
|
||||
title={t('Regel löschen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -97,7 +97,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
<div className={styles.permissionsGrid}>
|
||||
{/* View Toggle */}
|
||||
<div className={styles.permissionItem}>
|
||||
<span className={styles.permissionLabel}>View</span>
|
||||
<span className={styles.permissionLabel}>{t('Ansicht')}</span>
|
||||
<div className={styles.viewToggle}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -113,7 +113,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
{isDataRule ? (
|
||||
<>
|
||||
<div className={styles.permissionItem}>
|
||||
<span className={styles.permissionLabel}>Read</span>
|
||||
<span className={styles.permissionLabel}>{t('Lesen')}</span>
|
||||
<AccessLevelSelect
|
||||
value={rule.read}
|
||||
onChange={(value) => onUpdate(rule.id, { read: value })}
|
||||
|
|
@ -122,7 +122,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
/>
|
||||
</div>
|
||||
<div className={styles.permissionItem}>
|
||||
<span className={styles.permissionLabel}>Create</span>
|
||||
<span className={styles.permissionLabel}>{t('Erstellen')}</span>
|
||||
<AccessLevelSelect
|
||||
value={rule.create}
|
||||
onChange={(value) => onUpdate(rule.id, { create: value })}
|
||||
|
|
@ -131,7 +131,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
/>
|
||||
</div>
|
||||
<div className={styles.permissionItem}>
|
||||
<span className={styles.permissionLabel}>Update</span>
|
||||
<span className={styles.permissionLabel}>{t('Bearbeiten')}</span>
|
||||
<AccessLevelSelect
|
||||
value={rule.update}
|
||||
onChange={(value) => onUpdate(rule.id, { update: value })}
|
||||
|
|
@ -140,7 +140,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
/>
|
||||
</div>
|
||||
<div className={styles.permissionItem}>
|
||||
<span className={styles.permissionLabel}>{t('delete')}</span>
|
||||
<span className={styles.permissionLabel}>{t('Löschen')}</span>
|
||||
<AccessLevelSelect
|
||||
value={rule.delete}
|
||||
onChange={(value) => onUpdate(rule.id, { delete: value })}
|
||||
|
|
@ -214,7 +214,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
};
|
||||
|
||||
const getLabel = (obj: CatalogObject): string => {
|
||||
return obj.label.de || obj.label.en || obj.objectKey;
|
||||
return (typeof obj.label === 'string' ? obj.label : '') || obj.objectKey;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -260,7 +260,9 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
)}
|
||||
|
||||
<span className={styles.formHint}>
|
||||
Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).
|
||||
{t(
|
||||
'Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*).'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -272,7 +274,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
onChange={(e) => setView(e.target.checked)}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
Sichtbar (View)
|
||||
{t('Sichtbar (Ansicht)')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -290,7 +292,12 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
{(['create', 'read', 'update', 'delete'] as const).map(op => {
|
||||
const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read;
|
||||
const setValue = op === 'delete' ? setDel : op === 'create' ? setCreate : op === 'update' ? setUpdate : setRead;
|
||||
const labels = { create: 'Create', read: 'Read', update: 'Update', delete: 'Delete' };
|
||||
const labels = {
|
||||
create: t('Erstellen'),
|
||||
read: t('Lesen'),
|
||||
update: t('Bearbeiten'),
|
||||
delete: t('Löschen'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={op} className={styles.matrixRow}>
|
||||
|
|
@ -310,7 +317,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
setValue(hierarchy[idx - 1] || 'n');
|
||||
}
|
||||
}}
|
||||
title={`${labels[op]} - ${level === 'm' ? 'Eigene' : level === 'g' ? 'Gruppe' : 'Alle'}`}
|
||||
title={`${labels[op]} - ${level === 'm' ? t('Eigene') : level === 'g' ? t('Gruppe') : t('Alle')}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -322,10 +329,10 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
|
||||
<div className={styles.formActions}>
|
||||
<button type="button" className={styles.secondaryButton} onClick={onCancel}>
|
||||
Abbrechen
|
||||
{t('Abbrechen')}
|
||||
</button>
|
||||
<button type="submit" className={styles.primaryButton}>
|
||||
<FaPlus /> Hinzufügen
|
||||
<FaPlus /> {t('Hinzufügen')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -355,6 +362,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
|||
onDelete,
|
||||
onAdd,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA
|
||||
|
||||
|
|
@ -373,9 +381,12 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
|||
|
||||
const getEmptyText = () => {
|
||||
switch (context) {
|
||||
case 'DATA': return 'Keine Daten-Regeln definiert';
|
||||
case 'UI': return 'Keine UI-Regeln definiert';
|
||||
case 'RESOURCE': return 'Keine Ressourcen-Regeln definiert';
|
||||
case 'DATA':
|
||||
return t('Keine Daten-Regeln definiert');
|
||||
case 'UI':
|
||||
return t('Keine UI-Regeln definiert');
|
||||
case 'RESOURCE':
|
||||
return t('Keine Ressourcen-Regeln definiert');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -384,7 +395,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
|||
{!readOnly && !showAddForm && (
|
||||
<div className={styles.sectionHeader}>
|
||||
<span className={styles.sectionTitle}>
|
||||
{rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'}
|
||||
{rules.length} {rules.length === 1 ? t('Regel') : t('Regeln')}
|
||||
</span>
|
||||
<div className={styles.headerActions}>
|
||||
{/* View Toggle */}
|
||||
|
|
@ -393,14 +404,14 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
|||
<button
|
||||
className={`${styles.viewToggleButton} ${useTableView ? styles.active : ''}`}
|
||||
onClick={() => setUseTableView(true)}
|
||||
title="Tabellenansicht"
|
||||
title={t('Tabellenansicht')}
|
||||
>
|
||||
<FaThList />
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.viewToggleButton} ${!useTableView ? styles.active : ''}`}
|
||||
onClick={() => setUseTableView(false)}
|
||||
title="Kartenansicht"
|
||||
title={t('Kartenansicht')}
|
||||
>
|
||||
<FaTh />
|
||||
</button>
|
||||
|
|
@ -410,7 +421,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
|||
className={styles.addButton}
|
||||
onClick={() => setShowAddForm(true)}
|
||||
>
|
||||
<FaPlus /> Neue Regel
|
||||
<FaPlus /> {t('Neue Regel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -431,7 +442,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
|
|||
<p className={styles.emptyText}>{getEmptyText()}</p>
|
||||
{!readOnly && (
|
||||
<p className={styles.emptyHint}>
|
||||
Klicken Sie auf "Neue Regel" um eine Berechtigung hinzuzufügen.
|
||||
{t('Klicken Sie auf „Neue Regel“, um eine Berechtigung hinzuzufügen.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -469,6 +480,7 @@ interface JsonEditorProps {
|
|||
}
|
||||
|
||||
const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) => {
|
||||
const { t } = useLanguage();
|
||||
const [jsonText, setJsonText] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -481,7 +493,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
|
|||
try {
|
||||
const parsed = JSON.parse(jsonText);
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('JSON muss ein Array sein');
|
||||
throw new Error(t('JSON muss ein Array sein'));
|
||||
}
|
||||
setError(null);
|
||||
onApply(parsed);
|
||||
|
|
@ -501,8 +513,9 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
|
|||
/>
|
||||
{error && <div className={styles.jsonError}>{error}</div>}
|
||||
<p className={styles.jsonHint}>
|
||||
Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON.
|
||||
Änderungen werden erst nach Klick auf "Anwenden" übernommen.
|
||||
{t(
|
||||
'Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON. Änderungen werden erst nach Klick auf „Anwenden“ übernommen.'
|
||||
)}
|
||||
</p>
|
||||
{!readOnly && (
|
||||
<div className={styles.formActions}>
|
||||
|
|
@ -512,7 +525,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
|
|||
onClick={handleApply}
|
||||
disabled={!!error}
|
||||
>
|
||||
JSON anwenden
|
||||
{t('JSON anwenden')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -607,7 +620,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
|||
setHasChanges(false);
|
||||
onSave?.();
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Speichern');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Speichern'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -655,7 +668,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
|||
<div className={styles.editorHeader}>
|
||||
<h3 className={styles.editorTitle}>
|
||||
Berechtigungen{roleName ? `: ${roleName}` : ''}
|
||||
{isTemplate && <span className={styles.templateBadge}>Template</span>}
|
||||
{isTemplate && <span className={styles.templateBadge}>{t('Vorlage')}</span>}
|
||||
</h3>
|
||||
{!readOnly && hasChanges && (
|
||||
<div className={styles.headerActions}>
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
onDelete,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const opTitle = (op: 'create' | 'read' | 'update' | 'delete') =>
|
||||
({ create: t('Erstellen'), read: t('Lesen'), update: t('Bearbeiten'), delete: t('Löschen') })[op];
|
||||
const handleLevelToggle = (
|
||||
field: 'read' | 'create' | 'update' | 'delete',
|
||||
targetLevel: 'm' | 'g' | 'a',
|
||||
|
|
@ -112,7 +114,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
checked={rule.view}
|
||||
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
|
||||
disabled={readOnly}
|
||||
title="Sichtbar"
|
||||
title={t('Sichtbar')}
|
||||
/>
|
||||
</td>
|
||||
|
||||
|
|
@ -127,7 +129,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
checked={hasLevel(rule[op] as AccessLevel, 'm')}
|
||||
onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)}
|
||||
disabled={readOnly}
|
||||
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Eigene`}
|
||||
title={`${opTitle(op)} - ${t('Eigene')}`}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -140,7 +142,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
checked={hasLevel(rule[op] as AccessLevel, 'g')}
|
||||
onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)}
|
||||
disabled={readOnly}
|
||||
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Gruppe`}
|
||||
title={`${opTitle(op)} - ${t('Gruppe')}`}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -153,7 +155,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
checked={hasLevel(rule[op] as AccessLevel, 'a')}
|
||||
onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)}
|
||||
disabled={readOnly}
|
||||
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Alle`}
|
||||
title={`${opTitle(op)} - ${t('Alle')}`}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -166,7 +168,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
<button
|
||||
className={`${styles.iconButton} ${styles.danger}`}
|
||||
onClick={() => onDelete(rule.id)}
|
||||
title={t('delete rule')}
|
||||
title={t('Regel löschen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -200,7 +202,7 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
|||
<thead>
|
||||
<tr>
|
||||
<th className={styles.colObject}>{t('object dot notation')}</th>
|
||||
<th className={styles.colView}>View</th>
|
||||
<th className={styles.colView}>{t('Ansicht')}</th>
|
||||
{isDataContext && (
|
||||
<>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th>
|
||||
|
|
@ -214,18 +216,18 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
|||
<tr className={styles.subHeader}>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th title="Create">C</th>
|
||||
<th title="Read">R</th>
|
||||
<th title="Update">U</th>
|
||||
<th title={t('delete')}>D</th>
|
||||
<th title="Create">C</th>
|
||||
<th title="Read">R</th>
|
||||
<th title="Update">U</th>
|
||||
<th title={t('delete')}>D</th>
|
||||
<th title="Create">C</th>
|
||||
<th title="Read">R</th>
|
||||
<th title="Update">U</th>
|
||||
<th title={t('delete')}>D</th>
|
||||
<th title={t('Erstellen')}>C</th>
|
||||
<th title={t('Lesen')}>R</th>
|
||||
<th title={t('Bearbeiten')}>U</th>
|
||||
<th title={t('Löschen')}>D</th>
|
||||
<th title={t('Erstellen')}>C</th>
|
||||
<th title={t('Lesen')}>R</th>
|
||||
<th title={t('Bearbeiten')}>U</th>
|
||||
<th title={t('Löschen')}>D</th>
|
||||
<th title={t('Erstellen')}>C</th>
|
||||
<th title={t('Lesen')}>R</th>
|
||||
<th title={t('Bearbeiten')}>U</th>
|
||||
<th title={t('Löschen')}>D</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* Simple text input with send button, usable by both Workspace and Editor.
|
||||
*/
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
|
|
@ -17,11 +18,13 @@ interface ChatInputProps {
|
|||
export const ChatInput: React.FC<ChatInputProps> = ({
|
||||
onSend,
|
||||
isProcessing,
|
||||
placeholder = 'Type a message...',
|
||||
placeholder,
|
||||
disabled,
|
||||
autoFocus = true,
|
||||
style,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedPlaceholder = placeholder ?? t('Nachricht eingeben…');
|
||||
const [value, setValue] = useState('');
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
|
|
@ -62,7 +65,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
|||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={_handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
placeholder={resolvedPlaceholder}
|
||||
disabled={isProcessing || disabled}
|
||||
rows={1}
|
||||
style={{
|
||||
|
|
@ -95,7 +98,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
|||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{isProcessing ? '...' : 'Send'}
|
||||
{isProcessing ? '…' : t('Senden')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
|
|
@ -31,9 +32,11 @@ const _roleColors: Record<string, string> = {
|
|||
export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
||||
messages,
|
||||
isProcessing,
|
||||
emptyMessage = 'No messages yet.',
|
||||
emptyMessage,
|
||||
style,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedEmpty = emptyMessage ?? t('Noch keine Nachrichten.');
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -55,7 +58,7 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
|||
>
|
||||
{messages.length === 0 && (
|
||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px', textAlign: 'center', marginTop: '24px' }}>
|
||||
{emptyMessage}
|
||||
{resolvedEmpty}
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
|
|
@ -80,7 +83,7 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
|
|||
))}
|
||||
{isProcessing && (
|
||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '12px', fontStyle: 'italic' }}>
|
||||
Processing...
|
||||
{t('Wird verarbeitet…')}
|
||||
</div>
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function ContentPreview({
|
|||
setError(t('Ungültige Datei-ID'));
|
||||
return;
|
||||
}
|
||||
if (!fileName || fileName === 'Unknown Item') {
|
||||
if (!fileName || fileName === 'Unknown Item' || fileName === 'Unbekanntes Element') {
|
||||
setError(t('Dateiname nicht verfügbar'));
|
||||
return;
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ export function ContentPreview({
|
|||
}
|
||||
setError(null);
|
||||
}
|
||||
}, [isOpen, fileId, fileName]);
|
||||
}, [isOpen, fileId, fileName, t]);
|
||||
|
||||
|
||||
const loadPreview = async () => {
|
||||
|
|
@ -95,7 +95,7 @@ export function ContentPreview({
|
|||
}
|
||||
// If it's text content but MIME type says PDF, we'll handle it in renderPreview
|
||||
} else {
|
||||
setError(result.error || 'Failed to load preview');
|
||||
setError(result.error || t('Vorschau konnte nicht geladen werden.'));
|
||||
}
|
||||
} catch (err) {
|
||||
setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
|
||||
|
|
@ -201,7 +201,7 @@ export function ContentPreview({
|
|||
</div>
|
||||
<pre className={styles.jsonPreview}>
|
||||
<code className={styles.jsonCode}>
|
||||
{previewContent || 'No content available'}
|
||||
{previewContent || t('Kein Inhalt verfügbar')}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ export function UrlContentPreview({
|
|||
|
||||
const warningTimeout = setTimeout(() => {
|
||||
if (isLoading && !hasLoaded) {
|
||||
setWarning('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.');
|
||||
setWarning(
|
||||
t('PDF lädt langsam. Sie können es auch direkt herunterladen oder in einem neuen Tab öffnen.')
|
||||
);
|
||||
// Don't set isLoading to false - let it continue
|
||||
}
|
||||
}, WARNING_TIMEOUT);
|
||||
|
|
@ -107,7 +109,7 @@ export function UrlContentPreview({
|
|||
console.log('PDF loading timeout, switching to PDF.js fallback');
|
||||
setUsePdfJs(true);
|
||||
setIsLoading(true); // Restart loading with PDF.js
|
||||
setWarning('PDF lädt langsam. Versuche alternative Anzeigemethode...');
|
||||
setWarning(t('PDF lädt langsam. Alternative Anzeigemethode wird versucht…'));
|
||||
} else if (isLoading && !hasLoaded && usePdfJs) {
|
||||
// PDF.js also failed, show error
|
||||
setShowPdfAnyway(true);
|
||||
|
|
@ -121,7 +123,7 @@ export function UrlContentPreview({
|
|||
clearTimeout(errorTimeout);
|
||||
};
|
||||
}
|
||||
}, [isOpen, isLoading, hasLoaded, usePdfJs]);
|
||||
}, [isOpen, isLoading, hasLoaded, usePdfJs, t]);
|
||||
|
||||
// Validate URL
|
||||
useEffect(() => {
|
||||
|
|
@ -184,7 +186,7 @@ export function UrlContentPreview({
|
|||
padding: '0.5rem 1rem'
|
||||
}}
|
||||
>
|
||||
In neuem Tab öffnen
|
||||
{t('In neuem Tab öffnen')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
|
|
@ -195,7 +197,7 @@ export function UrlContentPreview({
|
|||
padding: '0.5rem 1rem'
|
||||
}}
|
||||
>
|
||||
Download
|
||||
{t('Herunterladen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -241,7 +243,7 @@ export function UrlContentPreview({
|
|||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
In neuem Tab öffnen
|
||||
{t('In neuem Tab öffnen')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
|
|
@ -253,7 +255,7 @@ export function UrlContentPreview({
|
|||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Download File
|
||||
{t('Datei herunterladen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -284,7 +286,7 @@ export function UrlContentPreview({
|
|||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
In neuem Tab öffnen
|
||||
{t('In neuem Tab öffnen')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
|
|
@ -296,7 +298,7 @@ export function UrlContentPreview({
|
|||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
Download
|
||||
{t('Herunterladen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -316,7 +318,7 @@ export function UrlContentPreview({
|
|||
<div className={styles.fileName}>{fileName}</div>
|
||||
<p>{t('Vorschau wird hierfür nicht unterstützt')}</p>
|
||||
<button onClick={handleDownload} className={styles.retryButton}>
|
||||
Download File
|
||||
{t('Datei herunterladen')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
|||
<button
|
||||
className={styles.collapseButton}
|
||||
onClick={() => toggleCollapse(rowPath)}
|
||||
title={isCollapsed ? 'Expand' : 'Collapse'}
|
||||
title={isCollapsed ? t('Aufklappen') : t('Einklappen')}
|
||||
>
|
||||
{isCollapsed ? '▶' : '▼'}
|
||||
</button>
|
||||
|
|
@ -479,7 +479,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
|||
);
|
||||
} catch (parseError) {
|
||||
const rawData = {
|
||||
keys: ['Raw Content'],
|
||||
keys: [t('Rohinhalt')],
|
||||
values: [previewContent],
|
||||
types: ['string'],
|
||||
isNested: [false]
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export function PdfJsRenderer({
|
|||
} catch (err) {
|
||||
console.error('Error loading PDF with PDF.js:', err);
|
||||
if (isMounted) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load PDF');
|
||||
setError(err instanceof Error ? err.message : t('PDF konnte nicht geladen werden.'));
|
||||
setIsLoading(false);
|
||||
onError();
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ export function PdfJsRenderer({
|
|||
} catch (err) {
|
||||
console.error('Error rendering PDF page:', err);
|
||||
if (isMounted) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to render PDF page');
|
||||
setError(err instanceof Error ? err.message : t('PDF-Seite konnte nicht gerendert werden.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -132,7 +132,9 @@ export function PdfJsRenderer({
|
|||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorIcon}>⚠️</div>
|
||||
<p>Fehler beim Laden der PDF: {error}</p>
|
||||
<p>
|
||||
{t('Fehler beim Laden der PDF:')} {error}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
|
|||
|
||||
const LOG = '[Automation2]';
|
||||
|
||||
const DEFAULT_INVOCATIONS = (): WorkflowEntryPoint[] =>
|
||||
buildInvocationsForPrimaryKind('manual', [], 'Jetzt ausführen');
|
||||
const _buildDefaultInvocations = (runLabel: string): WorkflowEntryPoint[] =>
|
||||
buildInvocationsForPrimaryKind('manual', [], runLabel);
|
||||
|
||||
interface Automation2FlowEditorProps {
|
||||
instanceId: string;
|
||||
|
|
@ -106,7 +106,9 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||
const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [invocations, setInvocations] = useState<WorkflowEntryPoint[]>(DEFAULT_INVOCATIONS);
|
||||
const [invocations, setInvocations] = useState<WorkflowEntryPoint[]>(() =>
|
||||
_buildDefaultInvocations(t('Jetzt ausführen'))
|
||||
);
|
||||
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
|
||||
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
|
||||
const [tracingRunId, setTracingRunId] = useState<string | null>(null);
|
||||
|
|
@ -176,7 +178,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
|
||||
const applyGraphWithSync = useCallback(
|
||||
(graph: Automation2Graph | null | undefined, wfInvocations: WorkflowEntryPoint[] | undefined) => {
|
||||
const inv = wfInvocations?.length ? wfInvocations : DEFAULT_INVOCATIONS();
|
||||
const inv = wfInvocations?.length ? wfInvocations : _buildDefaultInvocations(t('Jetzt ausführen'));
|
||||
setInvocations(inv);
|
||||
if (!graph?.nodes?.length) {
|
||||
const synced = syncCanvasStartNode([], [], inv, nodeTypes, language);
|
||||
|
|
@ -189,7 +191,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
setCanvasNodes(synced.nodes);
|
||||
setCanvasConnections(synced.connections);
|
||||
},
|
||||
[nodeTypes, language]
|
||||
[nodeTypes, language, t]
|
||||
);
|
||||
|
||||
const handleFromApiGraph = useCallback(
|
||||
|
|
@ -202,7 +204,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
const handleExecute = useCallback(async () => {
|
||||
const graph = toApiGraph(canvasNodes, canvasConnections);
|
||||
if (graph.nodes.length === 0) {
|
||||
setExecuteResult({ success: false, error: 'Keine Nodes im Workflow.' });
|
||||
setExecuteResult({ success: false, error: t('Keine Nodes im Workflow.') });
|
||||
return;
|
||||
}
|
||||
setExecuting(true);
|
||||
|
|
@ -222,12 +224,12 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
} finally {
|
||||
setExecuting(false);
|
||||
}
|
||||
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations]);
|
||||
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations, t]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const graph = toApiGraph(canvasNodes, canvasConnections);
|
||||
if (graph.nodes.length === 0) {
|
||||
setExecuteResult({ success: false, error: 'Keine Nodes zum Speichern.' });
|
||||
setExecuteResult({ success: false, error: t('Keine Nodes zum Speichern.') });
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
|
|
@ -236,17 +238,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations });
|
||||
setExecuteResult({ success: true } as ExecuteGraphResponse);
|
||||
} else {
|
||||
const label = await promptInput('Workflow-Name:', {
|
||||
const label = await promptInput(t('Workflow-Name:'), {
|
||||
title: t('Workflow speichern'),
|
||||
defaultValue: 'Neuer Workflow',
|
||||
placeholder: 'Name des Workflows',
|
||||
defaultValue: t('Neuer Workflow'),
|
||||
placeholder: t('Name des Workflows'),
|
||||
});
|
||||
if (!label) {
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
const created = await createWorkflow(request, instanceId, {
|
||||
label: label.trim() || 'Neuer Workflow',
|
||||
label: label.trim() || t('Neuer Workflow'),
|
||||
graph,
|
||||
invocations,
|
||||
});
|
||||
|
|
@ -260,7 +262,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations]);
|
||||
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t]);
|
||||
|
||||
const handleLoad = useCallback(
|
||||
async (workflowId: string) => {
|
||||
|
|
@ -287,17 +289,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
if (workflowId) handleLoad(workflowId);
|
||||
else {
|
||||
setExecuteResult(null);
|
||||
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS());
|
||||
applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
|
||||
}
|
||||
},
|
||||
[handleLoad, applyGraphWithSync]
|
||||
[handleLoad, applyGraphWithSync, t]
|
||||
);
|
||||
|
||||
const handleNew = useCallback(() => {
|
||||
setCurrentWorkflowId(null);
|
||||
setExecuteResult(null);
|
||||
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS());
|
||||
}, [applyGraphWithSync]);
|
||||
applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
|
||||
}, [applyGraphWithSync, t]);
|
||||
|
||||
const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record<string, unknown>) => {
|
||||
setCanvasNodes((prev) =>
|
||||
|
|
@ -401,7 +403,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
if (loading || nodeTypes.length === 0) return;
|
||||
if (currentWorkflowId || initialWorkflowId) return;
|
||||
if (canvasNodes.length > 0) return;
|
||||
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS());
|
||||
applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
|
||||
}, [
|
||||
loading,
|
||||
nodeTypes.length,
|
||||
|
|
@ -409,6 +411,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
initialWorkflowId,
|
||||
canvasNodes.length,
|
||||
applyGraphWithSync,
|
||||
t,
|
||||
]);
|
||||
|
||||
const toggleCategory = useCallback((id: string) => {
|
||||
|
|
@ -591,7 +594,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
return (
|
||||
<div className={styles.sidebar} style={_sidebarStyle}>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<h3 className={styles.sidebarTitle}>Nodes</h3>
|
||||
<h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
|
||||
</div>
|
||||
<div className={styles.loading}>
|
||||
<FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} />
|
||||
|
|
@ -604,12 +607,12 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
return (
|
||||
<div className={styles.sidebar} style={_sidebarStyle}>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<h3 className={styles.sidebarTitle}>Nodes</h3>
|
||||
<h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
|
||||
</div>
|
||||
<div className={styles.error}>
|
||||
<p>{error}</p>
|
||||
<button className={styles.retryButton} onClick={loadNodeTypes}>
|
||||
Erneut versuchen
|
||||
{t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -648,7 +651,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`}
|
||||
onClick={() => setUdbTab(tab)}
|
||||
>
|
||||
{{ chats: 'Chats', files: 'Dateien', sources: 'Quellen' }[tab]}
|
||||
{{ chats: t('Chats'), files: t('Dateien'), sources: t('Quellen') }[tab]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -755,13 +758,13 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
|
||||
onClick={() => setRightTab('nodes')}
|
||||
>
|
||||
Nodes
|
||||
{t('Knoten')}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.rightTab} ${rightTab === 'tracing' ? styles.rightTabActive : ''}`}
|
||||
onClick={() => { setRightTab('tracing'); if (!tracingRunId) setTracingRunId('select'); }}
|
||||
>
|
||||
Tracing
|
||||
{t('Ablaufverfolgung')}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa';
|
||||
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
|
@ -118,7 +118,15 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
return () => document.removeEventListener('mousedown', _handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const SCOPE_LABELS: Record<string, string> = { user: 'Meine Vorlagen', instance: 'Instanz', mandate: 'Mandant' };
|
||||
const scopeLabels = useMemo(
|
||||
() =>
|
||||
({
|
||||
user: t('Meine Vorlagen'),
|
||||
instance: t('Instanz'),
|
||||
mandate: t('Mandant'),
|
||||
}) as Record<string, string>,
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.canvasHeader}>
|
||||
|
|
@ -139,14 +147,14 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
className={styles.canvasTitle}
|
||||
style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }}
|
||||
onClick={_startNameEdit}
|
||||
title={onWorkflowRename ? 'Klicken zum Umbenennen' : undefined}
|
||||
title={onWorkflowRename ? t('Klicken zum Umbenennen') : undefined}
|
||||
>
|
||||
{currentWorkflow.label}
|
||||
</h4>
|
||||
)
|
||||
) : (
|
||||
<h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}>
|
||||
Neuer Workflow
|
||||
{t('Neuer Workflow')}
|
||||
</h4>
|
||||
)}
|
||||
{onWorkflowSettings && (
|
||||
|
|
@ -154,7 +162,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
type="button"
|
||||
className={styles.canvasGearBtn}
|
||||
title={t('Workflowkonfiguration Einstieg/Starts')}
|
||||
aria-label="Workflow-Konfiguration"
|
||||
aria-label={t('Workflow-Konfiguration')}
|
||||
onClick={onWorkflowSettings}
|
||||
>
|
||||
<FaCog />
|
||||
|
|
@ -165,7 +173,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
<div ref={newMenuRef} style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<button type="button" className={styles.retryButton} onClick={onNew} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}>
|
||||
Neu
|
||||
{t('Neu')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -184,7 +192,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
onClick={() => { onNew(); setNewMenuOpen(false); }}
|
||||
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem' }}
|
||||
>
|
||||
Leerer Workflow
|
||||
{t('Leerer Workflow')}
|
||||
</button>
|
||||
{onNewFromTemplate && (
|
||||
<button
|
||||
|
|
@ -192,7 +200,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
onClick={() => { onNewFromTemplate(); setNewMenuOpen(false); }}
|
||||
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem', borderTop: '1px solid var(--border-color, #e0e0e0)' }}
|
||||
>
|
||||
Aus Vorlage...
|
||||
{t('Aus Vorlage…')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -205,7 +213,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
onClick={onSave}
|
||||
disabled={saving || !hasNodes}
|
||||
>
|
||||
{saving ? <FaSpinner className={styles.spinner} /> : 'Speichern'}
|
||||
{saving ? <FaSpinner className={styles.spinner} /> : t('Speichern')}
|
||||
</button>
|
||||
|
||||
{/* Save as template */}
|
||||
|
|
@ -229,7 +237,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
onClick={() => { onSaveAsTemplate(s); setTemplateMenuOpen(false); }}
|
||||
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem', borderTop: s !== 'user' ? '1px solid var(--border-color, #e0e0e0)' : undefined }}
|
||||
>
|
||||
{SCOPE_LABELS[s]}
|
||||
{scopeLabels[s]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -260,19 +268,19 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
{executing ? (
|
||||
<>
|
||||
<FaSpinner className={styles.spinner} style={{ marginRight: '0.5rem', display: 'inline-block' }} />
|
||||
Ausführen…
|
||||
{t('Ausführen…')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaPlay style={{ marginRight: '0.5rem' }} />
|
||||
Ausführen
|
||||
{t('Ausführen')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{onToggleChat && (
|
||||
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('Workspace-Panel: Chats, Dateien, Quellen')}>
|
||||
<FaDatabase style={{ marginRight: '0.4rem' }} />
|
||||
Workspace
|
||||
{t('Workspace')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -280,7 +288,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
{/* Version Selector */}
|
||||
{currentWorkflowId && versions && versions.length > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary, #666)' }}>Version:</span>
|
||||
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary, #666)' }}>{t('Version:')}</span>
|
||||
<select
|
||||
value={currentVersionId ?? ''}
|
||||
onChange={(e) => onVersionSelect?.(e.target.value || null)}
|
||||
|
|
@ -316,7 +324,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||
>
|
||||
<FaCloudUploadAlt style={{ marginRight: 4 }} />
|
||||
Publish
|
||||
{t('Veröffentlichen')}
|
||||
</button>
|
||||
)}
|
||||
{currentVersion && currentStatus === 'published' && onUnpublishVersion && (
|
||||
|
|
@ -329,7 +337,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||
>
|
||||
<FaCloudDownloadAlt style={{ marginRight: 4 }} />
|
||||
Unpublish
|
||||
{t('Veröffentlichung aufheben')}
|
||||
</button>
|
||||
)}
|
||||
{currentVersion && currentStatus !== 'archived' && onArchiveVersion && (
|
||||
|
|
@ -388,7 +396,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
|||
Task zu bearbeiten.
|
||||
</>
|
||||
) : (
|
||||
<>✗ {executeResult.error ?? 'Unbekannter Fehler'}</>
|
||||
<>✗ {executeResult.error ?? t('Unbekannter Fehler')}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -130,29 +130,29 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
|||
},
|
||||
onComplete: () => {
|
||||
if (!accumulated) {
|
||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: 'Done.' } : m));
|
||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: t('Fertig.') } : m));
|
||||
}
|
||||
onGraphUpdated?.();
|
||||
setLoading(false);
|
||||
},
|
||||
onError: (event) => {
|
||||
const errText = event.content || 'Request failed';
|
||||
const errText = event.content || t('Anfrage fehlgeschlagen');
|
||||
if (!accumulated) {
|
||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `Error: ${errText}` } : m));
|
||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `${t('Fehler')}: ${errText}` } : m));
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
onStopped: () => setLoading(false),
|
||||
},
|
||||
onConnectionError: (err) => {
|
||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `Error: ${err.message}` } : m));
|
||||
setMessages(prev => prev.map(m => m.id === assistantId ? { ...m, content: `${t('Fehler')}: ${err.message}` } : m));
|
||||
setLoading(false);
|
||||
},
|
||||
onStreamEnd: () => setLoading(false),
|
||||
});
|
||||
|
||||
abortRef.current = cleanup;
|
||||
}, [prompt, loading, workflowId, instanceId, messages, onGraphUpdated, pendingFiles, attachedDataSourceIds, attachedFeatureDataSourceIds]);
|
||||
}, [prompt, loading, workflowId, instanceId, messages, onGraphUpdated, pendingFiles, attachedDataSourceIds, attachedFeatureDataSourceIds, t]);
|
||||
|
||||
const _handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
|
|
@ -325,7 +325,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
|||
minWidth: 220, maxHeight: 260, overflowY: 'auto',
|
||||
}}>
|
||||
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>
|
||||
Active Sources auswählen
|
||||
{t('Aktive Quellen auswählen')}
|
||||
</div>
|
||||
{dataSources.map(ds => {
|
||||
const isSelected = attachedDataSourceIds.includes(ds.id);
|
||||
|
|
@ -354,7 +354,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
|||
{featureDataSources.length > 0 && (
|
||||
<>
|
||||
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderTop: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0' }}>
|
||||
Feature Data Sources
|
||||
{t('Feature-Datenquellen')}
|
||||
</div>
|
||||
{featureDataSources.map(fds => {
|
||||
const isSelected = attachedFeatureDataSourceIds.includes(fds.id);
|
||||
|
|
@ -394,7 +394,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
|||
<button onClick={() => abortRef.current?.()} style={{
|
||||
padding: '8px 14px', borderRadius: 8, border: 'none',
|
||||
background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600, fontSize: 12,
|
||||
}}>Stop</button>
|
||||
}}>{t('Stopp')}</button>
|
||||
) : (
|
||||
<button onClick={_handleSend} disabled={!prompt.trim() || !workflowId} style={{
|
||||
padding: '8px 14px', borderRadius: 8, border: 'none',
|
||||
|
|
|
|||
|
|
@ -560,17 +560,30 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
|||
>
|
||||
{selectedNodeIds.size > 1 && !selectedConnectionId && !connectingFrom && (
|
||||
<div className={styles.connectionHint}>
|
||||
{selectedNodeIds.size} Nodes ausgewählt • <kbd>Entf</kbd> zum Löschen • Ziehen zum Verschieben • <kbd>Shift</kbd>+Klick zum Hinzufügen/Entfernen
|
||||
{selectedNodeIds.size} {t('Knoten ausgewählt')}
|
||||
{' · '}
|
||||
<kbd>Entf</kbd> {t('zum Löschen')}
|
||||
{' · '}
|
||||
{t('Ziehen zum Verschieben')}
|
||||
{' · '}
|
||||
<kbd>Shift</kbd>
|
||||
{t('+Klick zum Hinzufügen oder Entfernen')}
|
||||
</div>
|
||||
)}
|
||||
{connectingFrom && !selectedConnectionId && (
|
||||
<div className={styles.connectionHint}>
|
||||
Ziehen Sie zum Eingang oder klicken Sie auf einen Eingang • <kbd>Esc</kbd> zum Abbrechen
|
||||
{t('Ziehen Sie zum Eingang oder klicken Sie auf einen Eingang')}
|
||||
{' · '}
|
||||
<kbd>Esc</kbd> {t('zum Abbrechen')}
|
||||
</div>
|
||||
)}
|
||||
{selectedConnectionId && (
|
||||
<div className={styles.connectionHint}>
|
||||
Pfeil ausgewählt • <kbd>Entf</kbd> zum Löschen • Klicken Sie auf einen anderen Eingang zum Umleiten
|
||||
{t('Verbindungspfeil ausgewählt')}
|
||||
{' · '}
|
||||
<kbd>Entf</kbd> {t('zum Löschen')}
|
||||
{' · '}
|
||||
{t('Anderen Eingang anklicken zum Umleiten')}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
|
|
@ -840,7 +853,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
|||
)}
|
||||
{nodes.length === 0 && (
|
||||
<div className={styles.canvasPlaceholder}>
|
||||
<p>{t('Nodes aus der Liste links')}</p>
|
||||
<p>{t('Knoten aus der Liste links ziehen')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
|||
placeholder={t('z.B. Kundenformular prüfen, Land')}
|
||||
/>
|
||||
<p className={styles.nodeConfigNameHint}>
|
||||
Wird im Data Picker angezeigt, um diesen Node zu identifizieren.
|
||||
{t('Wird im Data Picker angezeigt, um diesen Node zu identifizieren.')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({ nodeTypes,
|
|||
return (
|
||||
<div className={styles.sidebar} style={style}>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<h3 className={styles.sidebarTitle}>Nodes</h3>
|
||||
<h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.sidebarSearch}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
if (!runId) {
|
||||
return (
|
||||
<div style={{ padding: '16px', color: 'var(--text-secondary, #888)', fontSize: '13px' }}>
|
||||
Select a run to see tracing details.
|
||||
{t('Run auswählen, um Tracing-Details zu sehen.')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -180,7 +180,10 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
return (
|
||||
<div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '12px' }}>
|
||||
Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
|
||||
{t('Run-Schritte')}{' '}
|
||||
{loading && (
|
||||
<span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>({t('wird geladen…')})</span>
|
||||
)}
|
||||
</div>
|
||||
{steps.length === 0 && !loading && (
|
||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('Noch keine Schritte aufgezeichnet')}</div>
|
||||
|
|
@ -223,7 +226,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
{step.retryCount > 0 && (
|
||||
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}>
|
||||
{step.retryCount}x retry
|
||||
{step.retryCount}x {t('Wiederholung')}
|
||||
</span>
|
||||
)}
|
||||
{step.durationMs != null && (
|
||||
|
|
@ -244,11 +247,13 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
|
||||
)}
|
||||
{step.tokensUsed > 0 && (
|
||||
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>{step.tokensUsed} tokens</div>
|
||||
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>
|
||||
{step.tokensUsed} {t('Tokens')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CollapsibleSection label="Input" content={inputStr} />
|
||||
<CollapsibleSection label="Output" content={outputStr} />
|
||||
<CollapsibleSection label={t('Eingabe')} content={inputStr} />
|
||||
<CollapsibleSection label={t('Ausgabe')} content={outputStr} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* TemplatePicker - modal to browse and select a workflow template for creating a new workflow.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { FaSpinner } from 'react-icons/fa';
|
||||
import {
|
||||
fetchTemplates,
|
||||
|
|
@ -11,14 +11,7 @@ import {
|
|||
type ApiRequestFunction,
|
||||
} from '../../../api/workflowApi';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
const SCOPE_LABELS: Record<AutoTemplateScope | 'all', string> = {
|
||||
all: 'Alle',
|
||||
user: 'Meine',
|
||||
instance: 'Instanz',
|
||||
mandate: 'Mandant',
|
||||
system: 'System',
|
||||
};
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
interface TemplatePickerProps {
|
||||
open: boolean;
|
||||
|
|
@ -35,6 +28,18 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
instanceId,
|
||||
request,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const scopeLabels = useMemo(
|
||||
() =>
|
||||
({
|
||||
all: t('Alle'),
|
||||
user: t('Meine'),
|
||||
instance: t('Instanz'),
|
||||
mandate: t('Mandant'),
|
||||
system: t('System'),
|
||||
}) as Record<AutoTemplateScope | 'all', string>,
|
||||
[t]
|
||||
);
|
||||
const [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeScope, setActiveScope] = useState<AutoTemplateScope | 'all'>('all');
|
||||
|
|
@ -76,10 +81,10 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
<div className={styles.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="tpl-picker-title">
|
||||
<div className={styles.workflowModal} style={{ maxWidth: 600, maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<h3 id="tpl-picker-title" className={styles.workflowModalTitle}>
|
||||
Neu aus Vorlage
|
||||
{t('Neu aus Vorlage')}
|
||||
</h3>
|
||||
<p className={styles.workflowModalHint}>
|
||||
Wählen Sie eine Vorlage, um einen neuen Workflow zu erstellen.
|
||||
{t('Wählen Sie eine Vorlage, um einen neuen Workflow zu erstellen.')}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
|
|
@ -91,7 +96,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
onClick={() => setActiveScope(s)}
|
||||
style={{ fontSize: '0.8rem', padding: '4px 10px' }}
|
||||
>
|
||||
{SCOPE_LABELS[s]}
|
||||
{scopeLabels[s]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -103,14 +108,14 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
</div>
|
||||
) : templates.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: 24, color: 'var(--text-secondary, #888)' }}>
|
||||
Keine Vorlagen gefunden.
|
||||
{t('Keine Vorlagen gefunden.')}
|
||||
</div>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--border-color, #e0e0e0)', textAlign: 'left' }}>
|
||||
<th style={{ padding: '6px 8px' }}>Name</th>
|
||||
<th style={{ padding: '6px 8px', width: 80 }}>Scope</th>
|
||||
<th style={{ padding: '6px 8px' }}>{t('Name')}</th>
|
||||
<th style={{ padding: '6px 8px', width: 80 }}>{t('Scope')}</th>
|
||||
<th style={{ padding: '6px 8px', width: 100 }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -119,7 +124,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
<tr key={tpl.id} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
|
||||
<td style={{ padding: '8px' }}>{tpl.label}</td>
|
||||
<td style={{ padding: '8px', fontSize: '0.8rem', color: 'var(--text-secondary, #888)' }}>
|
||||
{SCOPE_LABELS[(tpl.templateScope as AutoTemplateScope) || 'user']}
|
||||
{scopeLabels[(tpl.templateScope as AutoTemplateScope) || 'user']}
|
||||
</td>
|
||||
<td style={{ padding: '8px', textAlign: 'right' }}>
|
||||
<button
|
||||
|
|
@ -129,7 +134,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
onClick={() => _handleSelect(tpl.id)}
|
||||
disabled={copying !== null}
|
||||
>
|
||||
{copying === tpl.id ? <FaSpinner className={styles.spinner} /> : 'Übernehmen'}
|
||||
{copying === tpl.id ? <FaSpinner className={styles.spinner} /> : t('Übernehmen')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -141,7 +146,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
|
|||
|
||||
<div className={styles.workflowModalActions} style={{ marginTop: 12 }}>
|
||||
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
|
||||
Abbrechen
|
||||
{t('Abbrechen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const label =
|
||||
titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || 'Start';
|
||||
titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || t('Start');
|
||||
const next = buildInvocationsForPrimaryKind(kind, invocations, label);
|
||||
onApply(next);
|
||||
onClose();
|
||||
|
|
@ -74,15 +74,16 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
<div className={styles.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="wf-cfg-title">
|
||||
<div className={styles.workflowModal}>
|
||||
<h3 id="wf-cfg-title" className={styles.workflowModalTitle}>
|
||||
Workflow-Konfiguration
|
||||
{t('Workflow-Konfiguration')}
|
||||
</h3>
|
||||
<p className={styles.workflowModalHint}>
|
||||
Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem
|
||||
gewählten Einstieg an (z. B. Formular-Felder auf der Start-Node bearbeiten).
|
||||
{t(
|
||||
'Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem gewählten Einstieg an (z. B. Formular-Felder auf der Start-Node bearbeiten).'
|
||||
)}
|
||||
</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label className={styles.workflowModalLabel} htmlFor="wf-start-title">
|
||||
Titel der Start Node
|
||||
{t('Titel der Start Node')}
|
||||
</label>
|
||||
<input
|
||||
id="wf-start-title"
|
||||
|
|
@ -92,7 +93,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
placeholder={t('z.B. Angebot anlegen')}
|
||||
/>
|
||||
|
||||
<div className={styles.workflowModalRadioGroup} role="radiogroup" aria-label="Einstiegsart">
|
||||
<div className={styles.workflowModalRadioGroup} role="radiogroup" aria-label={t('Einstiegsart')}>
|
||||
{kindOptions.map((o) => (
|
||||
<label key={o.value} className={styles.workflowModalRadio}>
|
||||
<input
|
||||
|
|
@ -109,10 +110,10 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
|
||||
<div className={styles.workflowModalActions}>
|
||||
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
|
||||
Abbrechen
|
||||
{t('Abbrechen')}
|
||||
</button>
|
||||
<button type="submit" className={styles.workflowModalBtnPrimary}>
|
||||
Übernehmen
|
||||
{t('Übernehmen')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
|
||||
return (
|
||||
<div>
|
||||
<label>Felder</label>
|
||||
<label>{t('Felder')}</label>
|
||||
<div className={styles.formFieldsList}>
|
||||
{fields.map((f, i) => (
|
||||
<div
|
||||
|
|
@ -87,7 +87,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
</span>
|
||||
<div className={styles.formFieldInputs}>
|
||||
<input
|
||||
placeholder="name"
|
||||
placeholder={t('name')}
|
||||
value={f.name ?? ''}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
|
|
@ -96,7 +96,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
}}
|
||||
/>
|
||||
<input
|
||||
placeholder="label"
|
||||
placeholder={t('label')}
|
||||
value={f.label ?? ''}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
|
|
@ -111,13 +111,13 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
value={f.type ?? 'string'}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
const t = e.target.value;
|
||||
const fieldType = e.target.value;
|
||||
next[i] = {
|
||||
...next[i],
|
||||
type: t,
|
||||
...(t === 'clickup_tasks'
|
||||
type: fieldType,
|
||||
...(fieldType === 'clickup_tasks'
|
||||
? { clickupStatusOptions: undefined }
|
||||
: t === 'clickup_status'
|
||||
: fieldType === 'clickup_status'
|
||||
? { clickupConnectionId: undefined, clickupListId: undefined }
|
||||
: {
|
||||
clickupConnectionId: undefined,
|
||||
|
|
@ -129,10 +129,10 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
}}
|
||||
style={{ width: 'auto', minWidth: 90 }}
|
||||
>
|
||||
<option value="string">Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="boolean">Checkbox</option>
|
||||
<option value="string">{t('Text')}</option>
|
||||
<option value="number">{t('Zahl')}</option>
|
||||
<option value="date">{t('Datum')}</option>
|
||||
<option value="boolean">{t('Kontrollkästchen')}</option>
|
||||
<option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
|
||||
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
|
||||
</select>
|
||||
|
|
@ -146,7 +146,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
updateParam('fields', next);
|
||||
}}
|
||||
/>
|
||||
Pflichtfeld
|
||||
{t('Pflichtfeld')}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -161,13 +161,16 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%', fontSize: '0.8rem', color: 'var(--text-secondary, #666)' }}>
|
||||
{Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? (
|
||||
<p style={{ margin: '0 0 6px' }}>
|
||||
Dropdown mit {f.clickupStatusOptions.length} Status aus der ClickUp-Liste (Wert = exakter
|
||||
Status-Name für die API).
|
||||
{t(
|
||||
'Dropdown mit {count} Status aus der ClickUp-Liste (Wert = exakter Status-Name für die API).',
|
||||
{ count: String(f.clickupStatusOptions.length) }
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p style={{ margin: '0 0 6px' }}>
|
||||
Keine Optionen — im ClickUp-Knoten „Aufgabe erstellen“ Liste wählen und „Formular mit Liste
|
||||
abgleichen“.
|
||||
{t(
|
||||
'Keine Optionen — im ClickUp-Knoten „Aufgabe erstellen“ Liste wählen und „Formular mit Liste abgleichen“.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -175,7 +178,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
{f.type === 'clickup_tasks' ? (
|
||||
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
|
||||
ClickUp-Verbindung
|
||||
{t('ClickUp-Verbindung')}
|
||||
</label>
|
||||
<select
|
||||
value={f.clickupConnectionId ?? ''}
|
||||
|
|
@ -187,7 +190,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
disabled={connectionsLoading || !instanceId}
|
||||
style={{ width: '100%', marginBottom: 8 }}
|
||||
>
|
||||
<option value="">{connectionsLoading ? 'Lade…' : 'Verbindung wählen…'}</option>
|
||||
<option value="">{connectionsLoading ? t('Lade…') : t('Verbindung wählen…')}</option>
|
||||
{connections.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.externalUsername ?? c.id}
|
||||
|
|
@ -195,7 +198,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
))}
|
||||
</select>
|
||||
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
|
||||
Listen-ID (verknüpfte Liste / Ziel-Liste)
|
||||
{t('Listen-ID (verknüpfte Liste / Ziel-Liste)')}
|
||||
</label>
|
||||
<input
|
||||
placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
|
||||
|
|
@ -208,9 +211,9 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
style={{ width: '100%' }}
|
||||
/>
|
||||
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginTop: 6 }}>
|
||||
Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:{' '}
|
||||
<code>{'{ add: [taskId], rem: [] }'}</code> — im ClickUp-Node per Datenquelle auf das
|
||||
Formularfeld mappen.
|
||||
{t('Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:')}{' '}
|
||||
<code>{'{ add: [taskId], rem: [] }'}</code>{' '}
|
||||
{t('— im ClickUp-Node per Datenquelle auf das Formularfeld mappen.')}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -222,7 +225,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }])
|
||||
}
|
||||
>
|
||||
+ Feld
|
||||
+ {t('Feld')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
|
|||
};
|
||||
|
||||
const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
|
||||
const { t } = useLanguage();
|
||||
const dependsOn = param.frontendOptions?.dependsOn as string | undefined;
|
||||
const depValue = dependsOn ? allParams?.[dependsOn] : undefined;
|
||||
const disabled = dependsOn && !depValue;
|
||||
|
|
@ -191,7 +192,7 @@ const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, al
|
|||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={!!disabled}
|
||||
placeholder={disabled ? `Select ${dependsOn} first` : param.name}
|
||||
placeholder={disabled ? t('Zuerst {field} wählen', { field: dependsOn ?? '' }) : param.name}
|
||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', opacity: disabled ? 0.5 : 1 }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -214,9 +215,9 @@ const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }
|
|||
{cases.map((c: Record<string, unknown>, i: number) => (
|
||||
<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="eq">{t('ist gleich')}</option>
|
||||
<option value="neq">{t('ungleich')}</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="contains">{t('enthält')}</option>
|
||||
<option value="gt">{t('größer als')}</option>
|
||||
<option value="lt">{t('kleiner als')}</option>
|
||||
</select>
|
||||
|
|
@ -244,18 +245,18 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
{fields.map((f: Record<string, unknown>, i: number) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}>
|
||||
<input type="text" placeholder="Name" value={String(f.name ?? '')} onChange={(e) => updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder={t('Name')} value={String(f.name ?? '')} onChange={(e) => updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<select value={String(f.type ?? 'text')} onChange={(e) => updateField(i, 'type', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
<option value="select">Select</option>
|
||||
<option value="textarea">Textarea</option>
|
||||
<option value="text">{t('Text')}</option>
|
||||
<option value="number">{t('Zahl')}</option>
|
||||
<option value="date">{t('Datum')}</option>
|
||||
<option value="checkbox">{t('Kontrollkästchen')}</option>
|
||||
<option value="select">{t('Auswahl')}</option>
|
||||
<option value="textarea">{t('Mehrzeilig')}</option>
|
||||
</select>
|
||||
<input type="text" placeholder="Label" value={String(f.label ?? '')} onChange={(e) => updateField(i, 'label', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder={t('Bezeichnung')} value={String(f.label ?? '')} onChange={(e) => updateField(i, 'label', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<label style={{ fontSize: 11, display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<input type="checkbox" checked={Boolean(f.required)} onChange={(e) => updateField(i, 'required', e.target.checked)} /> Req
|
||||
<input type="checkbox" checked={Boolean(f.required)} onChange={(e) => updateField(i, 'required', e.target.checked)} /> {t('Pflicht')}
|
||||
</label>
|
||||
<button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
|
|
@ -280,8 +281,8 @@ const KeyValueRowsEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
{rows.map((r: Record<string, unknown>, i: number) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||
<input type="text" placeholder="Key" value={String(r.key ?? r.fieldKey ?? '')} onChange={(e) => updateRow(i, 'key', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder="Value" value={String(r.value ?? '')} onChange={(e) => updateRow(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder={t('Schlüssel')} value={String(r.key ?? r.fieldKey ?? '')} onChange={(e) => updateRow(i, 'key', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder={t('Wert')} value={String(r.value ?? '')} onChange={(e) => updateRow(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -299,7 +300,7 @@ const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) =
|
|||
type="text"
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={t('index.5')}
|
||||
placeholder={t('0 9 * * *')}
|
||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }}
|
||||
/>
|
||||
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('Cron: Min Stunde Tag Monat')}</p>
|
||||
|
|
@ -316,17 +317,17 @@ const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange
|
|||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
<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="eq">{t('ist gleich')}</option>
|
||||
<option value="neq">{t('ungleich')}</option>
|
||||
<option value="gt">{t('größer als')}</option>
|
||||
<option value="lt">{t('kleiner als')}</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="contains">{t('enthält')}</option>
|
||||
<option value="empty">{t('ist leer')}</option>
|
||||
<option value="not_empty">{t('ist nicht leer')}</option>
|
||||
<option value="is_true">{t('ist wahr')}</option>
|
||||
<option value="is_false">{t('ist falsch')}</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' }} />
|
||||
<input type="text" placeholder={t('Wert')} value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -366,18 +367,18 @@ const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, on
|
|||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<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' }} />
|
||||
<input type="text" placeholder={t('Feld')} 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="eq">{t('ist gleich')}</option>
|
||||
<option value="neq">{t('ungleich')}</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="contains">{t('enthält')}</option>
|
||||
<option value="startsWith">{t('beginnt mit')}</option>
|
||||
<option value="isEmpty">{t('ist leer')}</option>
|
||||
<option value="isNotEmpty">{t('ist nicht leer')}</option>
|
||||
<option value="gt">{t('größer als')}</option>
|
||||
<option value="lt">{t('kleiner als')}</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' }} />
|
||||
<input type="text" placeholder={t('Wert')} value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
return (
|
||||
<div className={styles.ifElseConditionEditor}>
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Datenquelle</label>
|
||||
<label>{t('Datenquelle')}</label>
|
||||
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
|
||||
</div>
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
|
|
@ -114,7 +114,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
</div>
|
||||
{needsValue && (
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Wert</label>
|
||||
<label>{t('Wert')}</label>
|
||||
{mimeTypeOptions.length > 0 ? (
|
||||
<select
|
||||
value={String(value ?? '')}
|
||||
|
|
|
|||
|
|
@ -45,15 +45,19 @@ function _buildPathsFromSchema(
|
|||
return result;
|
||||
}
|
||||
|
||||
function _buildPathsFromPreview(obj: unknown, basePath: (string | number)[] = []): PickablePath[] {
|
||||
const pathLabel = basePath.length ? basePath.map(String).join(' → ') : '(ganze Ausgabe)';
|
||||
function _buildPathsFromPreview(
|
||||
obj: unknown,
|
||||
basePath: (string | number)[] = [],
|
||||
wholeOutputLabel = '(ganze Ausgabe)',
|
||||
): PickablePath[] {
|
||||
const pathLabel = basePath.length ? basePath.map(String).join(' → ') : wholeOutputLabel;
|
||||
if (obj == null || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
|
||||
return [{ path: [...basePath], label: pathLabel }];
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
|
||||
for (let i = 0; i < Math.min(obj.length, 5); i++) {
|
||||
result.push(..._buildPathsFromPreview(obj[i], [...basePath, i]));
|
||||
result.push(..._buildPathsFromPreview(obj[i], [...basePath, i], wholeOutputLabel));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -61,7 +65,7 @@ function _buildPathsFromPreview(obj: unknown, basePath: (string | number)[] = []
|
|||
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
|
||||
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
||||
if (k.startsWith('_')) continue;
|
||||
result.push(..._buildPathsFromPreview(v, [...basePath, k]));
|
||||
result.push(..._buildPathsFromPreview(v, [...basePath, k], wholeOutputLabel));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -160,7 +164,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
|||
onClick={() => setShowSystem(!showSystem)}
|
||||
>
|
||||
<span className={styles.dataPickerExpandIcon}>{showSystem ? '▼' : '▶'}</span>
|
||||
<span className={styles.dataPickerNodeLabel}>System</span>
|
||||
<span className={styles.dataPickerNodeLabel}>{t('System')}</span>
|
||||
</button>
|
||||
{showSystem && (
|
||||
<div className={styles.dataPickerTree}>
|
||||
|
|
@ -200,7 +204,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
|||
const schemaPaths = _buildPathsFromSchema(resolvedSchema);
|
||||
const paths = schemaPaths.length > 0
|
||||
? schemaPaths
|
||||
: _buildPathsFromPreview(nodeOutputsPreview[nodeId]);
|
||||
: _buildPathsFromPreview(nodeOutputsPreview[nodeId], [], t('(ganze Ausgabe)'));
|
||||
|
||||
return (
|
||||
<div key={nodeId} className={styles.dataPickerNodeSection}>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ function buildLoopOptions(
|
|||
sourceIds: string[],
|
||||
nodes: Array<{ id: string; type?: string; title?: string; parameters?: Record<string, unknown> }>,
|
||||
nodeOutputsPreview: Record<string, unknown>,
|
||||
getNodeLabel: (n: { id: string; type?: string; title?: string }) => string
|
||||
getNodeLabel: (n: { id: string; type?: string; title?: string }) => string,
|
||||
translate: (key: string) => string
|
||||
): LoopOption[] {
|
||||
const options: LoopOption[] = [];
|
||||
|
||||
|
|
@ -50,13 +51,13 @@ function buildLoopOptions(
|
|||
if (node?.type === 'trigger.form') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['payload']),
|
||||
label: `Alle Formularfelder (${nodeLabel})`,
|
||||
label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
|
||||
});
|
||||
const filesVal = getValueAtPath(preview, ['files']);
|
||||
if (Array.isArray(filesVal)) {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['files']),
|
||||
label: `Alle Dateien aus Formular (${nodeLabel})`,
|
||||
label: `${translate('Alle Dateien aus Formular')} (${nodeLabel})`,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
|
|
@ -65,7 +66,7 @@ function buildLoopOptions(
|
|||
if (node?.type === 'input.form') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, []),
|
||||
label: `Alle Formularfelder (${nodeLabel})`,
|
||||
label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -73,11 +74,11 @@ function buildLoopOptions(
|
|||
if (node?.type === 'input.upload') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['files']),
|
||||
label: `Alle hochgeladenen Dateien (${nodeLabel})`,
|
||||
label: `${translate('Alle hochgeladenen Dateien')} (${nodeLabel})`,
|
||||
});
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['fileIds']),
|
||||
label: `Alle Datei-IDs (${nodeLabel})`,
|
||||
label: `${translate('Alle Datei-IDs')} (${nodeLabel})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -85,7 +86,7 @@ function buildLoopOptions(
|
|||
if (node?.type === 'flow.loop') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['items']),
|
||||
label: `Alle Elemente aus Schleife (${nodeLabel})`,
|
||||
label: `${translate('Alle Elemente aus Schleife')} (${nodeLabel})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -93,7 +94,7 @@ function buildLoopOptions(
|
|||
if (node?.type === 'email.searchEmail') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['data', 'searchResults', 'results']),
|
||||
label: `Alle gefundenen E-Mails (${nodeLabel})`,
|
||||
label: `${translate('Alle gefundenen E-Mails')} (${nodeLabel})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -101,7 +102,7 @@ function buildLoopOptions(
|
|||
if (node?.type === 'email.checkEmail') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['data', 'emails', 'emails']),
|
||||
label: `Alle E-Mails (${nodeLabel})`,
|
||||
label: `${translate('Alle E-Mails')} (${nodeLabel})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -109,7 +110,7 @@ function buildLoopOptions(
|
|||
if (node?.type === 'sharepoint.listFiles') {
|
||||
options.push({
|
||||
ref: createRef(nodeId, ['files']),
|
||||
label: `Alle Dateien (${nodeLabel})`,
|
||||
label: `${translate('Alle Dateien')} (${nodeLabel})`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -157,7 +158,7 @@ interface LoopItemsSelectProps {
|
|||
|
||||
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||
onChange,
|
||||
placeholder = 'Über was soll iteriert werden?',
|
||||
placeholder,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
|
|
@ -167,7 +168,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
|||
if (sourceIds.length === 0) {
|
||||
return (
|
||||
<p className={styles.dynamicValueEmptyHint}>
|
||||
Keine vorherigen Nodes verbunden. Verbinden Sie zuerst Nodes mit der Schleife.
|
||||
{t('Keine vorherigen Nodes verbunden. Verbinden Sie zuerst Nodes mit der Schleife.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
|
@ -176,7 +177,8 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
|||
sourceIds,
|
||||
dataFlow.nodes,
|
||||
dataFlow.nodeOutputsPreview,
|
||||
dataFlow.getNodeLabel
|
||||
dataFlow.getNodeLabel,
|
||||
t
|
||||
);
|
||||
|
||||
const ref = isRef(value) ? value : null;
|
||||
|
|
@ -198,7 +200,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
|||
}}
|
||||
className={styles.startsInput}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
<option value="">{placeholder ?? t('Über was soll iteriert werden?')}</option>
|
||||
{options.map((o) => (
|
||||
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
||||
{o.label}
|
||||
|
|
@ -206,7 +208,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
|||
))}
|
||||
</select>
|
||||
<p className={styles.nodeConfigNameHint}>
|
||||
Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.
|
||||
{t('Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import React from 'react';
|
||||
import { createRef, isRef, isValue, createValue, type DataRef } from './dataRef';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
/** How to build path options for StatischKontextSelect / RefSourceSelect. */
|
||||
export type PathPickMode = 'default' | 'clickup_task_id' | 'exclude_forms';
|
||||
|
|
@ -131,6 +132,11 @@ export function refToOptionValue(ref: DataRef): string {
|
|||
return JSON.stringify(ref);
|
||||
}
|
||||
|
||||
function _pathLabelForDisplay(pathLabel: string, translate: (key: string) => string): string {
|
||||
if (pathLabel === 'Aufgaben-ID') return translate('Aufgaben-ID');
|
||||
return pathLabel;
|
||||
}
|
||||
|
||||
export function optionValueToRef(s: string): DataRef | null {
|
||||
try {
|
||||
const o = JSON.parse(s) as unknown;
|
||||
|
|
@ -190,10 +196,11 @@ interface StatischKontextSelectProps {
|
|||
export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '— Quelle wählen —',
|
||||
staticLabel = 'Statisch',
|
||||
placeholder,
|
||||
staticLabel,
|
||||
pathPickMode = 'default',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
if (!dataFlow) return null;
|
||||
|
||||
|
|
@ -213,7 +220,8 @@ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
|||
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
|
||||
const paths = pickPathsForNode(node, preview, pathPickMode);
|
||||
for (const p of paths) {
|
||||
const displayLabel = p.pathLabel ? `${nodeLabel} → ${p.pathLabel}` : nodeLabel;
|
||||
const pathLabelUi = _pathLabelForDisplay(p.pathLabel, t);
|
||||
const displayLabel = pathLabelUi ? `${nodeLabel} → ${pathLabelUi}` : nodeLabel;
|
||||
options.push({
|
||||
ref: createRef(nodeId, p.path),
|
||||
label: displayLabel,
|
||||
|
|
@ -245,8 +253,8 @@ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
|
|||
if (ref) onChange(ref);
|
||||
}}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
<option value={STATIC_SOURCE_VALUE}>{staticLabel}</option>
|
||||
<option value="">{placeholder ?? t('— Quelle wählen —')}</option>
|
||||
<option value={STATIC_SOURCE_VALUE}>{staticLabel ?? t('Statisch')}</option>
|
||||
{options.map((o) => (
|
||||
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
||||
{o.label}
|
||||
|
|
@ -267,9 +275,10 @@ interface RefSourceSelectProps {
|
|||
export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Datenquelle wählen…',
|
||||
placeholder,
|
||||
pathPickMode = 'default',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
if (!dataFlow) return null;
|
||||
|
||||
|
|
@ -289,7 +298,8 @@ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
|||
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
|
||||
const paths = pickPathsForNode(node, preview, pathPickMode);
|
||||
for (const p of paths) {
|
||||
const displayLabel = p.pathLabel ? `${nodeLabel} → ${p.pathLabel}` : nodeLabel;
|
||||
const pathLabelUi = _pathLabelForDisplay(p.pathLabel, t);
|
||||
const displayLabel = pathLabelUi ? `${nodeLabel} → ${pathLabelUi}` : nodeLabel;
|
||||
options.push({
|
||||
ref: createRef(nodeId, p.path),
|
||||
label: displayLabel,
|
||||
|
|
@ -312,7 +322,7 @@ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
|
|||
if (ref) onChange(ref);
|
||||
}}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
<option value="">{placeholder ?? t('Datenquelle wählen…')}</option>
|
||||
{options.map((o) => (
|
||||
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
|
||||
{o.label}
|
||||
|
|
@ -343,13 +353,13 @@ function getFormFieldType(
|
|||
if (!fieldName) return null;
|
||||
const field = raw.find((f: unknown) => f && typeof f === 'object' && (f as Record<string, unknown>).name === fieldName);
|
||||
if (!field || typeof field !== 'object') return null;
|
||||
const t = String((field as Record<string, unknown>).type ?? 'text').toLowerCase();
|
||||
if (t === 'number') return 'number';
|
||||
if (t === 'email') return 'email';
|
||||
if (t === 'date' || t === 'datetime') return 'date';
|
||||
if (t === 'boolean' || t === 'checkbox') return 'boolean';
|
||||
if (t === 'clickup_tasks') return 'string';
|
||||
if (t === 'clickup_status') return 'string';
|
||||
const rawFieldType = String((field as Record<string, unknown>).type ?? 'text').toLowerCase();
|
||||
if (rawFieldType === 'number') return 'number';
|
||||
if (rawFieldType === 'email') return 'email';
|
||||
if (rawFieldType === 'date' || rawFieldType === 'datetime') return 'date';
|
||||
if (rawFieldType === 'boolean' || rawFieldType === 'checkbox') return 'boolean';
|
||||
if (rawFieldType === 'clickup_tasks') return 'string';
|
||||
if (rawFieldType === 'clickup_status') return 'string';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
|
|||
const o = f as Record<string, unknown>;
|
||||
const fieldType = String(o.type ?? 'text');
|
||||
const name = String(o.name ?? `field${i + 1}`);
|
||||
const label = String(o.label ?? `Feld ${i + 1}`);
|
||||
const label = String(o.label ?? `${t('Feld')} ${i + 1}`);
|
||||
const type = (
|
||||
FORM_FIELD_TYPES.includes(fieldType as (typeof FORM_FIELD_TYPES)[number]) ? fieldType : 'text'
|
||||
) as FormField['type'];
|
||||
|
|
@ -39,7 +39,7 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
|
|||
}
|
||||
return { name, label, type };
|
||||
}
|
||||
return { name: `field${i + 1}`, label: `Feld ${i + 1}`, type: 'text' as const };
|
||||
return { name: `field${i + 1}`, label: `${t('Feld')} ${i + 1}`, type: 'text' as const };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -54,8 +54,9 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
return (
|
||||
<div className={styles.startNodeDoc}>
|
||||
<p className={styles.startNodeDocIntro}>
|
||||
<strong>Formular-Felder</strong> werden beim Start ausgefüllt und liegen unter{' '}
|
||||
<code>payload.<name></code> in der Start-Ausgabe.
|
||||
<strong>{t('Formular-Felder')}</strong>{' '}
|
||||
{t('werden beim Start ausgefüllt und liegen unter')}{' '}
|
||||
<code>payload.<name></code> {t('in der Start-Ausgabe.')}
|
||||
</p>
|
||||
<div className={styles.formFieldsList}>
|
||||
{fields.map((f, idx) => (
|
||||
|
|
@ -72,7 +73,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
/>
|
||||
<input
|
||||
className={styles.startsInput}
|
||||
placeholder="Beschriftung"
|
||||
placeholder={t('Beschriftung')}
|
||||
value={f.label}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
|
|
@ -94,11 +95,11 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
setFields(next);
|
||||
}}
|
||||
>
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Zahl</option>
|
||||
<option value="email">E-Mail</option>
|
||||
<option value="date">Datum</option>
|
||||
<option value="boolean">Ja/Nein</option>
|
||||
<option value="text">{t('Text')}</option>
|
||||
<option value="number">{t('Zahl')}</option>
|
||||
<option value="email">{t('E-Mail')}</option>
|
||||
<option value="date">{t('Datum')}</option>
|
||||
<option value="boolean">{t('Ja/Nein')}</option>
|
||||
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
|
||||
</select>
|
||||
<button
|
||||
|
|
@ -117,7 +118,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
setFields([...fields, { name: `field${fields.length + 1}`, label: t('Neues Feld'), type: 'text' }])
|
||||
}
|
||||
>
|
||||
+ Feld
|
||||
{t('+ Feld')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -209,8 +209,9 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
return (
|
||||
<div className={styles.schedulePanel}>
|
||||
<p className={styles.startNodeDocIntro}>
|
||||
Legen Sie fest, <strong>wann</strong> dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird
|
||||
unten automatisch erzeugt.
|
||||
{t(
|
||||
'Legen Sie fest, wann dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird unten automatisch erzeugt.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
<LayoutGroup>
|
||||
|
|
@ -255,7 +256,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
<div className={styles.scheduleModeConfig}>
|
||||
{o.value === 'daily' && (
|
||||
<label className={styles.scheduleFieldRow}>
|
||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||
<input
|
||||
type="time"
|
||||
step={60}
|
||||
|
|
@ -268,7 +269,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
|
||||
{o.value === 'weekdays' && (
|
||||
<label className={styles.scheduleFieldRow}>
|
||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||
<input
|
||||
type="time"
|
||||
step={60}
|
||||
|
|
@ -282,7 +283,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
{o.value === 'weekly' && (
|
||||
<>
|
||||
<div className={styles.scheduleFieldCol}>
|
||||
<span className={styles.scheduleFieldLabel}>Wochentage</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Wochentage')}</span>
|
||||
<div className={styles.scheduleWeekdayToggles}>
|
||||
{WEEKDAYS_MO_SO.map(({ cronDow, label }) => (
|
||||
<button
|
||||
|
|
@ -293,13 +294,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
}
|
||||
onClick={() => toggleWeekday(cronDow)}
|
||||
>
|
||||
{label}
|
||||
{t(label)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<label className={styles.scheduleFieldRow}>
|
||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||
<input
|
||||
type="time"
|
||||
step={60}
|
||||
|
|
@ -323,7 +324,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
}
|
||||
onClick={() => setCalendarPeriod('monthly')}
|
||||
>
|
||||
Monatlich
|
||||
{t('Monatlich')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -334,13 +335,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
}
|
||||
onClick={() => setCalendarPeriod('yearly')}
|
||||
>
|
||||
Jährlich
|
||||
{t('Jährlich')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{spec.calendarPeriod === 'monthly' && (
|
||||
<label className={styles.scheduleFieldRow}>
|
||||
<span className={styles.scheduleFieldLabel}>Monatstag</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Monatstag')}</span>
|
||||
<select
|
||||
className={styles.scheduleSelect}
|
||||
value={spec.monthDay}
|
||||
|
|
@ -358,7 +359,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
{spec.calendarPeriod === 'yearly' && (
|
||||
<div className={styles.scheduleYearlyRow}>
|
||||
<label className={styles.scheduleFieldRowGrow}>
|
||||
<span className={styles.scheduleFieldLabel}>Monat</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Monat')}</span>
|
||||
<select
|
||||
className={styles.scheduleSelect}
|
||||
value={spec.monthIndex}
|
||||
|
|
@ -366,13 +367,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
>
|
||||
{MONTH_NAMES_DE.map((name, i) => (
|
||||
<option key={i + 1} value={i + 1}>
|
||||
{name}
|
||||
{t(name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className={styles.scheduleFieldRowGrow}>
|
||||
<span className={styles.scheduleFieldLabel}>Tag</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Tag')}</span>
|
||||
<select
|
||||
className={styles.scheduleSelect}
|
||||
value={spec.monthDay}
|
||||
|
|
@ -389,7 +390,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
)}
|
||||
|
||||
<label className={styles.scheduleFieldRow}>
|
||||
<span className={styles.scheduleFieldLabel}>Uhrzeit</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
|
||||
<input
|
||||
type="time"
|
||||
step={60}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import React from 'react';
|
||||
import type { NodeConfigRendererProps } from '../shared/types';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
const SCHEMA_EXAMPLE = `{
|
||||
"trigger": {
|
||||
|
|
@ -22,17 +23,20 @@ const SCHEMA_EXAMPLE = `{
|
|||
}`;
|
||||
|
||||
export const StartNodeConfig: React.FC<NodeConfigRendererProps> = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.startNodeDoc}>
|
||||
<p className={styles.startNodeDocIntro}>
|
||||
Die <strong>Start</strong>-Node liefert beim Ausführen immer dieselbe Struktur. Den <strong>Einstiegstyp</strong>{' '}
|
||||
(manuell, Formular, Zeitplan, …) wählen Sie über das <strong>Zahnrad</strong> oben in der
|
||||
Workflow-Konfiguration.
|
||||
{t(
|
||||
'Die Start-Node liefert beim Ausführen immer dieselbe Struktur. Den Einstiegstyp (manuell, Formular, Zeitplan, …) wählen Sie über das Zahnrad oben in der Workflow-Konfiguration.'
|
||||
)}
|
||||
</p>
|
||||
<p className={styles.startNodeDocSub}>
|
||||
{t('Nachgelagerte Nodes können z. B. auf')}{' '}
|
||||
<code>payload</code> {t('und')} <code>trigger.type</code> {t('zugreifen.')}
|
||||
</p>
|
||||
<p className={styles.startNodeDocSub}>Nachgelagerte Nodes können z. B. auf <code>payload</code> und{' '}
|
||||
<code>trigger.type</code> zugreifen.</p>
|
||||
<div className={styles.startNodeSchema}>
|
||||
<div className={styles.startNodeSchemaTitle}>Ausgabe-Schema</div>
|
||||
<div className={styles.startNodeSchemaTitle}>{t('Ausgabe-Schema')}</div>
|
||||
<pre className={styles.startNodePre}>{SCHEMA_EXAMPLE}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
<option value="">{t('MIME-Typ wählen')}</option>
|
||||
{mimeTypeOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label} ({o.value})
|
||||
{t(o.label)} ({o.value})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -166,7 +166,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
className={styles.startsInput}
|
||||
value={valStr}
|
||||
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
||||
placeholder={isMimeTypeRef ? 'z.B. application/pdf' : `Wert`}
|
||||
placeholder={isMimeTypeRef ? t('z.B. application/pdf') : t('Wert')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -185,7 +185,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
return (
|
||||
<div className={styles.ifElseConditionEditor}>
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Datenquelle</label>
|
||||
<label>{t('Datenquelle')}</label>
|
||||
<RefSourceSelect
|
||||
value={ref}
|
||||
onChange={handleRefChange}
|
||||
|
|
@ -221,7 +221,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
>
|
||||
{operators.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
{t(o.label)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -241,7 +241,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
);
|
||||
})}
|
||||
<button type="button" className={styles.startsAddBtn} onClick={addCase}>
|
||||
+ Fall
|
||||
{t('+ Fall')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -162,13 +162,6 @@ const _SCOPE_ICONS: Record<string, string> = {
|
|||
|
||||
const _SCOPE_CYCLE: string[] = ['personal', 'featureInstance', 'mandate'];
|
||||
|
||||
const _SCOPE_LABELS: Record<string, string> = {
|
||||
personal: 'Persönlich',
|
||||
featureInstance: 'Instanz',
|
||||
mandate: 'Mandant',
|
||||
global: 'Global',
|
||||
};
|
||||
|
||||
interface SelectionCtx {
|
||||
selectedItemIds: Set<string>;
|
||||
selectedFileIds: string[];
|
||||
|
|
@ -187,6 +180,12 @@ interface SelectionCtx {
|
|||
|
||||
function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
||||
const { t } = useLanguage();
|
||||
const scopeLabels = useMemo((): Record<string, string> => ({
|
||||
personal: t('Persönlich'),
|
||||
featureInstance: t('Instanz'),
|
||||
mandate: t('Mandant'),
|
||||
global: t('Global'),
|
||||
}), [t]);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
|
|
@ -257,14 +256,14 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
|||
<span className={styles.rightZone}>
|
||||
<span className={styles.actions}>
|
||||
{sel.onRenameFile && !multiSelected && (
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(file.fileName); setRenaming(true); }} title="Umbenennen">
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(file.fileName); setRenaming(true); }} title={t('Umbenennen')}>
|
||||
<FaPen />
|
||||
</button>
|
||||
)}
|
||||
{multiSelected && isSelected ? (
|
||||
<>
|
||||
{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={t('{count} Ordner löschen', { count: String(sel.selectedFolderIds.length) })}>
|
||||
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
|
||||
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
|
||||
</button>
|
||||
|
|
@ -300,7 +299,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
|||
const next = _SCOPE_CYCLE[(idx + 1) % _SCOPE_CYCLE.length];
|
||||
sel.onScopeChange(file.id, next);
|
||||
}}
|
||||
title={`Scope: ${_SCOPE_LABELS[file.scope!] || file.scope} (klicken zum Wechseln)`}
|
||||
title={`${t('Scope')}: ${scopeLabels[file.scope!] || file.scope} (${t('klicken zum Wechseln')})`}
|
||||
style={{ fontSize: 14 }}
|
||||
>
|
||||
{_SCOPE_ICONS[file.scope!] || '\uD83D\uDC64'}
|
||||
|
|
@ -381,12 +380,12 @@ function _TreeNode({
|
|||
const _handleAdd = useCallback(async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!onCreateFolder) return;
|
||||
const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
|
||||
const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
|
||||
if (name?.trim()) {
|
||||
await onCreateFolder(name.trim(), node.id);
|
||||
if (!expandedIds.has(node.id)) onToggle(node.id);
|
||||
}
|
||||
}, [onCreateFolder, node.id, expandedIds, onToggle, promptFolderName]);
|
||||
}, [onCreateFolder, node.id, expandedIds, onToggle, promptFolderName, t]);
|
||||
|
||||
const _handleDeleteSingle = useCallback(async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -753,7 +752,7 @@ export default function FolderTree({
|
|||
onDrop={_handleRootDrop}
|
||||
>
|
||||
<span className={styles.folderIcon}><FaGlobe /></span>
|
||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>(Global)</span>
|
||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>({t('Global')})</span>
|
||||
<span className={styles.rootActions}>
|
||||
{onRefresh && (
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
|
||||
|
|
@ -765,7 +764,7 @@ export default function FolderTree({
|
|||
className={styles.actionBtn}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
|
||||
const name = await promptFolderName(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
|
||||
if (name?.trim()) await onCreateFolder(name.trim(), null);
|
||||
}}
|
||||
title={t('Neuer Ordner')}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa';
|
||||
import styles from './FolderTree.module.css';
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export interface BrowseEntry {
|
||||
name: string;
|
||||
|
|
@ -100,6 +101,7 @@ function _FolderRow({
|
|||
onSelectFolder?: (path: string) => void;
|
||||
foldersOnly: boolean;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const isExpanded = expandedPaths.has(entry.path);
|
||||
const isSelected = selectedPath === entry.path;
|
||||
const children = loadedChildren[entry.path] ?? [];
|
||||
|
|
@ -132,7 +134,7 @@ function _FolderRow({
|
|||
<span
|
||||
className={`${styles.chevron} ${isExpanded ? styles.expanded : ''}`}
|
||||
onClick={handleChevronClick}
|
||||
title={isExpanded ? 'Einklappen' : 'Erweitern'}
|
||||
title={isExpanded ? t('Einklappen') : t('Erweitern')}
|
||||
>
|
||||
<FaChevronRight />
|
||||
</span>
|
||||
|
|
@ -148,7 +150,7 @@ function _FolderRow({
|
|||
<div className={styles.children}>
|
||||
{isLoading ? (
|
||||
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
|
||||
Wird geladen…
|
||||
{t('Wird geladen…')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -177,7 +179,7 @@ function _FolderRow({
|
|||
))}
|
||||
{children.length === 0 && (
|
||||
<div style={{ padding: '0.4rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
|
||||
Leer
|
||||
{t('Leer')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -199,6 +201,7 @@ export function SharepointBrowseTree({
|
|||
selectedPath,
|
||||
initialChildren = [],
|
||||
}: SharepointBrowseTreeProps) {
|
||||
const { t } = useLanguage();
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set([rootPath]));
|
||||
const [loadedChildren, setLoadedChildren] = useState<Record<string, BrowseEntry[]>>(() =>
|
||||
initialChildren.length > 0 ? { [rootPath]: initialChildren } : {}
|
||||
|
|
@ -261,11 +264,12 @@ export function SharepointBrowseTree({
|
|||
<span
|
||||
className={`${styles.chevron} ${isRootExpanded ? styles.expanded : ''}`}
|
||||
onClick={() => handleToggle(rootPath)}
|
||||
title={isRootExpanded ? t('Einklappen') : t('Erweitern')}
|
||||
>
|
||||
<FaChevronRight />
|
||||
</span>
|
||||
<span className={styles.folderIcon}><FaGlobe /></span>
|
||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>SharePoint</span>
|
||||
<span className={`${styles.folderName} ${styles.rootLabel}`}>{t('SharePoint')}</span>
|
||||
{rootLoading && (
|
||||
<span style={{ fontSize: 10, color: 'var(--color-text-secondary,#999)', marginLeft: 4 }}>…</span>
|
||||
)}
|
||||
|
|
@ -274,7 +278,7 @@ export function SharepointBrowseTree({
|
|||
<div className={styles.children}>
|
||||
{rootLoading ? (
|
||||
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
|
||||
Sites werden geladen…
|
||||
{t('Sites werden geladen…')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -303,7 +307,7 @@ export function SharepointBrowseTree({
|
|||
))}
|
||||
{rootItems.length === 0 && !rootLoading && (
|
||||
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
|
||||
Keine Einträge
|
||||
{t('Keine Einträge')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -131,11 +131,11 @@ export function DeleteActionButton<T = any>({
|
|||
} else {
|
||||
// Refetch to restore the item in case of failure
|
||||
await refetch();
|
||||
onError?.(row, 'Delete failed');
|
||||
onError?.(row, t('Löschen fehlgeschlagen'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Delete failed:', error);
|
||||
onError?.(row, error.message || 'Delete failed');
|
||||
onError?.(row, error.message || t('Löschen fehlgeschlagen'));
|
||||
// Refetch to restore the item in case of failure
|
||||
await refetch();
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export function ViewActionButton<T = any>({
|
|||
isOpen={isPopupOpen}
|
||||
onClose={() => setIsPopupOpen(false)}
|
||||
fileId={(row as any)[idField]}
|
||||
fileName={(row as any)[nameField] || 'Unknown Item'}
|
||||
fileName={(row as any)[nameField] || t('Unbekanntes Element')}
|
||||
mimeType={mimeType}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -115,7 +115,7 @@ export function ViewActionButton<T = any>({
|
|||
>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h3 style={{ marginBottom: '20px', fontSize: '1.2rem', fontWeight: 'bold' }}>
|
||||
{(row as any)[nameField] || (row as any)[idField] || 'Details'}
|
||||
{(row as any)[nameField] || (row as any)[idField] || t('Details')}
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gap: '15px' }}>
|
||||
{Object.entries(row as Record<string, any>)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const isTextMultilingual = (value: any): boolean => {
|
|||
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
||||
return false;
|
||||
}
|
||||
return 'en' in value && typeof value.en === 'string';
|
||||
return 'xx' in value && typeof value.xx === 'string';
|
||||
};
|
||||
|
||||
// Note: Field types are determined ONLY by the explicit 'type' property.
|
||||
|
|
@ -50,7 +50,7 @@ export interface AttributeDefinition {
|
|||
|
||||
export interface AttributeOption {
|
||||
value: string | number;
|
||||
label: string | { [language: string]: string };
|
||||
label: string;
|
||||
}
|
||||
|
||||
// FormGeneratorForm props
|
||||
|
|
@ -243,7 +243,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
if (!isTextMultilingual(processedData[attr.name])) {
|
||||
// If it's a string, convert to TextMultilingual
|
||||
if (typeof processedData[attr.name] === 'string') {
|
||||
processedData[attr.name] = { en: processedData[attr.name] };
|
||||
processedData[attr.name] = { xx: processedData[attr.name] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -268,8 +268,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
if (attr.default !== undefined) {
|
||||
initialData[attr.name] = attr.default;
|
||||
} else if (isMultilingual(attr)) {
|
||||
// Initialize TextMultilingual fields with empty object
|
||||
initialData[attr.name] = { en: '' };
|
||||
initialData[attr.name] = { xx: '' };
|
||||
} else {
|
||||
initialData[attr.name] = getDefaultValueForType(attr.type);
|
||||
}
|
||||
|
|
@ -327,11 +326,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
if (typeof opt === 'string' || typeof opt === 'number') {
|
||||
return { value: opt, label: String(opt) };
|
||||
}
|
||||
// Handle multilingual labels
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return { value: opt.value, label: labelValue };
|
||||
return { value: opt.value, label: opt.label || String(opt.value) };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -436,12 +431,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
return { value: opt, label: opt };
|
||||
}
|
||||
if (typeof opt === 'object' && 'value' in opt) {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
label: opt.label || String(opt.value)
|
||||
};
|
||||
}
|
||||
return { value: String(opt), label: String(opt) };
|
||||
|
|
@ -464,9 +456,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
// Special handling for TextMultilingual fields (by explicit type only)
|
||||
const isMultilingualField = isMultilingualType(attr.type as AttributeType);
|
||||
if (isMultilingualField && isTextMultilingual(value)) {
|
||||
if (!value.en || typeof value.en !== 'string' || value.en.trim() === '') {
|
||||
if (!value.xx || typeof value.xx !== 'string' || value.xx.trim() === '') {
|
||||
newErrors[attr.name] = t('{fieldLabel} ist erforderlich', {
|
||||
fieldLabel: `${attr.label} (Englisch)`,
|
||||
fieldLabel: attr.label,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -655,21 +647,17 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
}
|
||||
};
|
||||
|
||||
// Build multilingual language list dynamically from availableLanguages.
|
||||
// 'en' is always first and required; remaining languages follow in DB order.
|
||||
// xx = source/default text (required), then all available languages dynamically.
|
||||
const multilingualLangs = useMemo(() => {
|
||||
const base: { code: string; uiLabel: string; required: boolean }[] = [
|
||||
{ code: 'en', uiLabel: 'EN', required: true },
|
||||
const langs: { code: string; uiLabel: string; required: boolean }[] = [
|
||||
{ code: 'xx', uiLabel: t('Quelltext'), required: true },
|
||||
];
|
||||
for (const lang of availableLanguages) {
|
||||
if (lang.code === 'en' || lang.code === 'xx') continue;
|
||||
base.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false });
|
||||
if (lang.code === 'xx') continue;
|
||||
langs.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false });
|
||||
}
|
||||
if (base.length === 1) {
|
||||
base.push({ code: 'de', uiLabel: 'DE', required: false });
|
||||
}
|
||||
return base;
|
||||
}, [availableLanguages]);
|
||||
return langs;
|
||||
}, [availableLanguages, t]);
|
||||
|
||||
const _handleAutoTranslate = async (attrName: string, multilingualValue: Record<string, string>) => {
|
||||
const sourceLang = multilingualLangs.find(l => (multilingualValue[l.code] || '').trim())?.code;
|
||||
|
|
@ -702,11 +690,11 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
|
||||
// Render multilingual field
|
||||
const renderMultilingualField = (attr: AttributeDefinition) => {
|
||||
const value = formData[attr.name] || { en: '' };
|
||||
const value = formData[attr.name] || { xx: '' };
|
||||
const hasError = errors[attr.name];
|
||||
const isReadonly = mode === 'display' || attr.readonly || !isFieldEditableInMode(attr, mode);
|
||||
|
||||
const multilingualValue = isTextMultilingual(value) ? value : { en: typeof value === 'string' ? value : '' };
|
||||
const multilingualValue = isTextMultilingual(value) ? value : { xx: typeof value === 'string' ? value : '' };
|
||||
|
||||
const handleMultilingualChange = (langCode: string, langValue: string) => {
|
||||
const newValue = { ...multilingualValue, [langCode]: langValue };
|
||||
|
|
@ -745,7 +733,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
onChange={(e) => handleMultilingualChange(lang.code, e.target.value)}
|
||||
onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)}
|
||||
onBlur={() => handleFieldFocus(`${attr.name}.${lang.code}`, false)}
|
||||
className={`${styles.fieldInput} ${hasError && lang.code === 'en' ? styles.fieldError : ''}`}
|
||||
className={`${styles.fieldInput} ${hasError && lang.code === 'xx' ? styles.fieldError : ''}`}
|
||||
/>
|
||||
<label className={getLabelClass(`${attr.name}.${lang.code}`, multilingualValue[lang.code])}>
|
||||
{lang.uiLabel}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,6 @@ const CHART_COLORS = [
|
|||
'#bab0ac'
|
||||
];
|
||||
|
||||
const MONTH_LABELS: Record<string, string> = {
|
||||
'01': 'Jan', '02': 'Feb', '03': 'Mär', '04': 'Apr',
|
||||
'05': 'Mai', '06': 'Jun', '07': 'Jul', '08': 'Aug',
|
||||
'09': 'Sep', '10': 'Okt', '11': 'Nov', '12': 'Dez'
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
|
@ -54,15 +48,22 @@ function _defaultFormatCurrency(value: number, currencyCode: string): string {
|
|||
return `${currencyCode} ${value.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function _formatDateLabel(dateStr: string): string {
|
||||
function _createFormatDateLabel(t: (key: string) => string): (dateStr: string) => string {
|
||||
const monthLabels: Record<string, string> = {
|
||||
'01': t('Jan'), '02': t('Feb'), '03': t('Mär'), '04': t('Apr'),
|
||||
'05': t('Mai'), '06': t('Jun'), '07': t('Jul'), '08': t('Aug'),
|
||||
'09': t('Sep'), '10': t('Okt'), '11': t('Nov'), '12': t('Dez'),
|
||||
};
|
||||
return (dateStr: string) => {
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parseInt(parts[2], 10)}.`;
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return MONTH_LABELS[parts[1]] || parts[1];
|
||||
return monthLabels[parts[1]] || parts[1];
|
||||
}
|
||||
return dateStr;
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -74,12 +75,13 @@ interface CustomTooltipProps {
|
|||
payload?: any[];
|
||||
label?: string;
|
||||
formatValue?: (value: number) => string;
|
||||
formatDateLabel: (dateStr: string) => string;
|
||||
}
|
||||
|
||||
const _CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload, label, formatValue }) => {
|
||||
const _CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload, label, formatValue, formatDateLabel }) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
|
||||
const displayLabel = label ? _formatDateLabel(String(label)) : '';
|
||||
const displayLabel = label ? formatDateLabel(String(label)) : '';
|
||||
|
||||
return (
|
||||
<div className={styles.customTooltip}>
|
||||
|
|
@ -115,15 +117,18 @@ const _renderKpiGrid = (section: ReportSectionKpi): React.ReactNode => {
|
|||
|
||||
// --- Bar Chart (vertical) ---
|
||||
|
||||
const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const _renderBarChart = (
|
||||
section: ReportSectionBarChart,
|
||||
currencyCode: string,
|
||||
formatDateLabel: (dateStr: string) => string,
|
||||
t: (key: string) => string,
|
||||
): React.ReactNode => {
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||
}
|
||||
|
||||
const chartData = section.data.map(d => ({
|
||||
name: _formatDateLabel(d.key),
|
||||
name: formatDateLabel(d.key),
|
||||
value: d.value,
|
||||
rawKey: d.key
|
||||
}));
|
||||
|
|
@ -146,12 +151,12 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
|
|||
tickFormatter={(v) => formatter(v)}
|
||||
width={70}
|
||||
/>
|
||||
<Tooltip content={<_CustomTooltip formatValue={formatter} />} />
|
||||
<Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill={section.color || CHART_COLORS[0]}
|
||||
radius={[4, 4, 0, 0]}
|
||||
name="Wert"
|
||||
name={t('Wert')}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
|
@ -161,9 +166,7 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
|
|||
|
||||
// --- Horizontal Bar Chart ---
|
||||
|
||||
const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string, t: (key: string) => string): React.ReactNode => {
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||
}
|
||||
|
|
@ -194,9 +197,12 @@ const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode:
|
|||
|
||||
// --- Line Chart ---
|
||||
|
||||
const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const _renderLineChart = (
|
||||
section: ReportSectionLineChart,
|
||||
currencyCode: string,
|
||||
formatDateLabel: (dateStr: string) => string,
|
||||
t: (key: string) => string,
|
||||
): React.ReactNode => {
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||
}
|
||||
|
|
@ -212,7 +218,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
|||
dataKey="date"
|
||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||
axisLine={{ stroke: 'var(--border-color, #333)' }}
|
||||
tickFormatter={_formatDateLabel}
|
||||
tickFormatter={formatDateLabel}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||
|
|
@ -220,7 +226,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
|||
tickFormatter={formatter}
|
||||
width={70}
|
||||
/>
|
||||
<Tooltip content={<_CustomTooltip formatValue={formatter} />} />
|
||||
<Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
|
||||
{section.series.map((s, i) => (
|
||||
<Line
|
||||
key={s.key}
|
||||
|
|
@ -242,9 +248,12 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
|||
|
||||
// --- Area Chart ---
|
||||
|
||||
const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const _renderAreaChart = (
|
||||
section: ReportSectionAreaChart,
|
||||
currencyCode: string,
|
||||
formatDateLabel: (dateStr: string) => string,
|
||||
t: (key: string) => string,
|
||||
): React.ReactNode => {
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||
}
|
||||
|
|
@ -260,7 +269,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
|||
dataKey="date"
|
||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||
axisLine={{ stroke: 'var(--border-color, #333)' }}
|
||||
tickFormatter={_formatDateLabel}
|
||||
tickFormatter={formatDateLabel}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
|
||||
|
|
@ -268,7 +277,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
|||
tickFormatter={formatter}
|
||||
width={70}
|
||||
/>
|
||||
<Tooltip content={<_CustomTooltip formatValue={formatter} />} />
|
||||
<Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
|
||||
{section.series.map((s, i) => (
|
||||
<Area
|
||||
key={s.key}
|
||||
|
|
@ -290,9 +299,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
|||
|
||||
// --- Pie Chart ---
|
||||
|
||||
const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string, t: (key: string) => string): React.ReactNode => {
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>{t('Keine Daten')}</div>;
|
||||
}
|
||||
|
|
@ -418,7 +425,7 @@ const _ReportTableSection: React.FC<ReportTableSectionProps> = ({ section, curre
|
|||
{hasMore && !showAll && (
|
||||
<div className={styles.showMoreRow}>
|
||||
<button className={styles.showMoreButton} onClick={() => setShowAll(true)}>
|
||||
Alle {section.rows.length} Einträge anzeigen
|
||||
{t('Alle {count} Einträge anzeigen', { count: String(section.rows.length) })}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -436,6 +443,9 @@ interface SectionWrapperProps {
|
|||
}
|
||||
|
||||
const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode }) => {
|
||||
const { t } = useLanguage();
|
||||
const formatDateLabel = useMemo(() => _createFormatDateLabel(t), [t]);
|
||||
|
||||
const spanClass = section.type === 'kpiGrid' || section.span === 'full'
|
||||
? styles.sectionFull
|
||||
: section.span === 'half'
|
||||
|
|
@ -452,20 +462,18 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
|
|||
);
|
||||
}
|
||||
|
||||
const _renderContent = (): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const renderContent = (): React.ReactNode => {
|
||||
switch (section.type) {
|
||||
case 'barChart':
|
||||
return _renderBarChart(section, currencyCode);
|
||||
return _renderBarChart(section, currencyCode, formatDateLabel, t);
|
||||
case 'horizontalBar':
|
||||
return _renderHorizontalBar(section, currencyCode);
|
||||
return _renderHorizontalBar(section, currencyCode, t);
|
||||
case 'lineChart':
|
||||
return _renderLineChart(section, currencyCode);
|
||||
return _renderLineChart(section, currencyCode, formatDateLabel, t);
|
||||
case 'areaChart':
|
||||
return _renderAreaChart(section, currencyCode);
|
||||
return _renderAreaChart(section, currencyCode, formatDateLabel, t);
|
||||
case 'pieChart':
|
||||
return _renderPieChart(section, currencyCode);
|
||||
return _renderPieChart(section, currencyCode, t);
|
||||
case 'table':
|
||||
return <_ReportTableSection section={section} currencyCode={currencyCode} />;
|
||||
default:
|
||||
|
|
@ -477,7 +485,7 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
|
|||
<div className={`${spanClass} ${styles.sectionCard}`}>
|
||||
{section.title && <h3 className={styles.sectionTitle}>{section.title}</h3>}
|
||||
{section.description && <p className={styles.sectionDescription}>{section.description}</p>}
|
||||
{_renderContent()}
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -607,7 +615,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
|||
value={filterState.dateRange?.from?.toISOString().split('T')[0] || ''}
|
||||
onChange={(e) => _handleDateRangeChange('from', e.target.value)}
|
||||
/>
|
||||
<span className={styles.toolbarLabel}>Bis</span>
|
||||
<span className={styles.toolbarLabel}>{t('Bis')}</span>
|
||||
<input
|
||||
type="date"
|
||||
className={styles.dateInput}
|
||||
|
|
@ -640,7 +648,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
|||
value={(filterState.filters[filter.key] as string) || ''}
|
||||
onChange={(e) => _handleFilterChange(filter.key, e.target.value)}
|
||||
>
|
||||
<option value="">{filter.placeholder || 'Alle'}</option>
|
||||
<option value="">{filter.placeholder || t('Alle')}</option>
|
||||
{filter.options?.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const UserSection: React.FC = () => {
|
|||
onClick={handleBilling}
|
||||
>
|
||||
<span className={styles.menuIcon}>💰</span>
|
||||
Guthaben
|
||||
{t('Guthaben')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -104,7 +104,7 @@ export const UserSection: React.FC = () => {
|
|||
onClick={handleSettings}
|
||||
>
|
||||
<span className={styles.menuIcon}>⚙️</span>
|
||||
Einstellungen
|
||||
{t('Einstellungen')}
|
||||
</button>
|
||||
|
||||
{onboardingHidden && (
|
||||
|
|
@ -113,7 +113,7 @@ export const UserSection: React.FC = () => {
|
|||
onClick={handleOnboarding}
|
||||
>
|
||||
<span className={styles.menuIcon}>{'\uD83E\uDDED'}</span>
|
||||
Onboarding-Assistent
|
||||
{t('Onboarding-Assistent')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ export const UserSection: React.FC = () => {
|
|||
onClick={handleLegal}
|
||||
>
|
||||
<span className={styles.menuIcon}>📜</span>
|
||||
Rechtliche Hinweise
|
||||
{t('Rechtliche Hinweise')}
|
||||
</button>
|
||||
|
||||
<div className={styles.menuDivider} />
|
||||
|
|
@ -133,7 +133,7 @@ export const UserSection: React.FC = () => {
|
|||
disabled={isLoggingOut}
|
||||
>
|
||||
<span className={styles.menuIcon}>🚪</span>
|
||||
{isLoggingOut ? 'Abmelden...' : 'Abmelden'}
|
||||
{isLoggingOut ? t('Abmelden...') : t('Abmelden')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -176,13 +176,13 @@ export const UserSection: React.FC = () => {
|
|||
|
||||
<div className={styles.legalLinks}>
|
||||
<a href="/poweron-privacy.html" target="_blank" rel="noopener noreferrer">
|
||||
Datenschutzrichtlinie
|
||||
{t('Datenschutzrichtlinie')}
|
||||
</a>
|
||||
<a href="/poweron-terms.html" target="_blank" rel="noopener noreferrer">
|
||||
Nutzungsbedingungen
|
||||
{t('Nutzungsbedingungen')}
|
||||
</a>
|
||||
<a href="/poweron-home.html" target="_blank" rel="noopener noreferrer">
|
||||
Über PowerOn
|
||||
{t('Über PowerOn')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,32 +20,32 @@ const typeIcons: Record<string, React.ReactNode> = {
|
|||
mention: <FaExclamationTriangle />
|
||||
};
|
||||
|
||||
// Format timestamp to relative time (Unix seconds)
|
||||
function formatRelativeTime(timestamp: number): string {
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
return '';
|
||||
}
|
||||
const now = Date.now() / 1000;
|
||||
const diff = now - timestamp;
|
||||
|
||||
if (diff < 60) return 'Gerade eben';
|
||||
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`;
|
||||
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`;
|
||||
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
return date.toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
interface NotificationBellProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const NotificationBell: React.FC<NotificationBellProps> = ({ className }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const formatRelativeTime = useCallback((timestamp: number): string => {
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
return '';
|
||||
}
|
||||
const now = Date.now() / 1000;
|
||||
const diff = now - timestamp;
|
||||
|
||||
if (diff < 60) return t('Gerade eben');
|
||||
if (diff < 3600) return t('vor {minutes} Min.', { minutes: String(Math.floor(diff / 60)) });
|
||||
if (diff < 86400) return t('vor {hours} Std.', { hours: String(Math.floor(diff / 3600)) });
|
||||
if (diff < 604800) return t('vor {days} Tagen', { days: String(Math.floor(diff / 86400)) });
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
return date.toLocaleDateString('de-DE');
|
||||
}, [t]);
|
||||
|
||||
const {
|
||||
notifications,
|
||||
unreadCount,
|
||||
|
|
@ -144,7 +144,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
<button
|
||||
className={styles.bellButton}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label={`Benachrichtigungen ${unreadCount > 0 ? `(${unreadCount} ungelesen)` : ''}`}
|
||||
aria-label={unreadCount > 0 ? t('Benachrichtigungen ({count} ungelesen)', { count: String(unreadCount) }) : t('Benachrichtigungen')}
|
||||
>
|
||||
<FaBell className={styles.bellIcon} />
|
||||
{unreadCount > 0 && (
|
||||
|
|
@ -165,7 +165,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
className={styles.markAllRead}
|
||||
onClick={() => markAllAsRead()}
|
||||
>
|
||||
Alle als gelesen markieren
|
||||
{t('Alle als gelesen markieren')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -201,7 +201,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
{actionSuccess === notification.id && (
|
||||
<div className={styles.successOverlay}>
|
||||
<FaCheckCircle />
|
||||
<span>{notification.actionResult || 'Erfolgreich'}</span>
|
||||
<span>{notification.actionResult || t('Erfolgreich')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import OnboardingWizard from './OnboardingWizard';
|
||||
|
|
@ -19,13 +19,6 @@ interface OnboardingAssistantProps {
|
|||
|
||||
const _STORAGE_KEY = 'onboarding_hidden';
|
||||
|
||||
const _CALLOUTS: Record<string, string> = {
|
||||
mandate: 'Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.',
|
||||
feature: 'Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.',
|
||||
connection: 'Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.',
|
||||
chat: 'Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.',
|
||||
};
|
||||
|
||||
export function _isOnboardingHidden(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(_STORAGE_KEY) === 'true';
|
||||
|
|
@ -48,6 +41,12 @@ function _hideOnboarding(): void {
|
|||
|
||||
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
|
||||
const { t } = useLanguage();
|
||||
const callouts = useMemo(() => ({
|
||||
mandate: t('Tipp: Ein Mandant ist Ihr Arbeitsbereich. Sie koennen spaeter weitere Mandanten fuer Teams oder Projekte erstellen.'),
|
||||
feature: t('Tipp: Im Store finden Sie AI-Workspace, CommCoach und weitere Features. Aktivieren Sie mindestens eines, um loszulegen.'),
|
||||
connection: t('Tipp: Verbinden Sie Ihre Datenquellen (z.B. SharePoint, Google Drive), damit der AI-Assistent auf Ihre Dokumente zugreifen kann.'),
|
||||
chat: t('Tipp: Starten Sie einen Chat mit dem AI-Assistenten. Er kann Ihre verbundenen Daten analysieren und Fragen beantworten.'),
|
||||
}), [t]);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [hidden, setHidden] = useState(() => _isOnboardingHidden());
|
||||
|
|
@ -99,7 +98,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
id: 'mandate',
|
||||
label: t('Mandat einrichten'),
|
||||
description: hasAdminMandate
|
||||
? 'Dein Mandant ist eingerichtet.'
|
||||
? t('Dein Mandant ist eingerichtet.')
|
||||
: hasFeature
|
||||
? t('Du bist Mitglied eines Mandanten')
|
||||
: t('Erstelle deinen Arbeitsbereich'),
|
||||
|
|
@ -166,7 +165,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [navigate, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const state = location.state as { showOnboarding?: number } | null;
|
||||
|
|
@ -215,7 +214,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
<div>
|
||||
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('Willkommen bei Poweron')}</h3>
|
||||
<p style={{ margin: '4px 0 0', fontSize: '0.85rem', color: 'var(--text-secondary, #6b7280)' }}>
|
||||
{completedCount} von {steps.length} Schritten abgeschlossen
|
||||
{t('{completed} von {total} Schritten abgeschlossen', { completed: String(completedCount), total: String(steps.length) })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -263,14 +262,14 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
<span style={{ fontSize: '0.8rem', color: 'var(--accent, #4f46e5)', fontWeight: 500 }}>{'\u2192'}</span>
|
||||
)}
|
||||
</div>
|
||||
{isNextStep && _CALLOUTS[step.id] && (
|
||||
{isNextStep && callouts[step.id as keyof typeof callouts] && (
|
||||
<div style={{
|
||||
marginTop: 4, marginLeft: 34, padding: '6px 10px',
|
||||
fontSize: '0.78rem', color: 'var(--accent, #4f46e5)',
|
||||
background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6,
|
||||
borderLeft: '3px solid var(--accent, #4f46e5)',
|
||||
}}>
|
||||
{_CALLOUTS[step.id]}
|
||||
{callouts[step.id as keyof typeof callouts]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -290,7 +289,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
onChange={(e) => setDontShowAgain(e.target.checked)}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
Nicht wieder anzeigen
|
||||
{t('Nicht wieder anzeigen')}
|
||||
</label>
|
||||
<button
|
||||
onClick={_handleDismiss}
|
||||
|
|
@ -304,7 +303,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
Schliessen
|
||||
{t('Schliessen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
window.dispatchEvent(new CustomEvent('features-changed'));
|
||||
onComplete();
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.detail || 'Fehler bei der Einrichtung');
|
||||
setError(err?.response?.data?.detail || t('Fehler bei der Einrichtung'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
}}>
|
||||
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>{t('Mandant erstellen')}</h2>
|
||||
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}>
|
||||
Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.
|
||||
{t('Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.')}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
|
||||
|
|
@ -62,7 +62,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
<div>
|
||||
<strong>{t('Kostenlos testen')}</strong>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||
7 Tage gratis, danach flexibel upgraden
|
||||
{t('7 Tage gratis, danach flexibel upgraden')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
|
@ -77,7 +77,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
<div>
|
||||
<strong>{t('Standard monatlich')}</strong>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||
Team-Workspace mit vollem Funktionsumfang
|
||||
{t('Team-Workspace mit vollem Funktionsumfang')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
|
@ -85,7 +85,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}>
|
||||
Name des Mandanten <span style={{ fontWeight: 400, color: 'var(--text-secondary, #666)' }}>(optional)</span>
|
||||
{t('Name des Mandanten')} <span style={{ fontWeight: 400, color: 'var(--text-secondary, #666)' }}>({t('optional')})</span>
|
||||
</label>
|
||||
<input
|
||||
type="text" value={mandateName}
|
||||
|
|
@ -106,7 +106,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)',
|
||||
background: 'transparent', cursor: 'pointer',
|
||||
}}>
|
||||
Abbrechen
|
||||
{t('Abbrechen')}
|
||||
</button>
|
||||
<button onClick={_handleSubmit} disabled={loading}
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -77,8 +77,7 @@ export function _toBackendProviders(
|
|||
return _resolveProviders(selection, allowedProviders);
|
||||
}
|
||||
|
||||
// Provider display names
|
||||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
const _PROVIDER_LABEL_KEYS: Record<string, string> = {
|
||||
anthropic: 'Anthropic (Claude)',
|
||||
openai: 'OpenAI (GPT)',
|
||||
mistral: 'Mistral (Le Chat)',
|
||||
|
|
@ -88,6 +87,11 @@ const PROVIDER_LABELS: Record<string, string> = {
|
|||
internal: 'Internal',
|
||||
};
|
||||
|
||||
function _providerLabel(provider: string, t: (key: string) => string): string {
|
||||
const key = _PROVIDER_LABEL_KEYS[provider];
|
||||
return key ? t(key) : provider;
|
||||
}
|
||||
|
||||
const PROVIDER_ICONS: Record<string, string> = {
|
||||
anthropic: '🤖',
|
||||
openai: '💬',
|
||||
|
|
@ -115,10 +119,11 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
|
|||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
label = 'AI-Provider',
|
||||
label,
|
||||
showLabel = true,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedLabel = label ?? t('AI-Provider');
|
||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -130,13 +135,13 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
|
|||
const providerOptions = useMemo(() => {
|
||||
return allowedProviders.map((provider) => ({
|
||||
value: provider,
|
||||
label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`,
|
||||
label: `${PROVIDER_ICONS[provider] || '🔌'} ${_providerLabel(provider, t)}`,
|
||||
}));
|
||||
}, [allowedProviders]);
|
||||
}, [allowedProviders, t]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.providerSelect} ${className || ''}`}>
|
||||
{showLabel && <label className={styles.label}>{label}</label>}
|
||||
{showLabel && <label className={styles.label}>{resolvedLabel}</label>}
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
|
@ -174,12 +179,13 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
label = 'AI-Provider',
|
||||
label,
|
||||
showLabel = true,
|
||||
defaultExpanded = false,
|
||||
excludeByDefault = [],
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedLabel = label ?? t('AI-Provider');
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -264,13 +270,13 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
}, [effectiveSelection, noneSelected]);
|
||||
|
||||
const summaryHint = useMemo(() => {
|
||||
if (noneSelected) return 'Kein Provider ausgewählt';
|
||||
if (allSelected) return 'Alle Provider aktiv (dynamisch)';
|
||||
if (noneSelected) return t('Kein Provider ausgewählt');
|
||||
if (allSelected) return t('Alle Provider aktiv (dynamisch)');
|
||||
if (selection.include.includes(PROVIDER_ALL)) {
|
||||
return `Alle ausser ${selection.exclude.length} Provider`;
|
||||
return t('Alle ausser {count} Provider', { count: String(selection.exclude.length) });
|
||||
}
|
||||
return `${effectiveSelection.length} von ${allowedProviders.length} Provider`;
|
||||
}, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders]);
|
||||
return t('{n} von {total} Provider', { n: String(effectiveSelection.length), total: String(allowedProviders.length) });
|
||||
}, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -289,7 +295,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
|
||||
{isExpanded && (
|
||||
<div className={styles.dropdownContent}>
|
||||
{showLabel && <div className={styles.dropdownHeader}>{label}</div>}
|
||||
{showLabel && <div className={styles.dropdownHeader}>{resolvedLabel}</div>}
|
||||
|
||||
<div className={styles.selectActions}>
|
||||
<button
|
||||
|
|
@ -298,7 +304,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
disabled={disabled}
|
||||
className={`${styles.actionButton} ${allSelected ? styles.active : ''}`}
|
||||
>
|
||||
Alle
|
||||
{t('Alle')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -319,7 +325,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
/>
|
||||
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
||||
<span className={styles.providerName}>
|
||||
{PROVIDER_LABELS[provider] || provider}
|
||||
{_providerLabel(provider, t)}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
|
|
@ -355,7 +361,7 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
|
|||
<div className={`${styles.providerBadges} ${className || ''}`}>
|
||||
{providers.map((provider) => (
|
||||
<span key={provider} className={styles.badge}>
|
||||
{PROVIDER_ICONS[provider] || '🔌'} {PROVIDER_LABELS[provider] || provider}
|
||||
{PROVIDER_ICONS[provider] || '🔌'} {_providerLabel(provider, t)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -80,20 +80,20 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
|
|||
return (
|
||||
<div className={styles.preview}>
|
||||
<div className={styles.previewHeader}>
|
||||
<h4 className={styles.previewTitle}>Export-Vorschau</h4>
|
||||
<h4 className={styles.previewTitle}>{t('Export-Vorschau')}</h4>
|
||||
<button className={styles.closeButton} onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<div className={styles.previewContent}>
|
||||
<div className={styles.previewSection}>
|
||||
<h5>Scope</h5>
|
||||
<h5>{t('Scope')}</h5>
|
||||
<ul className={styles.previewList}>
|
||||
<li><strong>Typ:</strong> {data.scope.type}</li>
|
||||
<li><strong>{t('Typ:')}</strong> {data.scope.type}</li>
|
||||
{data.scope.mandateName && <li><strong>{t('Mandant')}</strong> {data.scope.mandateName}</li>}
|
||||
{data.scope.featureCode && <li><strong>Feature:</strong> {data.scope.featureCode}</li>}
|
||||
{data.scope.featureCode && <li><strong>{t('Feature:')}</strong> {data.scope.featureCode}</li>}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.previewSection}>
|
||||
<h5>Rollen ({data.roles.length})</h5>
|
||||
<h5>{t('Rollen ({count})', { count: String(data.roles.length) })}</h5>
|
||||
<ul className={styles.previewList}>
|
||||
{data.roles.slice(0, 5).map((role, i) => (
|
||||
<li key={i}>
|
||||
|
|
@ -102,21 +102,21 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
|
|||
</li>
|
||||
))}
|
||||
{data.roles.length > 5 && (
|
||||
<li className={styles.moreItems}>... und {data.roles.length - 5} weitere</li>
|
||||
<li className={styles.moreItems}>{t('... und {count} weitere', { count: String(data.roles.length - 5) })}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.previewSection}>
|
||||
<h5>Regeln ({data.accessRules.length})</h5>
|
||||
<h5>{t('Regeln ({count})', { count: String(data.accessRules.length) })}</h5>
|
||||
<ul className={styles.previewList}>
|
||||
{data.accessRules.slice(0, 5).map((rule, i) => (
|
||||
<li key={i}>
|
||||
<span className={styles.contextBadge}>{rule.context}</span>
|
||||
<code>{rule.item || '(global)'}</code>
|
||||
<code>{rule.item || t('(global)')}</code>
|
||||
</li>
|
||||
))}
|
||||
{data.accessRules.length > 5 && (
|
||||
<li className={styles.moreItems}>... und {data.accessRules.length - 5} weitere</li>
|
||||
<li className={styles.moreItems}>{t('... und {count} weitere', { count: String(data.accessRules.length - 5) })}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -154,7 +154,7 @@ const ImportResult: React.FC<ImportResultProps> = ({ result, onClose }) => {
|
|||
</div>
|
||||
<div className={styles.resultContent}>
|
||||
<ul className={styles.resultStats}>
|
||||
<li><strong>Modus:</strong> {importModes.find(m => m.value === result.mode)?.label}</li>
|
||||
<li><strong>{t('Modus:')}</strong> {importModes.find(m => m.value === result.mode)?.label}</li>
|
||||
<li><strong>{t('Rollen erstellt')}</strong> {result.rolesCreated}</li>
|
||||
<li><strong>{t('Rollen aktualisiert')}</strong> {result.rolesUpdated}</li>
|
||||
<li><strong>{t('Regeln erstellt')}</strong> {result.rulesCreated}</li>
|
||||
|
|
@ -238,7 +238,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
if (result.success && result.data) {
|
||||
setImportData(result.data);
|
||||
} else {
|
||||
setParseError(result.error || 'Fehler beim Parsen');
|
||||
setParseError(result.error || t('Fehler beim Parsen'));
|
||||
setImportData(null);
|
||||
}
|
||||
};
|
||||
|
|
@ -289,13 +289,14 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<FaFileExport className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>Export</h3>
|
||||
<h3 className={styles.sectionTitle}>{t('Export')}</h3>
|
||||
</div>
|
||||
<div className={styles.sectionContent}>
|
||||
<p className={styles.sectionDescription}>
|
||||
Exportiert alle Rollen und Berechtigungen
|
||||
{isGlobal ? ' der globalen Templates' : ` des Mandanten "${mandateName || mandateId}"`}
|
||||
{featureCode ? ` für Feature "${featureCode}"` : ''} als JSON-Datei.
|
||||
{t('Exportiert alle Rollen und Berechtigungen')}{' '}
|
||||
{isGlobal ? t('der globalen Templates') : t('des Mandanten "{name}"', { name: String(mandateName || mandateId || '') })}
|
||||
{featureCode ? <> {t('für Feature "{code}"', { code: featureCode })}</> : null}{' '}
|
||||
{t('als JSON-Datei.')}
|
||||
</p>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
|
|
@ -304,11 +305,11 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
>
|
||||
{exporting ? (
|
||||
<>
|
||||
<FaSpinner className="spinning" /> Exportieren...
|
||||
<FaSpinner className="spinning" /> {t('Exportieren...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload /> RBAC exportieren
|
||||
<FaDownload /> {t('RBAC exportieren')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -319,7 +320,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<FaFileImport className={styles.sectionIcon} />
|
||||
<h3 className={styles.sectionTitle}>Import</h3>
|
||||
<h3 className={styles.sectionTitle}>{t('Import')}</h3>
|
||||
</div>
|
||||
<div className={styles.sectionContent}>
|
||||
{/* File Upload */}
|
||||
|
|
@ -368,14 +369,14 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
<div className={styles.importInfo}>
|
||||
<div className={styles.importStats}>
|
||||
<span><strong>{t('Rollen')}</strong> {importData.roles.length}</span>
|
||||
<span><strong>Regeln:</strong> {importData.accessRules.length}</span>
|
||||
<span><strong>Quelle:</strong> {importData.scope.type}</span>
|
||||
<span><strong>{t('Regeln:')}</strong> {importData.accessRules.length}</span>
|
||||
<span><strong>{t('Quelle:')}</strong> {importData.scope.type}</span>
|
||||
</div>
|
||||
<button
|
||||
className={styles.previewButton}
|
||||
onClick={() => setShowPreview(true)}
|
||||
>
|
||||
<FaEye /> Vorschau
|
||||
<FaEye /> {t('Vorschau')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -383,7 +384,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
{/* Import Mode Selection */}
|
||||
{importData && (
|
||||
<div className={styles.importModeSection}>
|
||||
<h4 className={styles.importModeTitle}>Import-Modus</h4>
|
||||
<h4 className={styles.importModeTitle}>{t('Import-Modus')}</h4>
|
||||
<div className={styles.importModes}>
|
||||
{importModes.map(mode => (
|
||||
<label
|
||||
|
|
@ -416,11 +417,11 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<FaSpinner className="spinning" /> Importieren...
|
||||
<FaSpinner className="spinning" /> {t('Importieren...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaUpload /> RBAC importieren
|
||||
<FaUpload /> {t('RBAC importieren')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -430,7 +431,8 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
{importMode === 'replace' && importData && (
|
||||
<div className={styles.warningMessage}>
|
||||
<FaExclamationTriangle />
|
||||
<strong>Achtung:</strong> Im Modus "Ersetzen" werden alle bestehenden Rollen und Regeln gelöscht!
|
||||
<strong>{t('Achtung:')}</strong>{' '}
|
||||
{t('Im Modus Ersetzen werden alle bestehenden Rollen und Regeln gelöscht!')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
|||
setAutocompleteError(null); // Clear any previous errors on success
|
||||
} catch (err: any) {
|
||||
console.error('❌ [AddressAutocomplete] Error in performSearch:', err);
|
||||
const errorMessage = err?.response?.data?.detail || err?.message || 'Fehler beim Laden der Adressvorschläge';
|
||||
const errorMessage = err?.response?.data?.detail || err?.message || t('Fehler beim Laden der Adressvorschläge');
|
||||
setAutocompleteError(errorMessage);
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(true); // Show dropdown to display error
|
||||
|
|
@ -103,7 +103,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [minQueryLength, maxSuggestions]);
|
||||
}, [minQueryLength, maxSuggestions, t]);
|
||||
|
||||
// Handle input change with debouncing
|
||||
const handleInputChange = useCallback((newValue: string) => {
|
||||
|
|
@ -286,7 +286,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
|||
)}
|
||||
{!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
|
||||
<li className={styles.suggestionItem}>
|
||||
<span className={styles.noResultsText}>{t('no addresses found')}</span>
|
||||
<span className={styles.noResultsText}>{t('Keine Adressen gefunden')}</span>
|
||||
</li>
|
||||
)}
|
||||
{!isLoading && suggestions.map((suggestion, index) => (
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
<div className={styles.bauvorschriftenHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<h4 className={styles.subSectionTitle}>
|
||||
<FaRuler style={{ marginRight: '8px', display: 'inline' }} />
|
||||
Bauvorschriften - {bauvorschriften.zonenbezeichnung}
|
||||
{t('Bauvorschriften')} – {bauvorschriften.zonenbezeichnung}
|
||||
</h4>
|
||||
<button className={styles.expandButton}>
|
||||
{isExpanded ? <FaChevronUp /> : <FaChevronDown />}
|
||||
|
|
@ -48,7 +48,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
)}
|
||||
{bauvorschriften.vollgeschosse !== undefined && bauvorschriften.vollgeschosse !== null && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Vollgeschosse:</span>
|
||||
<span className={styles.label}>{t('Vollgeschosse')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.vollgeschosse}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -60,7 +60,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
)}
|
||||
{bauvorschriften.grenzabstand !== undefined && bauvorschriften.grenzabstand !== null && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Grenzabstand:</span>
|
||||
<span className={styles.label}>{t('Grenzabstand')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.grenzabstand} m</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -93,7 +93,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
className={styles.sourceLinkButton}
|
||||
>
|
||||
<FaFilePdf style={{ marginRight: '8px' }} />
|
||||
Nutzungsplan öffnen
|
||||
{t('Nutzungsplan öffnen')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -101,7 +101,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
{bauvorschriften.extraktionsDatum && (
|
||||
<div className={styles.bauvorschriftenFooter}>
|
||||
<span className={styles.lastUpdated}>
|
||||
Extrahiert: {new Date(bauvorschriften.extraktionsDatum).toLocaleString('de-CH')}
|
||||
{t('Extrahiert')}: {new Date(bauvorschriften.extraktionsDatum).toLocaleString('de-CH')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|||
const CreateButton: React.FC<CreateButtonProps> = ({
|
||||
onCreate,
|
||||
fields,
|
||||
popupTitle = 'Create New Item',
|
||||
popupTitle = 'Neues Element erstellen',
|
||||
popupSize = 'medium',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
|
|
@ -131,7 +131,7 @@ const CreateButton: React.FC<CreateButtonProps> = ({
|
|||
} catch (error: any) {
|
||||
console.error('Creation failed:', error);
|
||||
if (onError) {
|
||||
onError(error.message || 'Creation failed');
|
||||
onError(error.message || t('Erstellung fehlgeschlagen'));
|
||||
}
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { UploadButtonProps } from '../ButtonTypes';
|
||||
import Button from '../Button';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
const UploadButton: React.FC<UploadButtonProps> = ({
|
||||
onUpload,
|
||||
|
|
@ -71,7 +72,7 @@ const UploadButton: React.FC<UploadButtonProps> = ({
|
|||
{isUploading && (
|
||||
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
|
||||
)}
|
||||
{children || (isUploading ? 'Uploading...' : 'Upload File')}
|
||||
{children || (isUploading ? t('Wird hochgeladen…') : t('Datei hochladen'))}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function ConnectedFilesList({
|
|||
previewingFiles = new Set(),
|
||||
removingFiles = new Set(),
|
||||
workflowId: _workflowId,
|
||||
emptyMessage = 'No files connected to this workflow'
|
||||
emptyMessage = 'Keine Dateien mit diesem Workflow verbunden'
|
||||
}: ConnectedFilesListProps) {
|
||||
const { t } = useLanguage();
|
||||
// Combine workflow files and pending files, deduplicating by fileId
|
||||
|
|
@ -253,7 +253,7 @@ export function ConnectedFilesList({
|
|||
)}
|
||||
{isPendingFile && (
|
||||
<span style={{ fontSize: '0.75rem', color: '#4CAF50', fontWeight: 500 }}>
|
||||
• Attached
|
||||
• {t('Angehängt')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { IconType } from 'react-icons';
|
|||
import { IoChevronDown, IoClose } from 'react-icons/io5';
|
||||
import styles from './DropdownSelect.module.css';
|
||||
import { ButtonVariant, ButtonSize } from '../Button/ButtonTypes';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface DropdownSelectItem<T = any> {
|
||||
id: string | number;
|
||||
|
|
@ -37,8 +38,8 @@ function DropdownSelect<T = any>({
|
|||
items = [],
|
||||
selectedItemId,
|
||||
onSelect,
|
||||
placeholder = 'Select an item',
|
||||
emptyMessage = 'No items available',
|
||||
placeholder,
|
||||
emptyMessage,
|
||||
headerText,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
|
|
@ -54,6 +55,9 @@ function DropdownSelect<T = any>({
|
|||
showClearButton = true,
|
||||
clearButtonLabel
|
||||
}: DropdownSelectProps<T>) {
|
||||
const { t } = useLanguage();
|
||||
const resolvedPlaceholder = placeholder ?? t('Element auswählen');
|
||||
const resolvedEmptyMessage = emptyMessage ?? t('Keine Einträge verfügbar');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -116,7 +120,7 @@ function DropdownSelect<T = any>({
|
|||
return (
|
||||
<>
|
||||
<div className={styles.buttonSpinner} />
|
||||
<span>{placeholder}</span>
|
||||
<span>{resolvedPlaceholder}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -134,7 +138,7 @@ function DropdownSelect<T = any>({
|
|||
return (
|
||||
<>
|
||||
{Icon && <Icon className={styles.buttonIcon} />}
|
||||
<span className={styles.buttonText}>{placeholder}</span>
|
||||
<span className={styles.buttonText}>{resolvedPlaceholder}</span>
|
||||
<IoChevronDown className={`${styles.chevronIcon} ${isOpen ? styles.chevronOpen : ''}`} />
|
||||
</>
|
||||
);
|
||||
|
|
@ -153,7 +157,7 @@ function DropdownSelect<T = any>({
|
|||
className={buttonClasses}
|
||||
onClick={handleClear}
|
||||
disabled={disabled || loading}
|
||||
title={clearButtonLabel || `Clear selection: ${selectedItem.label}`}
|
||||
title={clearButtonLabel || t('Auswahl aufheben: {label}', { label: selectedItem.label })}
|
||||
>
|
||||
{Icon && <Icon className={styles.buttonIcon} />}
|
||||
<span className={styles.buttonText}>
|
||||
|
|
@ -197,7 +201,7 @@ function DropdownSelect<T = any>({
|
|||
|
||||
{items.length === 0 ? (
|
||||
<div className={styles.dropdownEmpty}>
|
||||
{emptyMessage}
|
||||
{resolvedEmptyMessage}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.dropdownItems}>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||
import { Button, TextField } from '../index';
|
||||
import { FaLocationArrow } from 'react-icons/fa';
|
||||
import styles from './LocationInput.module.css';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface LocationInputProps {
|
||||
value: string;
|
||||
|
|
@ -20,12 +21,15 @@ const LocationInput: React.FC<LocationInputProps> = ({
|
|||
onChange,
|
||||
onUseCurrentLocation,
|
||||
isGettingLocation = false,
|
||||
placeholder = 'Kanton, Gemeinde, Adresse oder Parzelle',
|
||||
label = 'Standort',
|
||||
placeholder,
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedPlaceholder = placeholder ?? t('Kanton, Gemeinde, Adresse oder Parzelle');
|
||||
const resolvedLabel = label ?? t('Standort');
|
||||
const [isRequestingLocation, setIsRequestingLocation] = useState(false);
|
||||
|
||||
const handleUseCurrentLocation = async () => {
|
||||
|
|
@ -62,7 +66,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
|
|||
loading={isGettingLocation || isRequestingLocation}
|
||||
className={styles.locationButton}
|
||||
>
|
||||
Meine Position verwenden
|
||||
{t('Meine Position verwenden')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { LogProps } from './LogTypes';
|
|||
import { AutoScroll } from '../AutoScroll';
|
||||
import { formatUnixTimestamp } from '../../../utils/time';
|
||||
import styles from './Log.module.css';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
// Helper to get status badge class
|
||||
const getStatusBadgeClass = (status?: string | null): string => {
|
||||
|
|
@ -22,11 +23,13 @@ const getStatusBadgeClass = (status?: string | null): string => {
|
|||
|
||||
const Log: React.FC<LogProps> = ({
|
||||
className = '',
|
||||
emptyMessage = 'No log information available',
|
||||
emptyMessage = 'Keine Log-Informationen verfügbar',
|
||||
dashboardTree,
|
||||
onToggleOperationExpanded,
|
||||
getChildOperations
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage;
|
||||
const formatLogTimestamp = (timestamp: number): string => {
|
||||
try {
|
||||
const formatted = formatUnixTimestamp(timestamp, undefined, {
|
||||
|
|
@ -87,7 +90,7 @@ const Log: React.FC<LogProps> = ({
|
|||
}
|
||||
|
||||
// Use stable operation name (from first log) or fallback to operationId
|
||||
const operationName = operation.operationName || `Operation ${operationId}`;
|
||||
const operationName = operation.operationName || `${t('Operation')} ${operationId}`;
|
||||
// Use latest message as status tag (updates with each poll)
|
||||
const latestMessage = operation.latestMessage || '';
|
||||
const operationStatus = operation.latestStatus || 'running';
|
||||
|
|
@ -137,7 +140,7 @@ const Log: React.FC<LogProps> = ({
|
|||
<button
|
||||
className={styles.expandButton}
|
||||
onClick={() => onToggleOperationExpanded?.(operationId)}
|
||||
aria-label={operation.expanded ? 'Collapse' : 'Expand'}
|
||||
aria-label={operation.expanded ? t('Einklappen') : t('Ausklappen')}
|
||||
>
|
||||
<span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}>
|
||||
▼
|
||||
|
|
@ -243,7 +246,7 @@ const Log: React.FC<LogProps> = ({
|
|||
|
||||
if (dashboardTree.rootOperations.length === 0) {
|
||||
return (
|
||||
<div className={styles.emptyState}>{emptyMessage}</div>
|
||||
<div className={styles.emptyState}>{resolvedEmptyMessage}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +263,7 @@ const Log: React.FC<LogProps> = ({
|
|||
if (!hasDashboardLogs) {
|
||||
return (
|
||||
<div className={`${styles.logContainer} ${className}`}>
|
||||
<div className={styles.emptyState}>{emptyMessage}</div>
|
||||
<div className={styles.emptyState}>{resolvedEmptyMessage}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
import { Message } from '../../Messages/MessagesTypes';
|
||||
import { DocumentItem, MessageMetadata, ActionInfo } from '../../Messages/MessageParts';
|
||||
import { WorkflowFile } from '../../../../hooks/usePlayground';
|
||||
|
|
@ -41,6 +42,7 @@ export const LogMessage: React.FC<LogMessageProps> = ({
|
|||
downloadingFiles,
|
||||
workflowId
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={`${logStyles.logMessage} ${styles.messageWrapper}`}>
|
||||
{/* Metadata row */}
|
||||
|
|
@ -58,7 +60,7 @@ export const LogMessage: React.FC<LogMessageProps> = ({
|
|||
|
||||
{message.summary && message.summary !== message.message && (
|
||||
<div className={logStyles.logSummary}>
|
||||
<strong>Summary:</strong> {message.summary}
|
||||
<strong>{t('Zusammenfassung')}:</strong> {message.summary}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,9 +223,9 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
|||
const polygon = L.polygon(latLngs, SELECTED_STYLE);
|
||||
polygon.bindPopup(`
|
||||
<div>
|
||||
<strong>Parzelle ${parcel.number || parcel.id}</strong><br/>
|
||||
<strong>${t('Parzelle')} ${parcel.number || parcel.id}</strong><br/>
|
||||
${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''}
|
||||
<em>{t('Ausgewählt')}</em>
|
||||
<em>${t('Ausgewählt')}</em>
|
||||
</div>
|
||||
`);
|
||||
if (onParcelClick) {
|
||||
|
|
@ -235,7 +235,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
|||
layersRef.current.push(polygon);
|
||||
});
|
||||
}
|
||||
}, [parcels, combinedOutline, onParcelClick]);
|
||||
}, [parcels, combinedOutline, onParcelClick, t]);
|
||||
|
||||
// Handle map clicks
|
||||
useEffect(() => {
|
||||
|
|
@ -361,7 +361,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
|||
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
|
||||
{parcels.length === 0 && !center && (
|
||||
<div className={styles.emptyStateOverlay}>
|
||||
<p>{emptyMessage}</p>
|
||||
<p>{typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
{showWfsParcels && isWfsLoading && (
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
|
|||
{/* Summary if different from message */}
|
||||
{message.summary && message.summary !== message.message && (
|
||||
<div className={styles.messageSummary}>
|
||||
<strong>Summary:</strong> {message.summary}
|
||||
<strong>{t('Zusammenfassung')}:</strong> {message.summary}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import { MessagesProps } from './MessagesTypes';
|
||||
import { ChatMessage } from './ChatMessages/ChatMessage';
|
||||
import { LogMessage } from '../Log/LogMessage/LogMessage';
|
||||
|
|
@ -15,7 +16,7 @@ const Messages: React.FC<MessagesProps> = ({
|
|||
showProgress = true,
|
||||
renderMessage,
|
||||
renderDocument,
|
||||
emptyMessage = 'No messages yet',
|
||||
emptyMessage = 'Noch keine Nachrichten',
|
||||
onFileDelete,
|
||||
onFileRemove,
|
||||
onFileView,
|
||||
|
|
@ -28,10 +29,12 @@ const Messages: React.FC<MessagesProps> = ({
|
|||
onMessageDelete,
|
||||
deletingMessages
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage;
|
||||
if (!messages || messages.length === 0) {
|
||||
return (
|
||||
<div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}>
|
||||
<div className={styles.emptyState}>{emptyMessage}</div>
|
||||
<div className={styles.emptyState}>{resolvedEmptyMessage}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||
import { FaChevronDown, FaChevronUp, FaFilePdf, FaInfoCircle } from 'react-icons/fa';
|
||||
import { UrlContentPreview } from '../../ContentPreview';
|
||||
import styles from './OerebSection.module.css';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface OerebData {
|
||||
extract_url?: string;
|
||||
|
|
@ -21,6 +22,7 @@ export interface OerebSectionProps {
|
|||
}
|
||||
|
||||
export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
||||
const { t } = useLanguage();
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const restrictions = oereb.restrictions || [];
|
||||
|
|
@ -34,7 +36,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
<div className={styles.oerebHeader} onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<h4 className={styles.subSectionTitle}>
|
||||
<FaInfoCircle style={{ marginRight: '8px', display: 'inline' }} />
|
||||
ÖREB-Kataster
|
||||
{t('ÖREB-Kataster')}
|
||||
{restrictions.length > 0 && (
|
||||
<span className={styles.badge}>({restrictions.length})</span>
|
||||
)}
|
||||
|
|
@ -54,7 +56,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
type="button"
|
||||
>
|
||||
<FaFilePdf style={{ marginRight: '8px' }} />
|
||||
Vollständigen ÖREB-Auszug öffnen (PDF)
|
||||
{t('Vollständigen ÖREB-Auszug öffnen (PDF)')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -68,14 +70,14 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
{restriction.law_status && (
|
||||
<span className={styles.restrictionStatus}>
|
||||
{restriction.law_status === 'inKraft' || restriction.law_status === 'inForce'
|
||||
? 'In Kraft'
|
||||
? t('In Kraft')
|
||||
: restriction.law_status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{restriction.type && (
|
||||
<div className={styles.restrictionType}>
|
||||
<span className={styles.label}>Typ:</span>
|
||||
<span className={styles.label}>{t('Typ:')}</span>
|
||||
<span className={styles.value}>{restriction.type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -86,7 +88,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
)}
|
||||
{restriction.documents && restriction.documents.length > 0 && (
|
||||
<div className={styles.restrictionDocuments}>
|
||||
<span className={styles.label}>Dokumente:</span>
|
||||
<span className={styles.label}>{t('Dokumente:')}</span>
|
||||
{restriction.documents.map((doc, docIndex) => (
|
||||
<a
|
||||
key={docIndex}
|
||||
|
|
@ -95,7 +97,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
rel="noopener noreferrer"
|
||||
className={styles.documentLink}
|
||||
>
|
||||
Dokument {docIndex + 1}
|
||||
{t('Dokument {nr}', { nr: String(docIndex + 1) })}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -105,14 +107,14 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
</div>
|
||||
) : (
|
||||
<div className={styles.noRestrictions}>
|
||||
Keine öffentlich-rechtlichen Beschränkungen gefunden.
|
||||
{t('Keine öffentlich-rechtlichen Beschränkungen gefunden.')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{oereb.last_updated && (
|
||||
<div className={styles.oerebFooter}>
|
||||
<span className={styles.lastUpdated}>
|
||||
Aktualisiert: {new Date(oereb.last_updated).toLocaleString('de-CH')}
|
||||
{t('Aktualisiert')}: {new Date(oereb.last_updated).toLocaleString('de-CH')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -124,7 +126,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
|
|||
isOpen={isPreviewOpen}
|
||||
onClose={() => setIsPreviewOpen(false)}
|
||||
url={oereb.extract_url}
|
||||
fileName="ÖREB-Auszug.pdf"
|
||||
fileName={t('ÖREB-Auszug.pdf')}
|
||||
mimeType="application/pdf"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -147,12 +147,12 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
} catch (e: any) {
|
||||
setDocsError(prev => ({
|
||||
...prev,
|
||||
[parcelId]: e?.response?.data?.detail || e?.message || 'Fehler beim Laden'
|
||||
[parcelId]: e?.response?.data?.detail || e?.message || t('Fehler beim Laden')
|
||||
}));
|
||||
} finally {
|
||||
setDocsLoading(prev => ({ ...prev, [parcelId]: false }));
|
||||
}
|
||||
}, [instanceId]);
|
||||
}, [instanceId, t]);
|
||||
|
||||
const runExtraction = useCallback(async (
|
||||
parcelId: string,
|
||||
|
|
@ -174,12 +174,12 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
} catch (e: any) {
|
||||
setExtractError(prev => ({
|
||||
...prev,
|
||||
[parcelId]: e?.response?.data?.detail || e?.message || 'Fehler bei der Extraktion'
|
||||
[parcelId]: e?.response?.data?.detail || e?.message || t('Fehler bei der Extraktion')
|
||||
}));
|
||||
} finally {
|
||||
setExtractLoading(prev => ({ ...prev, [parcelId]: false }));
|
||||
}
|
||||
}, [instanceId]);
|
||||
}, [instanceId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !instanceId) return;
|
||||
|
|
@ -218,7 +218,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
className={styles.panel}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>Parzellen-Informationen ({parcels.length})</h2>
|
||||
<h2 className={styles.title}>{t('Parzellen-Informationen')} ({parcels.length})</h2>
|
||||
<button className={styles.closeButton} onClick={onClose}>
|
||||
<FaTimes />
|
||||
</button>
|
||||
|
|
@ -248,7 +248,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
return [
|
||||
<section key={`h-${bz.bauzone}`} className={styles.bauzoneSection}>
|
||||
<h4 className={styles.bauzoneTitle}>
|
||||
Bauzone {bz.bauzone}
|
||||
{t('Bauzone')} {bz.bauzone}
|
||||
{bz.area_m2 != null && (
|
||||
<span className={styles.bauzoneArea}> — {bz.area_m2.toFixed(2)} m²</span>
|
||||
)}
|
||||
|
|
@ -260,7 +260,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
<section key={parcelData.parcel.id} className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h3 className={styles.sectionTitle}>
|
||||
Parzelle {idx + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'}
|
||||
{t('Parzelle')} {idx + 1}: {parcelData.parcel.number || parcelData.parcel.id || t('Unbekannt')}
|
||||
</h3>
|
||||
{onRemoveParcel && (
|
||||
<button
|
||||
|
|
@ -342,7 +342,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
)}
|
||||
{instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
|
||||
<div className={styles.bzoSection}>
|
||||
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
|
||||
<h4 className={styles.subSectionTitle}>{t('Bauzonenverordnung')}</h4>
|
||||
{docsLoading[parcelData.parcel.id] && (
|
||||
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
|
||||
)}
|
||||
|
|
@ -364,7 +364,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
})}
|
||||
title={t('Dokument öffnen')}
|
||||
>
|
||||
<FaEye /> Öffnen
|
||||
<FaEye /> {t('Öffnen')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -387,7 +387,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
) : (
|
||||
<FaFileAlt />
|
||||
)}
|
||||
Inhalt extrahieren (LangGraph)
|
||||
{t('Inhalt extrahieren (LangGraph)')}
|
||||
</button>
|
||||
)}
|
||||
{extractError[parcelData.parcel.id] && (
|
||||
|
|
@ -470,7 +470,9 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
<div className={styles.infoItem}>
|
||||
<span className={styles.label}>Zone:</span>
|
||||
<span className={styles.value}>
|
||||
{parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden
|
||||
{parcelData.parcel.zone.length !== 1
|
||||
? t('{n} Zonen gefunden', { n: String(parcelData.parcel.zone.length) })
|
||||
: t('1 Zone gefunden')}
|
||||
{(() => {
|
||||
// Extract zone types from zone array
|
||||
const zoneTypes = parcelData.parcel.zone
|
||||
|
|
@ -509,7 +511,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Link öffnen
|
||||
{t('Link öffnen')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -526,7 +528,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
<section key={parcelData.parcel.id || index} className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h3 className={styles.sectionTitle}>
|
||||
Parzelle {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || 'Unbekannt'}
|
||||
{t('Parzelle')} {index + 1}: {parcelData.parcel.number || parcelData.parcel.id || t('Unbekannt')}
|
||||
</h3>
|
||||
{onRemoveParcel && (
|
||||
<button
|
||||
|
|
@ -608,7 +610,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
)}
|
||||
{instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
|
||||
<div className={styles.bzoSection}>
|
||||
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
|
||||
<h4 className={styles.subSectionTitle}>{t('Bauzonenverordnung')}</h4>
|
||||
{docsLoading[parcelData.parcel.id] && (
|
||||
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
|
||||
)}
|
||||
|
|
@ -630,7 +632,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
})}
|
||||
title={t('Dokument öffnen')}
|
||||
>
|
||||
<FaEye /> Öffnen
|
||||
<FaEye /> {t('Öffnen')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -653,7 +655,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
) : (
|
||||
<FaFileAlt />
|
||||
)}
|
||||
Inhalt extrahieren (LangGraph)
|
||||
{t('Inhalt extrahieren (LangGraph)')}
|
||||
</button>
|
||||
)}
|
||||
{extractError[parcelData.parcel.id] && (
|
||||
|
|
@ -736,7 +738,9 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
<div className={styles.infoItem}>
|
||||
<span className={styles.label}>Zone:</span>
|
||||
<span className={styles.value}>
|
||||
{parcelData.parcel.zone.length} Zone{parcelData.parcel.zone.length !== 1 ? 'n' : ''} gefunden
|
||||
{parcelData.parcel.zone.length !== 1
|
||||
? t('{n} Zonen gefunden', { n: String(parcelData.parcel.zone.length) })
|
||||
: t('1 Zone gefunden')}
|
||||
{(() => {
|
||||
const zoneTypes = parcelData.parcel.zone
|
||||
.map((z: any) => {
|
||||
|
|
@ -773,7 +777,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
|||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
Link öffnen
|
||||
{t('Link öffnen')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,17 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
latestStats
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const statusLabel = (status: WorkflowStatusType | null): string => {
|
||||
if (!status) return '';
|
||||
const map: Record<string, string> = {
|
||||
completed: t('Abgeschlossen'),
|
||||
failed: t('Fehlgeschlagen'),
|
||||
started: t('Gestartet'),
|
||||
stopped: t('Gestoppt'),
|
||||
resumed: t('Fortgesetzt'),
|
||||
};
|
||||
return map[status] ?? status;
|
||||
};
|
||||
// Use workflow status and round from API response, fallback to extracting from logs
|
||||
const workflowStatus = useMemo(() => {
|
||||
if (workflowStatusFromApi) {
|
||||
|
|
@ -73,11 +84,11 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
)}
|
||||
{workflowStatus.status && (
|
||||
<span className={styles.statusBadge} data-status={workflowStatus.status}>
|
||||
{workflowStatus.status.charAt(0).toUpperCase() + workflowStatus.status.slice(1)}
|
||||
{statusLabel(workflowStatus.status)}
|
||||
</span>
|
||||
)}
|
||||
{workflowStatus.round !== null && (
|
||||
<span className={styles.roundBadge}>Round {workflowStatus.round}</span>
|
||||
<span className={styles.roundBadge}>{t('Runde {nr}', { nr: String(workflowStatus.round) })}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -85,7 +96,7 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
{latestStats && latestStats.priceCHF !== undefined && (
|
||||
<div className={styles.statsContainer}>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>Cost:</span>
|
||||
<span className={styles.statLabel}>{t('Kosten')}</span>
|
||||
<span className={styles.statValue}>{_formatCurrency(latestStats.priceCHF)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
}
|
||||
groupMap.get(fiId)!.chats.push({
|
||||
id: wf.id,
|
||||
label: wf.label || wf.name || `Chat ${wf.id.slice(0, 8)}`,
|
||||
label: wf.label || wf.name || `${t('Chat')} ${wf.id.slice(0, 8)}`,
|
||||
updatedAt: wf.updatedAt || wf.lastActivity || wf.startedAt,
|
||||
lastMessageAt: wf.lastMessageAt,
|
||||
featureInstanceId: fiId,
|
||||
|
|
@ -132,7 +132,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [context.instanceId]);
|
||||
}, [context.instanceId, t]);
|
||||
|
||||
useEffect(() => { _loadChats(); }, [_loadChats]);
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
<button
|
||||
className={styles.actionBtn}
|
||||
onClick={(e) => { e.stopPropagation(); _startEditing(chat); }}
|
||||
title="Umbenennen"
|
||||
title={t('Umbenennen')}
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
|
|
@ -292,7 +292,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
<button
|
||||
className={styles.actionBtn}
|
||||
onClick={(e) => { e.stopPropagation(); _restoreChat(chat.id); }}
|
||||
title="Wiederherstellen"
|
||||
title={t('Wiederherstellen')}
|
||||
>
|
||||
↩️
|
||||
</button>
|
||||
|
|
@ -300,7 +300,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
<button
|
||||
className={styles.actionBtn}
|
||||
onClick={(e) => { e.stopPropagation(); _archiveChat(chat.id); }}
|
||||
title="Archivieren"
|
||||
title={t('Archivieren')}
|
||||
>
|
||||
📦
|
||||
</button>
|
||||
|
|
@ -323,10 +323,10 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
|
||||
const _featureCodeLabel = (code: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
workspace: 'AI Workspace',
|
||||
commcoach: 'CommCoach',
|
||||
trustee: 'Trustee',
|
||||
automation: 'Automation',
|
||||
workspace: t('KI-Arbeitsbereich'),
|
||||
commcoach: t('CommCoach'),
|
||||
trustee: t('Trustee'),
|
||||
automation: t('Automation'),
|
||||
};
|
||||
return labels[code] || code;
|
||||
};
|
||||
|
|
@ -351,7 +351,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
<button
|
||||
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
|
||||
onClick={() => setFlatMode(!flatMode)}
|
||||
title={flatMode ? 'Baumansicht' : 'Listenansicht'}
|
||||
title={flatMode ? t('Baumansicht') : t('Listenansicht')}
|
||||
>
|
||||
{flatMode ? '\uD83C\uDF33' : '\uD83D\uDCCB'}
|
||||
</button>
|
||||
|
|
@ -362,13 +362,13 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
|||
className={`${styles.filterTab} ${filter === 'active' ? styles.filterTabActive : ''}`}
|
||||
onClick={() => setFilter('active')}
|
||||
>
|
||||
Aktiv ({_activeCount})
|
||||
{t('Aktiv')} ({_activeCount})
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.filterTab} ${filter === 'archived' ? styles.filterTabActive : ''}`}
|
||||
onClick={() => setFilter('archived')}
|
||||
>
|
||||
Archiv ({_archivedCount})
|
||||
{t('Archiv')} ({_archivedCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -250,12 +250,12 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
|||
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 13, fontWeight: 600, color: '#F25843',
|
||||
}}>
|
||||
Dateien hier ablegen
|
||||
{t('Dateien hier ablegen')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px' }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>Files</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>{t('Dateien')}</span>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
|
|
@ -331,10 +331,10 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
|||
</div>
|
||||
|
||||
<div className={styles.legend}>
|
||||
<span>{'\uD83D\uDC64'} Persönlich</span>
|
||||
<span>{'\uD83D\uDC65'} Instanz</span>
|
||||
<span>{'\uD83C\uDFE2'} Mandant</span>
|
||||
<span>{'\uD83D\uDD12'} Neutralisiert</span>
|
||||
<span>{'\uD83D\uDC64'} {t('Persönlich')}</span>
|
||||
<span>{'\uD83D\uDC65'} {t('Instanz')}</span>
|
||||
<span>{'\uD83C\uDFE2'} {t('Mandant')}</span>
|
||||
<span>{'\uD83D\uDD12'} {t('Neutralisiert')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ interface MandateGroupNode {
|
|||
interface FeatureTableNode {
|
||||
objectKey: string;
|
||||
tableName: string;
|
||||
label: Record<string, string>;
|
||||
label: string;
|
||||
fields: string[];
|
||||
isParent?: boolean;
|
||||
parentTable?: string;
|
||||
|
|
@ -185,13 +185,6 @@ const _SCOPE_ICONS: Record<string, string> = {
|
|||
global: '\uD83C\uDF10',
|
||||
};
|
||||
|
||||
const _SCOPE_LABELS: Record<string, string> = {
|
||||
personal: 'Personal',
|
||||
featureInstance: 'Feature Instance',
|
||||
mandate: 'Mandate',
|
||||
global: 'Global',
|
||||
};
|
||||
|
||||
function _nextScope(current: string): string {
|
||||
const idx = _SCOPE_ORDER.indexOf(current);
|
||||
if (idx === -1) return _SCOPE_ORDER[0];
|
||||
|
|
@ -348,6 +341,14 @@ function _Spinner(): React.ReactElement {
|
|||
|
||||
const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
|
||||
const { t } = useLanguage();
|
||||
const _scopeLabel = (scope: string) => ({
|
||||
personal: t('Persönlich'),
|
||||
featureInstance: t('Feature-Instanz'),
|
||||
mandate: t('Mandant'),
|
||||
global: t('Global'),
|
||||
} as Record<string, string>)[scope] || scope;
|
||||
const _scopeCycleTitle = (scope: string) =>
|
||||
`${t('Bereich')}: ${_scopeLabel(scope)} → ${_scopeLabel(_nextScope(scope))}`;
|
||||
const instanceId = context.instanceId;
|
||||
|
||||
/* ── Active sources (fetched internally) ── */
|
||||
|
|
@ -663,7 +664,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
featureCode: node.featureCode,
|
||||
tableName: table.tableName,
|
||||
objectKey: table.objectKey,
|
||||
label: table.label?.en || table.label?.de || table.tableName,
|
||||
label: table.label || table.tableName,
|
||||
});
|
||||
_fetchFeatureDataSources();
|
||||
onSourcesChanged?.();
|
||||
|
|
@ -764,7 +765,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName);
|
||||
|
||||
if (parentTable) {
|
||||
const parentLabel = `${parentTable.label?.en || parentTable.label?.de || parentTable.tableName}: ${parentRecord.displayLabel}`;
|
||||
const parentLabel = `${parentTable.label || parentTable.tableName}: ${parentRecord.displayLabel}`;
|
||||
await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
|
||||
featureInstanceId: node.featureInstanceId,
|
||||
featureCode: node.featureCode,
|
||||
|
|
@ -776,7 +777,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
}
|
||||
|
||||
for (const child of childTables) {
|
||||
const childLabel = `${child.label?.en || child.label?.de || child.tableName}: ${parentRecord.displayLabel}`;
|
||||
const childLabel = `${child.label || child.tableName}: ${parentRecord.displayLabel}`;
|
||||
await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
|
||||
featureInstanceId: node.featureInstanceId,
|
||||
featureCode: node.featureCode,
|
||||
|
|
@ -813,7 +814,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
{dataSources.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||
Active Personal Sources
|
||||
{t('Aktive persönliche Quellen')}
|
||||
</div>
|
||||
{[...dataSources].sort((a, b) => {
|
||||
const aKey = `${a.sourceType}|${a.label || a.path || ''}`;
|
||||
|
|
@ -842,7 +843,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
fontSize: 13, padding: '0 2px', lineHeight: 1,
|
||||
}}
|
||||
title={`Scope: ${_SCOPE_LABELS[ds.scope] || ds.scope} → ${_SCOPE_LABELS[_nextScope(ds.scope)]}`}
|
||||
title={_scopeCycleTitle(ds.scope)}
|
||||
>
|
||||
{_SCOPE_ICONS[ds.scope] || _SCOPE_ICONS.personal}
|
||||
</button>
|
||||
|
|
@ -874,7 +875,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
{/* ── Browse Sources header ── */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
||||
Browse Sources
|
||||
{t('Quellen durchsuchen')}
|
||||
</span>
|
||||
<button
|
||||
onClick={_loadConnections}
|
||||
|
|
@ -888,13 +889,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
{/* ── Browse Sources tree ── */}
|
||||
{loadingRoot && tree.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
Loading connections...
|
||||
{t('Verbindungen werden geladen…')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingRoot && tree.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
No active connections found.
|
||||
{t('Keine aktiven Verbindungen.')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -917,7 +918,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
{featureDataSources.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
|
||||
Active Feature Sources
|
||||
{t('Aktive Feature-Quellen')}
|
||||
</div>
|
||||
{(() => {
|
||||
const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || ''));
|
||||
|
|
@ -979,7 +980,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
<button
|
||||
onClick={() => _cycleFeatureScope(fds)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1 }}
|
||||
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope} → ${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
|
||||
title={_scopeCycleTitle(fds.scope)}
|
||||
>
|
||||
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
||||
</button>
|
||||
|
|
@ -1022,7 +1023,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
<button
|
||||
onClick={() => _cycleFeatureScope(fds)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1 }}
|
||||
title={`Scope: ${_SCOPE_LABELS[fds.scope] || fds.scope} → ${_SCOPE_LABELS[_nextScope(fds.scope)]}`}
|
||||
title={_scopeCycleTitle(fds.scope)}
|
||||
>
|
||||
{_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
|
||||
</button>
|
||||
|
|
@ -1053,7 +1054,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
{/* ── Feature Data header ── */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
|
||||
Feature Data
|
||||
{t('Feature-Daten')}
|
||||
</span>
|
||||
<button
|
||||
onClick={_loadFeatureConnections}
|
||||
|
|
@ -1067,13 +1068,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
{/* ── Feature Data tree ── */}
|
||||
{loadingFeatures && featureTree.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
Loading feature instances...
|
||||
{t('Feature-Instanzen werden geladen…')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingFeatures && featureTree.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
No feature instances found.
|
||||
{t('Keine Feature-Instanzen gefunden.')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1169,7 +1170,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
|
|||
}}
|
||||
title={t('Als Datenquelle hinzufügen')}
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
{isAdding ? '...' : `+ ${t('Hinzufügen')}`}
|
||||
</button>
|
||||
)}
|
||||
{canAdd && alreadyAdded && (
|
||||
|
|
@ -1197,7 +1198,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
|
|||
|
||||
{node.expanded && node.children && node.children.length === 0 && !node.loading && (
|
||||
<div style={{ paddingLeft: (depth + 1) * 16 + 20, fontSize: 11, color: '#bbb', padding: '2px 0 2px ' + ((depth + 1) * 16 + 20) + 'px' }}>
|
||||
(empty)
|
||||
{t('(leer)')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1299,6 +1300,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
|||
onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
|
||||
expandedParentGroups, loadingParentGroup, addingParentKey,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const chevron = node.expanded ? '\u25BE' : '\u25B8';
|
||||
|
||||
|
|
@ -1329,7 +1331,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
|||
{node.label}
|
||||
</span>
|
||||
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
||||
{node.tableCount} tables
|
||||
{node.tableCount} {t('Tabellen')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -1342,7 +1344,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
|||
const isGroupLoading = loadingParentGroup === groupKey;
|
||||
const records = node.parentRecords[pt.tableName];
|
||||
const childTables = (node.tables || []).filter(t => t.parentTable === pt.tableName);
|
||||
const ptLabel = pt.label?.en || pt.label?.de || pt.tableName;
|
||||
const ptLabel = pt.label || pt.tableName;
|
||||
|
||||
return (
|
||||
<_ParentGroupView
|
||||
|
|
@ -1380,7 +1382,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
|
|||
|
||||
{node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
|
||||
<div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}>
|
||||
(no tables)
|
||||
{t('(keine Tabellen)')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1402,7 +1404,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
|
|||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const tableLabel = table.label?.en || table.label?.de || table.tableName;
|
||||
const tableLabel = table.label || table.tableName;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -1433,7 +1435,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
|
|||
}}
|
||||
title={t('Als Feature-Datenquelle hinzufügen')}
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
{isAdding ? '...' : `+ ${t('Hinzufügen')}`}
|
||||
</button>
|
||||
)}
|
||||
{isAdded && (
|
||||
|
|
@ -1467,6 +1469,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
|
|||
featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables,
|
||||
onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const chevron = expanded ? '\u25BE' : '\u25B8';
|
||||
|
||||
|
|
@ -1493,7 +1496,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
|
|||
</span>
|
||||
{childTables.length > 0 && (
|
||||
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
|
||||
+{childTables.length} tables
|
||||
+{childTables.length} {t('Tabellen')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1518,7 +1521,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
|
|||
|
||||
{expanded && records && records.length === 0 && !loading && (
|
||||
<div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}>
|
||||
(no records)
|
||||
{t('(keine Einträge)')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1580,7 +1583,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
|
|||
}}
|
||||
title={t('Alle Tabellen für diese Quelle hinzufügen')}
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
{isAdding ? '...' : `+ ${t('Hinzufügen')}`}
|
||||
</button>
|
||||
)}
|
||||
{isAdded && (
|
||||
|
|
@ -1593,7 +1596,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
|
|||
{record.expanded && (
|
||||
<div style={{ paddingLeft: 64 }}>
|
||||
{childTables.map(ct => {
|
||||
const ctLabel = ct.label?.en || ct.label?.de || ct.tableName;
|
||||
const ctLabel = ct.label || ct.tableName;
|
||||
return (
|
||||
<div key={ct.objectKey} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 4,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ interface UnifiedDataBarProps {
|
|||
}
|
||||
|
||||
const _TAB_KEYS: Record<UdbTab, string> = {
|
||||
chats: 'Chats',
|
||||
chats: 'Chatverläufe',
|
||||
files: 'Dateien',
|
||||
sources: 'Quellen',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,8 +83,6 @@ 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);
|
||||
}
|
||||
|
|
@ -460,7 +458,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
|||
setSidebarItems(items);
|
||||
} catch (err) {
|
||||
console.error('❌ SidebarProvider: Error refreshing sidebar:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load sidebar items');
|
||||
setError(err instanceof Error ? err.message : t('Seitenleiste konnte nicht geladen werden'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,16 +34,11 @@ export interface GenericPageData {
|
|||
type TranslationFunction = (key: string, fallback?: string) => string;
|
||||
|
||||
/**
|
||||
* Resolve display text from a page name that may be a i18n key or { de?, en? }.
|
||||
* Resolve display text from a page name (i18n key) via the translation function.
|
||||
*/
|
||||
export function resolveLanguageText(
|
||||
name: string | { de?: string; en?: string },
|
||||
name: string,
|
||||
t: TranslationFunction
|
||||
): string {
|
||||
if (typeof name === 'string') {
|
||||
const resolved = t(name);
|
||||
return resolved !== name ? resolved : name;
|
||||
}
|
||||
const lang = (typeof navigator !== 'undefined' && navigator.language?.startsWith('en')) ? 'en' : 'de';
|
||||
return name[lang] ?? name.de ?? name.en ?? '';
|
||||
return t(name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useState, useCallback } from 'react';
|
||||
import api from '../api';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
|
|
@ -78,6 +79,7 @@ interface SaveResult {
|
|||
// =============================================================================
|
||||
|
||||
export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) {
|
||||
const { t } = useLanguage();
|
||||
const [rules, setRules] = useState<AccessRule[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -113,14 +115,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac'
|
|||
setRules(fetchedRules);
|
||||
return fetchedRules;
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Regeln';
|
||||
const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Laden der Regeln');
|
||||
setError(errorMsg);
|
||||
console.error('Error fetching rules:', err);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [roleId, apiBasePath, isInstanceApi, getHeaders]);
|
||||
}, [roleId, apiBasePath, isInstanceApi, getHeaders, t]);
|
||||
|
||||
/**
|
||||
* Save all rules for the role
|
||||
|
|
@ -196,14 +198,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac'
|
|||
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Speichern';
|
||||
const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Speichern');
|
||||
setError(errorMsg);
|
||||
console.error('Error saving rules:', err);
|
||||
return { success: false, error: errorMsg };
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders]);
|
||||
}, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders, t]);
|
||||
|
||||
/**
|
||||
* Get rules grouped by context
|
||||
|
|
|
|||
|
|
@ -188,15 +188,10 @@ export function useMandates() {
|
|||
// Handle options - can be array or string reference
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attrOptions;
|
||||
|
|
@ -206,15 +201,10 @@ export function useMandates() {
|
|||
// Handle options - can be array or string reference
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attrOptions;
|
||||
|
|
@ -327,15 +317,10 @@ export function useMandates() {
|
|||
fieldType = 'enum';
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
optionsReference = attrOptions;
|
||||
}
|
||||
|
|
@ -343,15 +328,10 @@ export function useMandates() {
|
|||
fieldType = 'multiselect';
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
optionsReference = attrOptions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,15 +229,10 @@ export function useRbacRoles() {
|
|||
// Handle options - can be array or string reference
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attrOptions;
|
||||
|
|
@ -247,15 +242,10 @@ export function useRbacRoles() {
|
|||
// Handle options - can be array or string reference
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attrOptions;
|
||||
|
|
@ -368,15 +358,10 @@ export function useRbacRoles() {
|
|||
fieldType = 'enum';
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
optionsReference = attrOptions;
|
||||
}
|
||||
|
|
@ -384,15 +369,10 @@ export function useRbacRoles() {
|
|||
fieldType = 'multiselect';
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
optionsReference = attrOptions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,15 +205,10 @@ export function useRbacRules() {
|
|||
// Handle options - can be array or string reference
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attrOptions;
|
||||
|
|
@ -223,15 +218,10 @@ export function useRbacRules() {
|
|||
// Handle options - can be array or string reference
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attrOptions;
|
||||
|
|
@ -344,15 +334,10 @@ export function useRbacRules() {
|
|||
fieldType = 'enum';
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
optionsReference = attrOptions;
|
||||
}
|
||||
|
|
@ -360,15 +345,10 @@ export function useRbacRules() {
|
|||
fieldType = 'multiselect';
|
||||
const attrOptions = (attr as any).options;
|
||||
if (Array.isArray(attrOptions)) {
|
||||
options = attrOptions.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attrOptions.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attrOptions === 'string') {
|
||||
optionsReference = attrOptions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export interface PaginationMetadata {
|
|||
|
||||
export interface Feature {
|
||||
code: string;
|
||||
label: string | { [key: string]: string };
|
||||
label: string;
|
||||
icon?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ export interface FeatureAccessUser {
|
|||
export interface FeatureInstanceRole {
|
||||
id: string;
|
||||
roleLabel: string;
|
||||
description?: { [key: string]: string };
|
||||
description?: string;
|
||||
featureCode?: string;
|
||||
isSystemRole?: boolean;
|
||||
}
|
||||
|
|
@ -312,7 +312,7 @@ export function useFeatureAccess() {
|
|||
name: string;
|
||||
features: Array<{
|
||||
code: string;
|
||||
label: string | { [key: string]: string };
|
||||
label: string;
|
||||
instances: Array<{
|
||||
id: string;
|
||||
featureCode: string;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import api from '../api';
|
|||
export interface Role {
|
||||
id: string;
|
||||
roleLabel: string;
|
||||
description?: string | { [key: string]: string };
|
||||
description?: string;
|
||||
mandateId?: string;
|
||||
featureInstanceId?: string;
|
||||
featureCode?: string;
|
||||
|
|
@ -24,7 +24,7 @@ export interface Role {
|
|||
|
||||
export interface RoleCreate {
|
||||
roleLabel: string;
|
||||
description?: string | { [key: string]: string };
|
||||
description?: string;
|
||||
mandateId?: string;
|
||||
featureInstanceId?: string;
|
||||
featureCode?: string;
|
||||
|
|
@ -32,7 +32,7 @@ export interface RoleCreate {
|
|||
|
||||
export interface RoleUpdate {
|
||||
roleLabel?: string;
|
||||
description?: string | { [key: string]: string };
|
||||
description?: string;
|
||||
mandateId?: string | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export interface AttributeDefinition {
|
|||
description?: string;
|
||||
required?: boolean;
|
||||
default?: any;
|
||||
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string;
|
||||
options?: Array<{ value: string | number; label: string }> | string;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
searchable?: boolean;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export type { Prompt, AttributeDefinition, PaginationParams };
|
|||
// Re-export AttributeOption for backward compatibility
|
||||
export interface AttributeOption {
|
||||
value: string | number;
|
||||
label: string | { [key: string]: string };
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Prompts list hook
|
||||
|
|
@ -185,15 +185,10 @@ export function usePrompts() {
|
|||
fieldType = 'enum';
|
||||
// Handle options - can be array or string reference
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: String(opt.label ?? opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
@ -201,15 +196,10 @@ export function usePrompts() {
|
|||
fieldType = 'multiselect';
|
||||
// Handle options - can be array or string reference
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: String(opt.label ?? opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export interface RbacExportScope {
|
|||
|
||||
export interface RbacExportRole {
|
||||
roleLabel: string;
|
||||
description?: { [key: string]: string };
|
||||
description?: string;
|
||||
featureCode?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
|||
if (Array.isArray(attr.options)) {
|
||||
options = (attr.options as any[]).map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
||||
label: opt.label || String(opt.value),
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||
} else if (attr.type === 'multiselect') {
|
||||
|
|
@ -189,7 +189,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
|||
if (Array.isArray(attr.options)) {
|
||||
options = (attr.options as any[]).map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
||||
label: opt.label || String(opt.value),
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||
} else if (attr.type === 'textarea') fieldType = 'textarea';
|
||||
|
|
@ -224,7 +224,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
|||
if (Array.isArray(attr.options)) {
|
||||
options = (attr.options as any[]).map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
||||
label: opt.label || String(opt.value),
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||
} else if (attr.type === 'multiselect') {
|
||||
|
|
@ -232,7 +232,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
|
|||
if (Array.isArray(attr.options)) {
|
||||
options = (attr.options as any[]).map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: typeof opt.label === 'string' ? opt.label : opt.label?.en || String(opt.value),
|
||||
label: opt.label || String(opt.value),
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') optionsReference = attr.options;
|
||||
} else if (attr.type === 'textarea') fieldType = 'textarea';
|
||||
|
|
|
|||
|
|
@ -237,24 +237,18 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return { value: opt.value, label: labelValue };
|
||||
});
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value, label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
} else if (attr.type === 'multiselect') {
|
||||
fieldType = 'multiselect';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return { value: opt.value, label: labelValue };
|
||||
});
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value, label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
@ -303,24 +297,18 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return { value: opt.value, label: labelValue };
|
||||
});
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value, label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
} else if (attr.type === 'multiselect') {
|
||||
fieldType = 'multiselect';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return { value: opt.value, label: labelValue };
|
||||
});
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value, label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,15 +193,10 @@ export function useTrusteeAccess() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,15 +193,10 @@ export function useTrusteeContracts() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,15 +194,10 @@ export function useTrusteeDocuments() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,30 +191,20 @@ export function useTrusteeOrganisations() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
} else if (attr.type === 'multiselect') {
|
||||
fieldType = 'multiselect';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,15 +181,10 @@ export function useTrusteePositionDocuments() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,15 +207,10 @@ export function useTrusteePositions() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,15 +201,10 @@ export function useTrusteeRoles() {
|
|||
} else if (attr.type === 'select') {
|
||||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map((opt: any) => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map((opt: any) => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export interface UserMandateResponse {
|
|||
export interface Role {
|
||||
id: string;
|
||||
roleLabel: string;
|
||||
description?: string | { [key: string]: string };
|
||||
description?: string;
|
||||
mandateId?: string;
|
||||
featureInstanceId?: string;
|
||||
featureCode?: string;
|
||||
|
|
@ -60,7 +60,7 @@ export interface Role {
|
|||
|
||||
export interface Mandate {
|
||||
id: string;
|
||||
name: string | { [key: string]: string };
|
||||
name: string;
|
||||
label?: string;
|
||||
code?: string;
|
||||
language?: string;
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ export function useCurrentUser() {
|
|||
// Re-export AttributeOption for backward compatibility
|
||||
export interface AttributeOption {
|
||||
value: string | number;
|
||||
label: string | { [key: string]: string };
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Organization users hook (list, update, delete) - following prompts/workflows pattern
|
||||
|
|
@ -436,15 +436,10 @@ export function useOrgUsers() {
|
|||
fieldType = 'enum';
|
||||
// Handle options - can be array or string reference
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attr.options;
|
||||
|
|
@ -453,15 +448,10 @@ export function useOrgUsers() {
|
|||
fieldType = 'multiselect';
|
||||
// Handle options - can be array or string reference
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
// Options reference (e.g., "user.role", "auth.authority")
|
||||
optionsReference = attr.options;
|
||||
|
|
@ -582,12 +572,9 @@ export function useOrgUsers() {
|
|||
fieldType = 'enum';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
label: opt.label || String(opt.value)
|
||||
};
|
||||
});
|
||||
} else if (typeof attr.options === 'string') {
|
||||
|
|
@ -596,15 +583,10 @@ export function useOrgUsers() {
|
|||
} else if (attrType === 'multiselect') {
|
||||
fieldType = 'multiselect';
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export type { AttributeDefinition } from '../api/attributesApi';
|
|||
// Attribute option interface (from backend)
|
||||
export interface AttributeOption {
|
||||
value: string | number;
|
||||
label: string | { [key: string]: string }; // Can be string or object with language keys
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Pagination parameters
|
||||
|
|
@ -300,15 +300,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
fieldType = 'enum';
|
||||
// Handle options - can be array or string reference
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
@ -316,15 +311,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
fieldType = 'multiselect';
|
||||
// Handle options - can be array or string reference
|
||||
if (Array.isArray(attr.options)) {
|
||||
options = attr.options.map(opt => {
|
||||
const labelValue = typeof opt.label === 'string'
|
||||
? opt.label
|
||||
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
|
||||
return {
|
||||
options = attr.options.map(opt => ({
|
||||
value: opt.value,
|
||||
label: labelValue
|
||||
};
|
||||
});
|
||||
label: opt.label || String(opt.value)
|
||||
}));
|
||||
} else if (typeof attr.options === 'string') {
|
||||
optionsReference = attr.options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
* Stellt den Instanz-Kontext bereit und rendert Sidebar + Content.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Outlet, Navigate, useLocation } from 'react-router-dom';
|
||||
import { useCurrentInstance } from '../hooks/useCurrentInstance';
|
||||
import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore';
|
||||
import useNavigation from '../hooks/useNavigation';
|
||||
import styles from './FeatureLayout.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
|
@ -44,7 +45,7 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
|
|||
<h2>{t('Zugriff nicht möglich')}</h2>
|
||||
<p>{message}</p>
|
||||
<a href={returnPath} className={styles.errorLink}>
|
||||
Zurück zur Übersicht
|
||||
{t('Zurück zur Übersicht')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -65,10 +66,27 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
|
|||
* Bei Erfolg: Rendert <Outlet /> für die verschachtelten Routes
|
||||
*/
|
||||
export const FeatureLayout: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const location = useLocation();
|
||||
const initialized = useFeaturesInitialized();
|
||||
const loading = useFeaturesLoading();
|
||||
const { instance, mandate, feature, isValid, isLoading } = useCurrentInstance();
|
||||
const { instance, mandate, feature, isValid, isLoading, mandateId, featureCode, instanceId } = useCurrentInstance();
|
||||
const { dynamicBlock } = useNavigation();
|
||||
|
||||
const navLabels = useMemo(() => {
|
||||
if (!dynamicBlock || !mandateId) return null;
|
||||
const navMandate = dynamicBlock.mandates.find(m => m.id === mandateId);
|
||||
if (!navMandate) return null;
|
||||
const navFeature = featureCode ? navMandate.features.find(f => f.uiComponent === featureCode) : undefined;
|
||||
const navInstance = navFeature && instanceId
|
||||
? navFeature.instances.find(i => i.id === instanceId)
|
||||
: undefined;
|
||||
return {
|
||||
mandate: t(navMandate.uiLabel),
|
||||
feature: navFeature ? t(navFeature.uiLabel) : undefined,
|
||||
instance: navInstance ? t(navInstance.uiLabel) : undefined,
|
||||
};
|
||||
}, [dynamicBlock, mandateId, featureCode, instanceId, t]);
|
||||
|
||||
// Warten bis Features geladen sind
|
||||
if (!initialized || loading || isLoading) {
|
||||
|
|
@ -86,7 +104,7 @@ export const FeatureLayout: React.FC = () => {
|
|||
|
||||
return (
|
||||
<ErrorScreen
|
||||
message="Die angeforderte Feature-Instanz existiert nicht oder Sie haben keinen Zugriff."
|
||||
message={t('Die angeforderte Feature-Instanz existiert nicht oder Sie haben keinen Zugriff.')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -97,11 +115,11 @@ export const FeatureLayout: React.FC = () => {
|
|||
{/* Header mit Instanz-Info */}
|
||||
<header className={styles.featureHeader}>
|
||||
<div className={styles.breadcrumb}>
|
||||
<span className={styles.mandateName}>{mandate?.name}</span>
|
||||
<span className={styles.mandateName}>{navLabels?.mandate || mandate?.label || mandate?.name}</span>
|
||||
<span className={styles.separator}>/</span>
|
||||
<span className={styles.featureName}>{feature?.label?.de || feature?.code}</span>
|
||||
<span className={styles.featureName}>{navLabels?.feature || feature?.code}</span>
|
||||
<span className={styles.separator}>/</span>
|
||||
<span className={styles.instanceName}>{instance?.instanceLabel}</span>
|
||||
<span className={styles.instanceName}>{navLabels?.instance || instance?.instanceLabel}</span>
|
||||
</div>
|
||||
<div className={styles.roleIndicator}>
|
||||
<span className={styles.roleBadge}>{instance?.userRoles?.join(', ') || '-'}</span>
|
||||
|
|
@ -133,6 +151,7 @@ export const ProtectedFeatureRoute: React.FC<ProtectedFeatureRouteProps> = ({
|
|||
requiredView,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { instance, isValid } = useCurrentInstance();
|
||||
|
||||
if (!isValid) {
|
||||
|
|
@ -146,7 +165,7 @@ export const ProtectedFeatureRoute: React.FC<ProtectedFeatureRouteProps> = ({
|
|||
if (!hasViewAccess) {
|
||||
return (
|
||||
<ErrorScreen
|
||||
message={`Sie haben keine Berechtigung für diesen Bereich (${requiredView}).`}
|
||||
message={t('Sie haben keine Berechtigung für diesen Bereich ({view}).', { view: requiredView })}
|
||||
returnPath={`/mandates/${instance?.mandateId}/${instance?.featureCode}/${instance?.id}`}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -84,13 +84,13 @@ const MainLayoutInner: React.FC = () => {
|
|||
<nav className={styles.navigation}>
|
||||
{loading && (
|
||||
<div className={styles.loadingNav}>
|
||||
Lade Navigation...
|
||||
{t('Lade Navigation…')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className={styles.errorNav}>
|
||||
Fehler: {error}
|
||||
{t('Fehler')}: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,8 +170,8 @@ export const AutomationsDashboardPage: React.FC = () => {
|
|||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (v: string) => (
|
||||
<span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600, textTransform: 'capitalize' }}>
|
||||
{v}
|
||||
<span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600 }}>
|
||||
{t(v === 'completed' ? 'Abgeschlossen' : v === 'failed' ? 'Fehlgeschlagen' : v === 'running' ? 'Laufend' : v)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -85,7 +85,12 @@ export const DashboardPage: React.FC = () => {
|
|||
<h1>{t('Übersicht')}</h1>
|
||||
{totalInstances > 0 && (
|
||||
<p className={styles.subtitle}>
|
||||
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}.
|
||||
{t('Du hast Zugriff auf {instanceCount} {instanceWord} in {mandateCount} {mandateWord}.', {
|
||||
instanceCount: totalInstances,
|
||||
instanceWord: totalInstances === 1 ? t('Feature-Instanz') : t('Feature-Instanzen'),
|
||||
mandateCount: totalMandates,
|
||||
mandateWord: totalMandates === 1 ? t('Mandant') : t('Mandanten'),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -73,12 +73,12 @@ const ChatworkflowDashboard: React.FC = () => {
|
|||
|
||||
const ChatworkflowRuns: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return <PlaceholderView title="Runs" description={t('Workflow-Ausführungen')} />;
|
||||
return <PlaceholderView title={t('Ausführungen')} description={t('Workflow-Ausführungen')} />;
|
||||
};
|
||||
|
||||
const ChatworkflowFiles: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return <PlaceholderView title={t('Dateien')} description="Workflow-Dateien" />;
|
||||
return <PlaceholderView title={t('Dateien')} description={t('Workflow-Dateien')} />;
|
||||
};
|
||||
|
||||
// Chatbot Views
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const GDPRPage: React.FC = () => {
|
|||
} catch (error: any) {
|
||||
console.error('Failed to load GDPR consent info:', error);
|
||||
if (isActive) {
|
||||
setConsentError('Consent information could not be loaded.');
|
||||
setConsentError(t('Einwilligungsinformationen konnten nicht geladen werden.'));
|
||||
}
|
||||
} finally {
|
||||
if (isActive) {
|
||||
|
|
@ -82,7 +82,7 @@ export const GDPRPage: React.FC = () => {
|
|||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const handleDataExport = async () => {
|
||||
if (isActionLocked) return;
|
||||
|
|
@ -91,10 +91,10 @@ export const GDPRPage: React.FC = () => {
|
|||
try {
|
||||
const response = await api.get('/api/user/me/data-export');
|
||||
downloadJson(response.data, 'gdpr-data-export.json');
|
||||
setActionMessage({ type: 'success', text: 'Data export downloaded.' });
|
||||
setActionMessage({ type: 'success', text: t('Datenexport heruntergeladen.') });
|
||||
} catch (error: any) {
|
||||
console.error('GDPR export failed:', error);
|
||||
setActionMessage({ type: 'error', text: 'Data export failed. Please try again.' });
|
||||
setActionMessage({ type: 'error', text: t('Datenexport fehlgeschlagen. Bitte erneut versuchen.') });
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
|
|
@ -109,10 +109,10 @@ export const GDPRPage: React.FC = () => {
|
|||
headers: { Accept: 'application/ld+json' }
|
||||
});
|
||||
downloadJson(response.data, 'gdpr-data-portability.json', 'application/ld+json');
|
||||
setActionMessage({ type: 'success', text: 'Portable export downloaded.' });
|
||||
setActionMessage({ type: 'success', text: t('Portabler Export heruntergeladen.') });
|
||||
} catch (error: any) {
|
||||
console.error('GDPR portability export failed:', error);
|
||||
setActionMessage({ type: 'error', text: 'Portable export failed. Please try again.' });
|
||||
setActionMessage({ type: 'error', text: t('Portabler Export fehlgeschlagen. Bitte erneut versuchen.') });
|
||||
} finally {
|
||||
setIsPortabilityExporting(false);
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ export const GDPRPage: React.FC = () => {
|
|||
const handleDeleteAccount = async () => {
|
||||
setActionMessage(null);
|
||||
if (deleteConfirmText !== 'LOESCHEN') {
|
||||
setActionMessage({ type: 'error', text: 'Please type LOESCHEN to confirm deletion.' });
|
||||
setActionMessage({ type: 'error', text: t('Bitte geben Sie LOESCHEN ein, um die Löschung zu bestätigen.') });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +132,11 @@ export const GDPRPage: React.FC = () => {
|
|||
sessionStorage.removeItem('auth_authority');
|
||||
clearUserDataCache();
|
||||
setIsDeleted(true);
|
||||
setActionMessage({ type: 'success', text: 'Account deleted. Redirecting to login...' });
|
||||
setActionMessage({ type: 'success', text: t('Konto gelöscht. Weiterleitung zur Anmeldung…') });
|
||||
window.location.replace('/login');
|
||||
} catch (error: any) {
|
||||
console.error('GDPR deletion failed:', error);
|
||||
setActionMessage({ type: 'error', text: 'Account deletion failed. Please try again.' });
|
||||
setActionMessage({ type: 'error', text: t('Kontolöschung fehlgeschlagen. Bitte erneut versuchen.') });
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
|
|
@ -148,14 +148,14 @@ export const GDPRPage: React.FC = () => {
|
|||
<div>
|
||||
<h1 className={styles.title}>
|
||||
<FaShieldAlt className={styles.titleIcon} />
|
||||
GDPR / Privacy
|
||||
{t('DSGVO / Datenschutz')}
|
||||
</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Manage your personal data exports and account deletion.
|
||||
{t('Verwalten Sie Ihre personenbezogenen Datenexporte und Kontolöschung.')}
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/settings" className={styles.backLink}>
|
||||
Back to Settings
|
||||
{t('Zurück zu Einstellungen')}
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
|
|
@ -174,12 +174,12 @@ export const GDPRPage: React.FC = () => {
|
|||
{isExporting ? (
|
||||
<span className={styles.buttonSpinner}>
|
||||
<FaSpinner />
|
||||
Exporting...
|
||||
{t('Export wird erstellt…')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload />
|
||||
Export data
|
||||
{t('Daten exportieren')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -196,12 +196,12 @@ export const GDPRPage: React.FC = () => {
|
|||
{isPortabilityExporting ? (
|
||||
<span className={styles.buttonSpinner}>
|
||||
<FaSpinner />
|
||||
Exporting...
|
||||
{t('Export wird erstellt…')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<FaFileExport />
|
||||
Export portable data
|
||||
{t('Portabler Datenexport')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -217,13 +217,15 @@ export const GDPRPage: React.FC = () => {
|
|||
disabled={isActionLocked}
|
||||
>
|
||||
<FaTrash />
|
||||
Start deletion
|
||||
{t('Löschung starten')}
|
||||
</button>
|
||||
)}
|
||||
{showDeleteConfirm && (
|
||||
<div className={styles.deleteConfirm}>
|
||||
<p className={styles.deleteWarning}>
|
||||
This action is irreversible. Type <strong>LOESCHEN</strong> to confirm.
|
||||
{t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', {
|
||||
word: 'LOESCHEN',
|
||||
})}
|
||||
</p>
|
||||
<input
|
||||
className={styles.deleteInput}
|
||||
|
|
@ -241,7 +243,7 @@ export const GDPRPage: React.FC = () => {
|
|||
}}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
{t('Abbrechen')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.dangerButton}
|
||||
|
|
@ -251,12 +253,12 @@ export const GDPRPage: React.FC = () => {
|
|||
{isDeleting ? (
|
||||
<span className={styles.buttonSpinner}>
|
||||
<FaSpinner />
|
||||
Deleting...
|
||||
{t('Wird gelöscht…')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<FaTrash />
|
||||
Confirm deletion
|
||||
{t('Löschung bestätigen')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -314,7 +316,7 @@ export const GDPRPage: React.FC = () => {
|
|||
</ul>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>Contact</h3>
|
||||
<h3>{t('Kontakt')}</h3>
|
||||
<ul>
|
||||
{Object.entries({
|
||||
...(consentInfo.contact || {}),
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export const InvitePage: React.FC = () => {
|
|||
sessionStorage.removeItem('auth_authority');
|
||||
handleLoginRedirect();
|
||||
} else {
|
||||
setError(result.error || 'Fehler beim Annehmen der Einladung');
|
||||
setError(result.error || t('Fehler beim Annehmen der Einladung'));
|
||||
}
|
||||
|
||||
setAccepting(false);
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ function Login() {
|
|||
}}
|
||||
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
|
||||
</div>
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
|
|
@ -190,7 +190,7 @@ function Login() {
|
|||
</div>
|
||||
<div className={styles.disclaimer}>
|
||||
<p>
|
||||
Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.
|
||||
{t('Mit der Anmeldung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -198,7 +198,7 @@ function Login() {
|
|||
onClick={handleCredentialLogin}
|
||||
disabled={isLoginLoading}
|
||||
>
|
||||
{isLoginLoading ? "wird geladen..." : "Anmelden"}
|
||||
{isLoginLoading ? t('wird geladen…') : t('Anmelden')}
|
||||
</button>
|
||||
|
||||
<div className={styles.passwordResetLink}>
|
||||
|
|
@ -206,12 +206,12 @@ function Login() {
|
|||
className={styles.textButton}
|
||||
onClick={() => navigate("/password-reset-request")}
|
||||
>
|
||||
Passwort vergessen?
|
||||
{t('Passwort vergessen?')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider}>
|
||||
<span>oder</span>
|
||||
<span>{t('oder')}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -221,7 +221,7 @@ function Login() {
|
|||
>
|
||||
<div className={styles.buttonContent}>
|
||||
<FaMicrosoft />
|
||||
{isMsalLoading ? "Signing in..." : "Mit Microsoft anmelden"}
|
||||
{isMsalLoading ? t('Anmeldung läuft…') : t('Mit Microsoft anmelden')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ function Login() {
|
|||
>
|
||||
<div className={styles.buttonContent}>
|
||||
<FaGoogle />
|
||||
{isGoogleLoading ? "Signing in..." : "Mit Google anmelden"}
|
||||
{isGoogleLoading ? t('Anmeldung läuft…') : t('Mit Google anmelden')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
@ -245,7 +245,7 @@ function Login() {
|
|||
className={styles.ctaPrimary}
|
||||
onClick={() => navigate('/register', { state: location.state })}
|
||||
>
|
||||
Kostenlos registrieren
|
||||
{t('Kostenlos registrieren')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,26 +28,26 @@ function PasswordResetRequest() {
|
|||
setValidationError(null);
|
||||
|
||||
if (!username.trim()) {
|
||||
setValidationError('Bitte geben Sie Ihren Benutzernamen ein.');
|
||||
setValidationError(t('Bitte geben Sie Ihren Benutzernamen ein.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await requestReset(username.trim());
|
||||
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
|
||||
setSuccessMessage(t('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.'));
|
||||
|
||||
// Redirect to login after delay
|
||||
setTimeout(() => {
|
||||
navigate('/login', {
|
||||
state: {
|
||||
passwordResetRequested: true,
|
||||
message: 'Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.'
|
||||
message: t('Bitte prüfen Sie Ihre E-Mail für den Passwort-Reset-Link.')
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
// For security, still show success message even on error
|
||||
setSuccessMessage('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.');
|
||||
setSuccessMessage(t('Falls ein Konto mit diesem Benutzernamen existiert, wurde ein Reset-Link an die hinterlegte E-Mail-Adresse gesendet. Bitte prüfen Sie auch Ihren Spam-Ordner.'));
|
||||
setTimeout(() => {
|
||||
navigate('/login');
|
||||
}, 5000);
|
||||
|
|
@ -97,7 +97,7 @@ function PasswordResetRequest() {
|
|||
}}
|
||||
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
||||
<label className={usernameFocused || username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.infoMessage}>
|
||||
|
|
@ -109,7 +109,7 @@ function PasswordResetRequest() {
|
|||
onClick={handleSubmit}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Wird gesendet..." : "Reset-Link anfordern"}
|
||||
{isLoading ? t('Wird gesendet…') : t('Reset-Link anfordern')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -120,7 +120,7 @@ function PasswordResetRequest() {
|
|||
className={styles.textButton}
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
Login
|
||||
{t('Login')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,12 +58,12 @@ function Register() {
|
|||
|
||||
const _validateForm = (): boolean => {
|
||||
if (!formData.username || !formData.email || !formData.fullName) {
|
||||
setValidationError('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||
setValidationError(t('Bitte füllen Sie alle Pflichtfelder aus.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!formData.email.includes('@')) {
|
||||
setValidationError('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
|
||||
setValidationError(t('Bitte geben Sie eine gültige E-Mail-Adresse ein.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -83,19 +83,19 @@ function Register() {
|
|||
if (!availabilityResult.available) {
|
||||
const errorMessage = availabilityResult.message || 'Username is not available';
|
||||
if (errorMessage === 'Username is already taken') {
|
||||
setValidationError('Benutzername ist bereits vergeben');
|
||||
setValidationError(t('Benutzername ist bereits vergeben'));
|
||||
setUsernameHighlight(true);
|
||||
} else {
|
||||
setValidationError('Benutzername ist nicht verfügbar');
|
||||
setValidationError(t('Benutzername ist nicht verfügbar'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await register({ ...formData, registrationType: 'personal' });
|
||||
|
||||
let message = 'Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.';
|
||||
let message = t('Registrierung erfolgreich! Bitte prüfen Sie Ihre E-Mail (auch den Spam-Ordner) für den Link zum Setzen Ihres Passworts.');
|
||||
if (hasPendingInvitation) {
|
||||
message += ' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.';
|
||||
message += t(' Nach dem Setzen Ihres Passworts können Sie sich anmelden und Ihre Einladung annehmen.');
|
||||
}
|
||||
|
||||
setSuccessMessage(message);
|
||||
|
|
@ -104,7 +104,7 @@ function Register() {
|
|||
navigate('/login', {
|
||||
state: {
|
||||
registered: true,
|
||||
message: 'Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.',
|
||||
message: t('Registrierung erfolgreich. Bitte prüfen Sie Ihre E-Mail für den Passwort-Link.'),
|
||||
...(location.state || {})
|
||||
}
|
||||
});
|
||||
|
|
@ -116,9 +116,9 @@ function Register() {
|
|||
|
||||
const _getErrorMessage = () => {
|
||||
if (validationError) return validationError;
|
||||
if (registerError) return typeof registerError === 'string' ? registerError : 'Registration failed';
|
||||
if (msalError) return typeof msalError === 'string' ? msalError : 'Microsoft registration failed';
|
||||
if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : 'Username availability check failed';
|
||||
if (registerError) return typeof registerError === 'string' ? registerError : t('Registrierung fehlgeschlagen');
|
||||
if (msalError) return typeof msalError === 'string' ? msalError : t('Microsoft-Registrierung fehlgeschlagen');
|
||||
if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : t('Benutzernamen-Prüfung fehlgeschlagen');
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ function Register() {
|
|||
onBlur={() => setUsernameFocused(false)}
|
||||
className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`}
|
||||
/>
|
||||
<label className={usernameFocused || formData.username ? styles.focusedLabel : styles.label}>Benutzername</label>
|
||||
<label className={usernameFocused || formData.username ? styles.focusedLabel : styles.label}>{t('Benutzername')}</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.floatingLabelInput}>
|
||||
|
|
@ -177,7 +177,7 @@ function Register() {
|
|||
onBlur={() => setEmailFocused(false)}
|
||||
className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>E-Mail</label>
|
||||
<label className={emailFocused || formData.email ? styles.focusedLabel : styles.label}>{t('E-Mail')}</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.floatingLabelInput}>
|
||||
|
|
@ -200,7 +200,7 @@ function Register() {
|
|||
|
||||
<div className={styles.disclaimer}>
|
||||
<p>
|
||||
Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.
|
||||
{t('Mit der Registrierung stimmen Sie unseren Datenschutzbestimmungen zur KI-Nutzung zu.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ function Register() {
|
|||
onClick={handleSubmit}
|
||||
disabled={isLoading || isChecking}
|
||||
>
|
||||
{isLoading ? "Registrierung läuft..." : isChecking ? "Benutzername wird geprüft..." : 'Kostenlos registrieren'}
|
||||
{isLoading ? t('Registrierung läuft…') : isChecking ? t('Benutzername wird geprüft…') : t('Kostenlos registrieren')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -220,7 +220,7 @@ function Register() {
|
|||
className={styles.textButton}
|
||||
onClick={() => navigate("/login", { state: location.state })}
|
||||
>
|
||||
Jetzt anmelden
|
||||
{t('Jetzt anmelden')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ function Reset() {
|
|||
|
||||
// Validate token exists and format
|
||||
if (!token) {
|
||||
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
|
||||
setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'));
|
||||
} else if (!_isValidUUID(token)) {
|
||||
setTokenError('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.');
|
||||
setTokenError(t('Ungültiger Reset-Link. Bitte fordern Sie einen neuen Link an.'));
|
||||
}
|
||||
}, [token]);
|
||||
}, [token, t]);
|
||||
|
||||
const _isValidUUID = (str: string): boolean => {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
|
@ -44,12 +44,12 @@ function Reset() {
|
|||
|
||||
const validateForm = (): boolean => {
|
||||
if (!password || password.length < 8) {
|
||||
setValidationError('Passwort muss mindestens 8 Zeichen lang sein.');
|
||||
setValidationError(t('Passwort muss mindestens 8 Zeichen lang sein.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setValidationError('Die Passwörter stimmen nicht überein.');
|
||||
setValidationError(t('Die Passwörter stimmen nicht überein.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -65,28 +65,28 @@ function Reset() {
|
|||
}
|
||||
|
||||
if (!token) {
|
||||
setValidationError('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.');
|
||||
setValidationError(t('Token fehlt. Bitte fordern Sie einen neuen Reset-Link an.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await resetPassword(token, password);
|
||||
setSuccessMessage('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet...');
|
||||
setSuccessMessage(t('Passwort erfolgreich gesetzt! Sie werden zum Login weitergeleitet…'));
|
||||
|
||||
// Redirect to login after delay
|
||||
setTimeout(() => {
|
||||
navigate('/login', {
|
||||
state: {
|
||||
passwordReset: true,
|
||||
message: 'Passwort erfolgreich geändert. Bitte melden Sie sich an.'
|
||||
message: t('Passwort erfolgreich geändert. Bitte melden Sie sich an.')
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
// Error is already set by the hook
|
||||
const errorMessage = err?.response?.data?.detail || err?.message || 'Passwort-Zurücksetzung fehlgeschlagen.';
|
||||
const errorMessage = err?.response?.data?.detail || err?.message || t('Passwort-Zurücksetzung fehlgeschlagen.');
|
||||
if (errorMessage.includes('abgelaufen') || errorMessage.includes('expired') || errorMessage.includes('Ungültig') || errorMessage.includes('invalid')) {
|
||||
setValidationError('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.');
|
||||
setValidationError(t('Der Reset-Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.'));
|
||||
} else {
|
||||
setValidationError(errorMessage);
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ function Reset() {
|
|||
className={styles.textButton}
|
||||
onClick={() => navigate("/password-reset-request")}
|
||||
>
|
||||
Neuen Reset-Link anfordern
|
||||
{t('Neuen Reset-Link anfordern')}
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.registerLink}>
|
||||
|
|
@ -124,7 +124,7 @@ function Reset() {
|
|||
className={styles.textButton}
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
Login
|
||||
{t('Login')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -200,7 +200,7 @@ function Reset() {
|
|||
className={`${styles.button} ${styles.loginButton}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Wird gespeichert..." : "Passwort setzen"}
|
||||
{isLoading ? t('Wird gespeichert…') : t('Passwort setzen')}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
|
@ -211,7 +211,7 @@ function Reset() {
|
|||
className={styles.textButton}
|
||||
onClick={() => navigate("/login")}
|
||||
>
|
||||
Login
|
||||
{t('Login')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
|
|||
await onSave(formData);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Speichern des Profils');
|
||||
setError(err.message || t('Fehler beim Speichern des Profils'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
|
|
@ -520,17 +520,17 @@ export const SettingsPage: React.FC = () => {
|
|||
</div>
|
||||
{currentUser && (
|
||||
<div className={styles.userInfoCard}>
|
||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>Benutzername</span><span className={styles.userInfoValue}>{currentUser.username}</span></div>
|
||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>Name</span><span className={styles.userInfoValue}>{currentUser.fullName || '-'}</span></div>
|
||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>E-Mail</span><span className={styles.userInfoValue}>{currentUser.email || '-'}</span></div>
|
||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>{t('Benutzername')}</span><span className={styles.userInfoValue}>{currentUser.username}</span></div>
|
||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>{t('Name')}</span><span className={styles.userInfoValue}>{currentUser.fullName || '-'}</span></div>
|
||||
<div className={styles.userInfoRow}><span className={styles.userInfoLabel}>{t('E-Mail')}</span><span className={styles.userInfoValue}>{currentUser.email || '-'}</span></div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Ueber</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('Applikation')}</h2>
|
||||
<div className={styles.infoCard}>
|
||||
<div className={styles.infoRow}><span className={styles.infoLabel}>Version</span><span className={styles.infoValue}>2.0.0</span></div>
|
||||
<div className={styles.infoRow}><span className={styles.infoLabel}>Build</span><span className={styles.infoValue}>2026.03.23</span></div>
|
||||
<div className={styles.infoRow}><span className={styles.infoLabel}>{t('Version')}</span><span className={styles.infoValue}>2.0.0</span></div>
|
||||
<div className={styles.infoRow}><span className={styles.infoLabel}>{t('Build')}</span><span className={styles.infoValue}>2026.03.23</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -24,18 +24,13 @@ import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard';
|
|||
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import { labelAsI18nKey } from '../../types/mandate';
|
||||
|
||||
function getMandateName(mandate: Mandate): string {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
return mandate.name || mandate.id;
|
||||
return mandate.label || mandate.name || mandate.id;
|
||||
}
|
||||
|
||||
function getFeatureLabel(feature: Feature, t: (k: string) => string): string {
|
||||
return t(labelAsI18nKey(feature.label, feature.code));
|
||||
return t(feature.label || feature.code);
|
||||
}
|
||||
|
||||
export interface InstanceWithStats extends FeatureInstance {
|
||||
|
|
@ -146,15 +141,19 @@ export const AccessManagementHub: React.FC = () => {
|
|||
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
||||
if (result.success && result.data) {
|
||||
showSuccess(
|
||||
'Rollen synchronisiert',
|
||||
`Hinzugefügt: ${result.data.added}, Entfernt: ${result.data.removed}, Unverändert: ${result.data.unchanged}`
|
||||
t('Rollen synchronisiert'),
|
||||
t('Hinzugefügt: {added}, Entfernt: {removed}, Unverändert: {unchanged}', {
|
||||
added: result.data.added,
|
||||
removed: result.data.removed,
|
||||
unchanged: result.data.unchanged,
|
||||
})
|
||||
);
|
||||
fetchInstances(selectedMandateId, selectedFeatureCode || undefined);
|
||||
} else {
|
||||
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren');
|
||||
showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren'));
|
||||
}
|
||||
} catch {
|
||||
showError('Fehler', 'Rollen konnten nicht synchronisiert werden');
|
||||
showError(t('Fehler'), t('Rollen konnten nicht synchronisiert werden'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -295,7 +294,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
return {
|
||||
id: inst.id,
|
||||
label: inst.label,
|
||||
featureLabel: feature ? getFeatureLabel(feature) : inst.featureCode,
|
||||
featureLabel: feature ? getFeatureLabel(feature, t) : inst.featureCode,
|
||||
userCount: inst.userCount ?? 0,
|
||||
};
|
||||
}),
|
||||
|
|
@ -307,9 +306,11 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||||
<p className={styles.errorMessage}>
|
||||
{t('Fehler')}: {error}
|
||||
</p>
|
||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||
<FaSync /> Erneut versuchen
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -320,9 +321,9 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<div className={styles.adminPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Zugriffsverwaltung</h1>
|
||||
<h1 className={styles.pageTitle}>{t('Zugriffsverwaltung')}</h1>
|
||||
<p className={styles.pageSubtitle}>
|
||||
Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten
|
||||
{t('Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -334,7 +335,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
Mandant:
|
||||
{t('Mandant')}:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
|
|
@ -352,7 +353,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
Feature:
|
||||
{t('Feature')}:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
|
|
@ -376,14 +377,14 @@ export const AccessManagementHub: React.FC = () => {
|
|||
}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowWizard(true)}
|
||||
disabled={features.length === 0}
|
||||
>
|
||||
+ Neue Instanz erstellen
|
||||
+ {t('Neue Instanz erstellen')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -398,21 +399,21 @@ export const AccessManagementHub: React.FC = () => {
|
|||
className={viewMode === 'list' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
|
||||
onClick={() => setViewMode('list')}
|
||||
>
|
||||
<FaList /> Listenansicht
|
||||
<FaList /> {t('Listenansicht')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={viewMode === 'hierarchy' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
|
||||
onClick={() => setViewMode('hierarchy')}
|
||||
>
|
||||
<FaSitemap /> Hierarchie
|
||||
<FaSitemap /> {t('Hierarchie')}
|
||||
</button>
|
||||
</div>
|
||||
<Link to="/admin/mandates" className={hubStyles.mandatesLink}>
|
||||
<FaBuilding /> Mandanten verwalten
|
||||
<FaBuilding /> {t('Mandanten verwalten')}
|
||||
</Link>
|
||||
<Link to="/admin/user-mandates" className={hubStyles.mandatesLink}>
|
||||
<FaUsers /> Mandant-Benutzer
|
||||
<FaUsers /> {t('Mandant-Benutzer')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
|
@ -423,7 +424,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
instancesByMandate={instancesByMandate}
|
||||
instanceUsersMap={instanceUsersMap}
|
||||
features={features}
|
||||
getFeatureLabel={getFeatureLabel}
|
||||
getFeatureLabel={(f) => getFeatureLabel(f, t)}
|
||||
loading={hierarchyUsersLoading}
|
||||
onOpenDetail={handleOpenDetail}
|
||||
/>
|
||||
|
|
@ -432,7 +433,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.
|
||||
{t('Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -444,7 +445,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<span className={hubStyles.statsValue}>
|
||||
{loading || statsLoading ? '…' : overviewStats.instances}
|
||||
</span>
|
||||
<span className={hubStyles.statsLabel}>Instanzen</span>
|
||||
<span className={hubStyles.statsLabel}>{t('Instanzen')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={hubStyles.statsCard}>
|
||||
|
|
@ -469,7 +470,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<div className={hubStyles.diagramCard}>
|
||||
<FaLink className={hubStyles.statsIcon} />
|
||||
<div className={hubStyles.diagramContent}>
|
||||
<span className={hubStyles.diagramTitle}>Beziehungen</span>
|
||||
<span className={hubStyles.diagramTitle}>{t('Beziehungen')}</span>
|
||||
<div className={hubStyles.diagramFlow}>
|
||||
<div className={hubStyles.diagramNode}>{relationshipData.mandateName}</div>
|
||||
<div className={hubStyles.diagramNodes}>
|
||||
|
|
@ -480,7 +481,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
))}
|
||||
{relationshipData.instances.length > 5 && (
|
||||
<div className={hubStyles.diagramNodeSmall}>
|
||||
+{relationshipData.instances.length - 5} weitere
|
||||
+{relationshipData.instances.length - 5} {t('weitere')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -491,7 +492,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<section className={hubStyles.section}>
|
||||
<h2 className={hubStyles.sectionTitle}>Feature-Instanzen</h2>
|
||||
<h2 className={hubStyles.sectionTitle}>{t('Feature-Instanzen')}</h2>
|
||||
{loading && filteredInstances.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
|
|
@ -502,14 +503,14 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanzen')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.
|
||||
{t('Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.')}
|
||||
</p>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowWizard(true)}
|
||||
disabled={features.length === 0}
|
||||
>
|
||||
+ Erste Instanz erstellen
|
||||
+ {t('Erste Instanz erstellen')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -526,8 +527,12 @@ export const AccessManagementHub: React.FC = () => {
|
|||
</div>
|
||||
<div className={hubStyles.instanceMeta}>
|
||||
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span>
|
||||
<span>{inst.userCount ?? '—'} Benutzer</span>
|
||||
<span>{inst.roleCount ?? '—'} Rollen</span>
|
||||
<span>
|
||||
{inst.userCount ?? '—'} {t('Benutzer')}
|
||||
</span>
|
||||
<span>
|
||||
{inst.roleCount ?? '—'} {t('Rollen')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={hubStyles.instanceActions}>
|
||||
<button
|
||||
|
|
@ -535,7 +540,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
className={hubStyles.cardAction}
|
||||
onClick={() => handleOpenDetail(inst, selectedMandateId)}
|
||||
>
|
||||
<FaUsers /> Benutzer verwalten
|
||||
<FaUsers /> {t('Benutzer verwalten')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -544,7 +549,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
disabled={!inst.enabled}
|
||||
title={t('Rollen synchronisieren')}
|
||||
>
|
||||
<FaCogs /> Rollen sync
|
||||
<FaCogs /> {t('Rollen sync')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { TextField } from '../../components/UiComponents/TextField';
|
|||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import { labelAsI18nKey } from '../../types/mandate';
|
||||
|
||||
export const AdminFeatureAccessPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
|
@ -94,7 +93,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
render: (value: string) => {
|
||||
const feature = features.find(f => f.code === value);
|
||||
if (feature) {
|
||||
return t(labelAsI18nKey(feature.label, value));
|
||||
return t(feature.label || value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
|
@ -122,7 +121,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
try {
|
||||
// Validate label
|
||||
if (!createLabel || createLabel.trim() === '') {
|
||||
showError('Fehler', 'Label ist erforderlich.');
|
||||
showError(t('Fehler'), t('Label ist erforderlich.'));
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -132,7 +131,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
if (createFeatureCode === 'chatbot') {
|
||||
// Validate required fields
|
||||
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
||||
showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.');
|
||||
showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.'));
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -176,9 +175,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
setChatbotAllowedProviders([]);
|
||||
fetchInstances(selectedMandateId);
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`);
|
||||
showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel }));
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -228,7 +227,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
if (editingInstance.featureCode === 'chatbot') {
|
||||
// Validate required fields
|
||||
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') {
|
||||
showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.');
|
||||
showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.'));
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -270,9 +269,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
setChatbotAllowedProviders([]);
|
||||
fetchInstances(selectedMandateId);
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`);
|
||||
showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label }));
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Feature-Instanz');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -285,10 +284,10 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
const result = await deleteInstance(selectedMandateId, instanceId);
|
||||
if (result.success) {
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
showSuccess('Instanz gelöscht', 'Die Feature-Instanz wurde gelöscht.');
|
||||
showSuccess(t('Instanz gelöscht'), t('Die Feature-Instanz wurde gelöscht.'));
|
||||
return true;
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Löschen der Feature-Instanz');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Feature-Instanz'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -301,11 +300,15 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
||||
if (result.success && result.data) {
|
||||
showSuccess(
|
||||
'Rollen synchronisiert',
|
||||
`Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}`
|
||||
t('Rollen synchronisiert'),
|
||||
t('Hinzugefügt: {added}\nEntfernt: {removed}\nUnverändert: {unchanged}', {
|
||||
added: result.data.added,
|
||||
removed: result.data.removed,
|
||||
unchanged: result.data.unchanged,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen');
|
||||
showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Rollen'));
|
||||
}
|
||||
} finally {
|
||||
setSyncingInstance(null);
|
||||
|
|
@ -314,18 +317,14 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
|
||||
// Get mandate name
|
||||
const getMandateName = (mandate: Mandate) => {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
return mandate.name || mandate.id;
|
||||
return mandate.label || mandate.name || mandate.id;
|
||||
};
|
||||
|
||||
// Get feature label
|
||||
const getFeatureLabel = (code: string) => {
|
||||
const feature = features.find(f => f.code === code);
|
||||
if (feature) {
|
||||
return t(labelAsI18nKey(feature.label, code));
|
||||
return t(feature.label || code);
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
|
@ -337,7 +336,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||
<FaSync /> Erneut versuchen
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -348,7 +347,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Feature-Instanzen</h1>
|
||||
<h1 className={styles.pageTitle}>{t('Feature-Instanzen')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('Verwalten Sie Feature-Instanzen für jeden')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -358,7 +357,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
Mandant auswählen:
|
||||
{t('Mandant auswählen:')}
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
|
|
@ -381,15 +380,19 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
onClick={() => fetchInstances(selectedMandateId)}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
disabled={features.length === 0}
|
||||
title={features.length === 0 ? 'Keine Features verfügbar. Bitte laden Sie die Seite neu oder prüfen Sie die Konsole auf Fehler.' : undefined}
|
||||
title={
|
||||
features.length === 0
|
||||
? t('Keine Features verfügbar. Bitte laden Sie die Seite neu oder prüfen Sie die Konsole auf Fehler.')
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<FaPlus /> Neue Instanz
|
||||
<FaPlus /> {t('Neue Instanz')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -411,16 +414,17 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
Keine Features geladen.
|
||||
{error ? ` Fehler: ${error}` : ' Die API hat keine Features zurückgegeben.'}
|
||||
{' '}Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/
|
||||
{t('Keine Features geladen.')}
|
||||
{error ? ` Fehler: ${error}` : ` ${t('Die API hat keine Features zurückgegeben.')}`}
|
||||
{' '}
|
||||
{t('Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/')}
|
||||
</span>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchFeatures()}
|
||||
style={{ marginLeft: '1rem' }}
|
||||
>
|
||||
<FaSync /> Features erneut laden
|
||||
<FaSync /> {t('Features erneut laden')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -431,7 +435,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.
|
||||
{t('Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -505,12 +509,12 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
{/* Feature Code Selector - Required for chatbot config */}
|
||||
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
|
||||
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
|
||||
Feature auswählen: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||
{t('Feature auswählen')}: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||
</label>
|
||||
<DropdownSelect
|
||||
items={features.map(f => ({
|
||||
id: f.code,
|
||||
label: t(labelAsI18nKey(f.label, f.code)),
|
||||
label: t(f.label || f.code),
|
||||
value: f.code
|
||||
}))}
|
||||
selectedItemId={createFeatureCode}
|
||||
|
|
@ -528,7 +532,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
/>
|
||||
{!createFeatureCode && (
|
||||
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
|
||||
Bitte wählen Sie ein Feature aus, um fortzufahren.
|
||||
{t('Bitte wählen Sie ein Feature aus, um fortzufahren.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -536,7 +540,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
{/* Chatbot Configuration Title - Show when chatbot is selected */}
|
||||
{createFeatureCode === 'chatbot' && (
|
||||
<h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
||||
Chatbot-Konfiguration
|
||||
{t('Chatbot-Konfiguration')}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
|
|
@ -544,7 +548,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
{createFeatureCode && (
|
||||
<div className={styles.configField} style={{ marginBottom: '1.5rem' }}>
|
||||
<label className={styles.configLabel}>
|
||||
Label: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||
{t('Label')}: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||
</label>
|
||||
<TextField
|
||||
type="text"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import api from '../../api';
|
|||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import { labelAsI18nKey } from '../../types/mandate';
|
||||
|
||||
export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
|
@ -93,7 +92,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
allOptions.push({
|
||||
mandateId: mandate.id,
|
||||
instanceId: inst.id,
|
||||
mandateName: mandate.label || (typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id)),
|
||||
mandateName: mandate.label || mandate.name || mandate.id,
|
||||
instanceLabel: inst.label || inst.id,
|
||||
featureCode: inst.featureCode,
|
||||
combinedKey: `${mandate.id}:${inst.id}`,
|
||||
|
|
@ -314,9 +313,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
setShowAddModal(false);
|
||||
refreshUsers();
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
showSuccess('Benutzer hinzugefügt', 'Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.');
|
||||
showSuccess(t('Benutzer hinzugefügt'), t('Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.'));
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers'));
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -338,9 +337,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
setEditingUser(null);
|
||||
refreshUsers();
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
showSuccess('Eintrag aktualisiert', 'Rollen und Aktiv-Status wurden erfolgreich aktualisiert.');
|
||||
showSuccess(t('Eintrag aktualisiert'), t('Rollen und Aktiv-Status wurden erfolgreich aktualisiert.'));
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Aktualisieren');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren'));
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -354,9 +353,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
if (result.success) {
|
||||
refreshUsers();
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`);
|
||||
showSuccess(t('Benutzer entfernt'), t('"{name}" wurde aus der Feature-Instanz entfernt.', { name: user.username }));
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -369,7 +368,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
const getFeatureLabel = (code: string) => {
|
||||
const feature = features.find(f => f.code === code);
|
||||
if (feature) {
|
||||
return t(labelAsI18nKey(feature.label, code));
|
||||
return t(feature.label || code);
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
|
@ -396,9 +395,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||||
<p className={styles.errorMessage}>
|
||||
{t('Fehler')}: {error}
|
||||
</p>
|
||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||
<FaSync /> Erneut versuchen
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -419,7 +420,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
Mandant / Feature-Instanz:
|
||||
{t('Mandant / Feature-Instanz')}:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
|
|
@ -458,14 +459,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
onClick={() => refreshUsers()}
|
||||
disabled={usersLoading}
|
||||
>
|
||||
<FaSync className={usersLoading ? 'spinning' : ''} /> Aktualisieren
|
||||
<FaSync className={usersLoading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
disabled={availableUsers.length === 0 || instanceRoles.length === 0}
|
||||
>
|
||||
<FaPlus /> Benutzer hinzufügen
|
||||
<FaPlus /> {t('Benutzer hinzufügen')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -475,10 +476,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
{selectedOption && (
|
||||
<div className={styles.infoBox}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
<span>Mandant: <strong>{selectedOption.mandateName}</strong></span>
|
||||
<span>
|
||||
{t('Mandant')}: <strong>{selectedOption.mandateName}</strong>
|
||||
</span>
|
||||
<span style={{ margin: '0 16px', color: 'var(--color-border)' }}>|</span>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Instanz: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})</span>
|
||||
<span>
|
||||
{t('Instanz')}: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -497,7 +502,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
|
||||
{/* Warning if no roles available */}
|
||||
{selectedInstance && instanceRoles.length === 0 && !usersLoading && (
|
||||
<div className={styles.warningBox || styles.infoBox}>
|
||||
<div className={styles.infoBox} style={{ borderColor: 'var(--warning-color, #d69e2e)', backgroundColor: 'var(--warning-bg, rgba(214, 158, 46, 0.12))' }}>
|
||||
<span>⚠️ </span>
|
||||
<span>{t('Diese Instanz hat noch keine')}</span>
|
||||
</div>
|
||||
|
|
@ -595,7 +600,9 @@ export const AdminFeatureInstanceUsersPage: 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}>Rollen bearbeiten: {editingUser.username}</h2>
|
||||
<h2 className={styles.modalTitle}>
|
||||
{t('Rollen bearbeiten')}: {editingUser.username}
|
||||
</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setEditingUser(null)}
|
||||
|
|
|
|||
|
|
@ -23,18 +23,18 @@ import { useLanguage } from '../../providers/language/LanguageContext';
|
|||
|
||||
interface Feature {
|
||||
id?: string;
|
||||
code: string; // Backend uses 'code' not 'featureCode'
|
||||
featureCode?: string; // Alias for backward compatibility
|
||||
label: string | { [key: string]: string }; // Backend uses 'label' not 'name'
|
||||
name?: string | { [key: string]: string }; // Alias for backward compatibility
|
||||
description?: string | { [key: string]: string };
|
||||
code: string;
|
||||
featureCode?: string;
|
||||
label: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface FeatureRole {
|
||||
id: string;
|
||||
roleLabel: string;
|
||||
description?: { [key: string]: string };
|
||||
description?: string;
|
||||
featureCode: string;
|
||||
mandateId?: string | null;
|
||||
featureInstanceId?: string | null;
|
||||
|
|
@ -118,17 +118,14 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedFeatureCode]);
|
||||
}, [selectedFeatureCode, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, [fetchRoles]);
|
||||
|
||||
// Get text from multilingual object
|
||||
const getTextValue = (value: string | { [key: string]: string } | undefined): string => {
|
||||
if (!value) return '-';
|
||||
if (typeof value === 'string') return value;
|
||||
return value.de || value.en || Object.values(value)[0] || '-';
|
||||
const getTextValue = (value: string | undefined): string => {
|
||||
return value || '-';
|
||||
};
|
||||
|
||||
// Table columns
|
||||
|
|
@ -148,7 +145,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
type: 'string' as const,
|
||||
sortable: false,
|
||||
width: 300,
|
||||
formatter: (value: string | { [key: string]: string }) => getTextValue(value)
|
||||
formatter: (value: string) => getTextValue(value)
|
||||
},
|
||||
{
|
||||
key: 'featureCode',
|
||||
|
|
@ -178,9 +175,9 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
{
|
||||
name: 'description',
|
||||
label: t('Beschreibung'),
|
||||
type: 'multilingual',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
description: t('Mehrsprachige Beschreibung')
|
||||
description: t('Beschreibung der Rolle')
|
||||
}
|
||||
];
|
||||
return fields;
|
||||
|
|
@ -200,15 +197,15 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
{
|
||||
name: 'description',
|
||||
label: t('Beschreibung'),
|
||||
type: 'multilingual',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
description: t('Mehrsprachige Beschreibung')
|
||||
description: t('Beschreibung der Rolle')
|
||||
}
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
// Handle create role
|
||||
const handleCreateRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => {
|
||||
const handleCreateRole = async (data: { roleLabel: string; description?: string }) => {
|
||||
if (!selectedFeatureCode) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
|
|
@ -216,20 +213,20 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
params.append('roleLabel', data.roleLabel);
|
||||
params.append('featureCode', selectedFeatureCode);
|
||||
|
||||
await api.post(`/api/features/templates/roles?${params.toString()}`, data.description || {});
|
||||
await api.post(`/api/features/templates/roles?${params.toString()}`, data.description ?? '');
|
||||
|
||||
setShowCreateModal(false);
|
||||
await fetchRoles();
|
||||
} catch (err: any) {
|
||||
console.error('Error creating role:', err);
|
||||
showError('Fehler', err.response?.data?.detail || 'Fehler beim Erstellen der Rolle');
|
||||
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Erstellen der Rolle'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit role
|
||||
const handleEditRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => {
|
||||
const handleEditRole = async (data: { roleLabel: string; description?: string }) => {
|
||||
if (!editingRole) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
|
|
@ -241,7 +238,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
await fetchRoles();
|
||||
} catch (err: any) {
|
||||
console.error('Error updating role:', err);
|
||||
showError('Fehler', err.response?.data?.detail || 'Fehler beim Aktualisieren der Rolle');
|
||||
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Aktualisieren der Rolle'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
|
@ -254,7 +251,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
await fetchRoles();
|
||||
} catch (err: any) {
|
||||
console.error('Error deleting role:', err);
|
||||
showError('Fehler', err.response?.data?.detail || 'Fehler beim Löschen der Rolle');
|
||||
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Löschen der Rolle'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -276,7 +273,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>{error}</p>
|
||||
<button className={styles.secondaryButton} onClick={() => window.location.reload()}>
|
||||
<FaSync /> Erneut versuchen
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -297,7 +294,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
Feature:
|
||||
{t('Feature:')}
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
|
|
@ -323,13 +320,13 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
onClick={() => fetchRoles()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<FaPlus /> Neue Feature-Rolle
|
||||
<FaPlus /> {t('Neue Feature-Rolle')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -340,8 +337,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.infoBox}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
<strong>Feature-Template-Rollen</strong> werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert.
|
||||
Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.
|
||||
<strong>{t('Feature-Template-Rollen')}</strong>{' '}
|
||||
{t('werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert. Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -352,7 +349,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.
|
||||
{t('Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -414,14 +411,16 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.modalContent}>
|
||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Feature: <strong>{selectedFeatureCode}</strong></span>
|
||||
<span>
|
||||
{t('Feature')}: <strong>{selectedFeatureCode}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<FormGeneratorForm
|
||||
attributes={createFields}
|
||||
mode="create"
|
||||
onSubmit={handleCreateRole}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'}
|
||||
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -445,7 +444,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.modalContent}>
|
||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Feature: <strong>{editingRole.featureCode}</strong></span>
|
||||
<span>{t('Feature:')} <strong>{editingRole.featureCode}</strong></span>
|
||||
</div>
|
||||
<FormGeneratorForm
|
||||
attributes={editFields}
|
||||
|
|
@ -468,7 +467,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>
|
||||
<FaShieldAlt style={{ marginRight: 8 }} />
|
||||
Berechtigungen: {permissionsRole.roleLabel}
|
||||
{t('Berechtigungen')}: {permissionsRole.roleLabel}
|
||||
</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
|
|
@ -480,7 +479,9 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.modalContent}>
|
||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Feature: <strong>{permissionsRole.featureCode}</strong></span>
|
||||
<span>
|
||||
{t('Feature')}: <strong>{permissionsRole.featureCode}</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: '1rem' }}>{t('Template-Rolle global')}</span>
|
||||
</div>
|
||||
<AccessRulesEditor
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue