fixed language logic items

This commit is contained in:
ValueOn AG 2026-04-11 19:44:52 +02:00
parent fbfe85f225
commit d1f0b3c3d6
155 changed files with 2091 additions and 2057 deletions

View file

@ -18,7 +18,7 @@ export interface AttributeDefinition {
description?: string; description?: string;
required?: boolean; required?: boolean;
default?: any; default?: any;
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; options?: Array<{ value: string | number; label: string }> | string;
validation?: any; validation?: any;
ui?: any; ui?: any;
readonly?: boolean; readonly?: boolean;

View file

@ -16,7 +16,7 @@ export interface Prompt {
export interface AttributeOption { export interface AttributeOption {
value: string | number; value: string | number;
label: string | { [key: string]: string }; label: string;
} }
export interface AttributeDefinition { export interface AttributeDefinition {

View file

@ -28,7 +28,7 @@ export interface AttributeDefinition {
description?: string; description?: string;
required?: boolean; required?: boolean;
default?: any; default?: any;
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; options?: Array<{ value: string | number; label: string }> | string;
validation?: any; validation?: any;
sortable?: boolean; sortable?: boolean;
filterable?: boolean; filterable?: boolean;

View file

@ -86,7 +86,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
<button <button
className={`${styles.iconButton} ${styles.danger}`} className={`${styles.iconButton} ${styles.danger}`}
onClick={() => onDelete(rule.id)} onClick={() => onDelete(rule.id)}
title={t('delete rule')} title={t('Regel löschen')}
> >
<FaTrash /> <FaTrash />
</button> </button>
@ -97,7 +97,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
<div className={styles.permissionsGrid}> <div className={styles.permissionsGrid}>
{/* View Toggle */} {/* View Toggle */}
<div className={styles.permissionItem}> <div className={styles.permissionItem}>
<span className={styles.permissionLabel}>View</span> <span className={styles.permissionLabel}>{t('Ansicht')}</span>
<div className={styles.viewToggle}> <div className={styles.viewToggle}>
<input <input
type="checkbox" type="checkbox"
@ -113,7 +113,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
{isDataRule ? ( {isDataRule ? (
<> <>
<div className={styles.permissionItem}> <div className={styles.permissionItem}>
<span className={styles.permissionLabel}>Read</span> <span className={styles.permissionLabel}>{t('Lesen')}</span>
<AccessLevelSelect <AccessLevelSelect
value={rule.read} value={rule.read}
onChange={(value) => onUpdate(rule.id, { read: value })} onChange={(value) => onUpdate(rule.id, { read: value })}
@ -122,7 +122,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
/> />
</div> </div>
<div className={styles.permissionItem}> <div className={styles.permissionItem}>
<span className={styles.permissionLabel}>Create</span> <span className={styles.permissionLabel}>{t('Erstellen')}</span>
<AccessLevelSelect <AccessLevelSelect
value={rule.create} value={rule.create}
onChange={(value) => onUpdate(rule.id, { create: value })} onChange={(value) => onUpdate(rule.id, { create: value })}
@ -131,7 +131,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
/> />
</div> </div>
<div className={styles.permissionItem}> <div className={styles.permissionItem}>
<span className={styles.permissionLabel}>Update</span> <span className={styles.permissionLabel}>{t('Bearbeiten')}</span>
<AccessLevelSelect <AccessLevelSelect
value={rule.update} value={rule.update}
onChange={(value) => onUpdate(rule.id, { update: value })} onChange={(value) => onUpdate(rule.id, { update: value })}
@ -140,7 +140,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
/> />
</div> </div>
<div className={styles.permissionItem}> <div className={styles.permissionItem}>
<span className={styles.permissionLabel}>{t('delete')}</span> <span className={styles.permissionLabel}>{t('Löschen')}</span>
<AccessLevelSelect <AccessLevelSelect
value={rule.delete} value={rule.delete}
onChange={(value) => onUpdate(rule.id, { delete: value })} onChange={(value) => onUpdate(rule.id, { delete: value })}
@ -214,7 +214,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
}; };
const getLabel = (obj: CatalogObject): string => { const getLabel = (obj: CatalogObject): string => {
return obj.label.de || obj.label.en || obj.objectKey; return (typeof obj.label === 'string' ? obj.label : '') || obj.objectKey;
}; };
return ( return (
@ -260,7 +260,9 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
)} )}
<span className={styles.formHint}> <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> </span>
</div> </div>
@ -272,7 +274,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
onChange={(e) => setView(e.target.checked)} onChange={(e) => setView(e.target.checked)}
style={{ marginRight: '0.5rem' }} style={{ marginRight: '0.5rem' }}
/> />
Sichtbar (View) {t('Sichtbar (Ansicht)')}
</label> </label>
</div> </div>
@ -290,7 +292,12 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
{(['create', 'read', 'update', 'delete'] as const).map(op => { {(['create', 'read', 'update', 'delete'] as const).map(op => {
const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read; 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 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 ( return (
<div key={op} className={styles.matrixRow}> <div key={op} className={styles.matrixRow}>
@ -310,7 +317,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
setValue(hierarchy[idx - 1] || 'n'); 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> </div>
))} ))}
@ -322,10 +329,10 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
<div className={styles.formActions}> <div className={styles.formActions}>
<button type="button" className={styles.secondaryButton} onClick={onCancel}> <button type="button" className={styles.secondaryButton} onClick={onCancel}>
Abbrechen {t('Abbrechen')}
</button> </button>
<button type="submit" className={styles.primaryButton}> <button type="submit" className={styles.primaryButton}>
<FaPlus /> Hinzufügen <FaPlus /> {t('Hinzufügen')}
</button> </button>
</div> </div>
</form> </form>
@ -355,6 +362,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
onDelete, onDelete,
onAdd, onAdd,
}) => { }) => {
const { t } = useLanguage();
const [showAddForm, setShowAddForm] = useState(false); const [showAddForm, setShowAddForm] = useState(false);
const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA
@ -373,9 +381,12 @@ const RulesSection: React.FC<RulesSectionProps> = ({
const getEmptyText = () => { const getEmptyText = () => {
switch (context) { switch (context) {
case 'DATA': return 'Keine Daten-Regeln definiert'; case 'DATA':
case 'UI': return 'Keine UI-Regeln definiert'; return t('Keine Daten-Regeln definiert');
case 'RESOURCE': return 'Keine Ressourcen-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 && ( {!readOnly && !showAddForm && (
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<span className={styles.sectionTitle}> <span className={styles.sectionTitle}>
{rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'} {rules.length} {rules.length === 1 ? t('Regel') : t('Regeln')}
</span> </span>
<div className={styles.headerActions}> <div className={styles.headerActions}>
{/* View Toggle */} {/* View Toggle */}
@ -393,14 +404,14 @@ const RulesSection: React.FC<RulesSectionProps> = ({
<button <button
className={`${styles.viewToggleButton} ${useTableView ? styles.active : ''}`} className={`${styles.viewToggleButton} ${useTableView ? styles.active : ''}`}
onClick={() => setUseTableView(true)} onClick={() => setUseTableView(true)}
title="Tabellenansicht" title={t('Tabellenansicht')}
> >
<FaThList /> <FaThList />
</button> </button>
<button <button
className={`${styles.viewToggleButton} ${!useTableView ? styles.active : ''}`} className={`${styles.viewToggleButton} ${!useTableView ? styles.active : ''}`}
onClick={() => setUseTableView(false)} onClick={() => setUseTableView(false)}
title="Kartenansicht" title={t('Kartenansicht')}
> >
<FaTh /> <FaTh />
</button> </button>
@ -410,7 +421,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
className={styles.addButton} className={styles.addButton}
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
> >
<FaPlus /> Neue Regel <FaPlus /> {t('Neue Regel')}
</button> </button>
</div> </div>
</div> </div>
@ -431,7 +442,7 @@ const RulesSection: React.FC<RulesSectionProps> = ({
<p className={styles.emptyText}>{getEmptyText()}</p> <p className={styles.emptyText}>{getEmptyText()}</p>
{!readOnly && ( {!readOnly && (
<p className={styles.emptyHint}> <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> </p>
)} )}
</div> </div>
@ -469,6 +480,7 @@ interface JsonEditorProps {
} }
const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) => { const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) => {
const { t } = useLanguage();
const [jsonText, setJsonText] = useState(''); const [jsonText, setJsonText] = useState('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -481,7 +493,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
try { try {
const parsed = JSON.parse(jsonText); const parsed = JSON.parse(jsonText);
if (!Array.isArray(parsed)) { if (!Array.isArray(parsed)) {
throw new Error('JSON muss ein Array sein'); throw new Error(t('JSON muss ein Array sein'));
} }
setError(null); setError(null);
onApply(parsed); onApply(parsed);
@ -501,8 +513,9 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
/> />
{error && <div className={styles.jsonError}>{error}</div>} {error && <div className={styles.jsonError}>{error}</div>}
<p className={styles.jsonHint}> <p className={styles.jsonHint}>
Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON. {t(
Änderungen werden erst nach Klick auf "Anwenden" übernommen. 'Experten-Modus: Bearbeiten Sie die Regeln direkt als JSON. Änderungen werden erst nach Klick auf „Anwenden“ übernommen.'
)}
</p> </p>
{!readOnly && ( {!readOnly && (
<div className={styles.formActions}> <div className={styles.formActions}>
@ -512,7 +525,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({ rules, readOnly, onApply }) =>
onClick={handleApply} onClick={handleApply}
disabled={!!error} disabled={!!error}
> >
JSON anwenden {t('JSON anwenden')}
</button> </button>
</div> </div>
)} )}
@ -607,7 +620,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
setHasChanges(false); setHasChanges(false);
onSave?.(); onSave?.();
} else { } 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}> <div className={styles.editorHeader}>
<h3 className={styles.editorTitle}> <h3 className={styles.editorTitle}>
Berechtigungen{roleName ? `: ${roleName}` : ''} Berechtigungen{roleName ? `: ${roleName}` : ''}
{isTemplate && <span className={styles.templateBadge}>Template</span>} {isTemplate && <span className={styles.templateBadge}>{t('Vorlage')}</span>}
</h3> </h3>
{!readOnly && hasChanges && ( {!readOnly && hasChanges && (
<div className={styles.headerActions}> <div className={styles.headerActions}>

View file

@ -77,6 +77,8 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
onDelete, onDelete,
}) => { }) => {
const { t } = useLanguage(); 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 = ( const handleLevelToggle = (
field: 'read' | 'create' | 'update' | 'delete', field: 'read' | 'create' | 'update' | 'delete',
targetLevel: 'm' | 'g' | 'a', targetLevel: 'm' | 'g' | 'a',
@ -112,7 +114,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
checked={rule.view} checked={rule.view}
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })} onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
disabled={readOnly} disabled={readOnly}
title="Sichtbar" title={t('Sichtbar')}
/> />
</td> </td>
@ -127,7 +129,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
checked={hasLevel(rule[op] as AccessLevel, 'm')} checked={hasLevel(rule[op] as AccessLevel, 'm')}
onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)} onChange={(e) => handleLevelToggle(op, 'm', e.target.checked)}
disabled={readOnly} disabled={readOnly}
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Eigene`} title={`${opTitle(op)} - ${t('Eigene')}`}
/> />
</td> </td>
))} ))}
@ -140,7 +142,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
checked={hasLevel(rule[op] as AccessLevel, 'g')} checked={hasLevel(rule[op] as AccessLevel, 'g')}
onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)} onChange={(e) => handleLevelToggle(op, 'g', e.target.checked)}
disabled={readOnly} disabled={readOnly}
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Gruppe`} title={`${opTitle(op)} - ${t('Gruppe')}`}
/> />
</td> </td>
))} ))}
@ -153,7 +155,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
checked={hasLevel(rule[op] as AccessLevel, 'a')} checked={hasLevel(rule[op] as AccessLevel, 'a')}
onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)} onChange={(e) => handleLevelToggle(op, 'a', e.target.checked)}
disabled={readOnly} disabled={readOnly}
title={`${op.charAt(0).toUpperCase() + op.slice(1)} - Alle`} title={`${opTitle(op)} - ${t('Alle')}`}
/> />
</td> </td>
))} ))}
@ -166,7 +168,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
<button <button
className={`${styles.iconButton} ${styles.danger}`} className={`${styles.iconButton} ${styles.danger}`}
onClick={() => onDelete(rule.id)} onClick={() => onDelete(rule.id)}
title={t('delete rule')} title={t('Regel löschen')}
> >
<FaTrash /> <FaTrash />
</button> </button>
@ -200,7 +202,7 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
<thead> <thead>
<tr> <tr>
<th className={styles.colObject}>{t('object dot notation')}</th> <th className={styles.colObject}>{t('object dot notation')}</th>
<th className={styles.colView}>View</th> <th className={styles.colView}>{t('Ansicht')}</th>
{isDataContext && ( {isDataContext && (
<> <>
<th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th> <th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th>
@ -214,18 +216,18 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
<tr className={styles.subHeader}> <tr className={styles.subHeader}>
<th></th> <th></th>
<th></th> <th></th>
<th title="Create">C</th> <th title={t('Erstellen')}>C</th>
<th title="Read">R</th> <th title={t('Lesen')}>R</th>
<th title="Update">U</th> <th title={t('Bearbeiten')}>U</th>
<th title={t('delete')}>D</th> <th title={t('Löschen')}>D</th>
<th title="Create">C</th> <th title={t('Erstellen')}>C</th>
<th title="Read">R</th> <th title={t('Lesen')}>R</th>
<th title="Update">U</th> <th title={t('Bearbeiten')}>U</th>
<th title={t('delete')}>D</th> <th title={t('Löschen')}>D</th>
<th title="Create">C</th> <th title={t('Erstellen')}>C</th>
<th title="Read">R</th> <th title={t('Lesen')}>R</th>
<th title="Update">U</th> <th title={t('Bearbeiten')}>U</th>
<th title={t('delete')}>D</th> <th title={t('Löschen')}>D</th>
<th></th> <th></th>
</tr> </tr>
)} )}

View file

@ -4,6 +4,7 @@
* Simple text input with send button, usable by both Workspace and Editor. * Simple text input with send button, usable by both Workspace and Editor.
*/ */
import React, { useState, useCallback, useRef, useEffect } from 'react'; import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useLanguage } from '../../providers/language/LanguageContext';
interface ChatInputProps { interface ChatInputProps {
onSend: (message: string) => void; onSend: (message: string) => void;
@ -17,11 +18,13 @@ interface ChatInputProps {
export const ChatInput: React.FC<ChatInputProps> = ({ export const ChatInput: React.FC<ChatInputProps> = ({
onSend, onSend,
isProcessing, isProcessing,
placeholder = 'Type a message...', placeholder,
disabled, disabled,
autoFocus = true, autoFocus = true,
style, style,
}) => { }) => {
const { t } = useLanguage();
const resolvedPlaceholder = placeholder ?? t('Nachricht eingeben…');
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
@ -62,7 +65,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
onKeyDown={_handleKeyDown} onKeyDown={_handleKeyDown}
placeholder={placeholder} placeholder={resolvedPlaceholder}
disabled={isProcessing || disabled} disabled={isProcessing || disabled}
rows={1} rows={1}
style={{ style={{
@ -95,7 +98,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{isProcessing ? '...' : 'Send'} {isProcessing ? '…' : t('Senden')}
</button> </button>
</div> </div>
); );

View file

@ -7,6 +7,7 @@
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { useLanguage } from '../../providers/language/LanguageContext';
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
@ -31,9 +32,11 @@ const _roleColors: Record<string, string> = {
export const ChatMessageList: React.FC<ChatMessageListProps> = ({ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
messages, messages,
isProcessing, isProcessing,
emptyMessage = 'No messages yet.', emptyMessage,
style, style,
}) => { }) => {
const { t } = useLanguage();
const resolvedEmpty = emptyMessage ?? t('Noch keine Nachrichten.');
const bottomRef = useRef<HTMLDivElement>(null); const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@ -55,7 +58,7 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
> >
{messages.length === 0 && ( {messages.length === 0 && (
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px', textAlign: 'center', marginTop: '24px' }}> <div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px', textAlign: 'center', marginTop: '24px' }}>
{emptyMessage} {resolvedEmpty}
</div> </div>
)} )}
{messages.map((msg) => ( {messages.map((msg) => (
@ -80,7 +83,7 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({
))} ))}
{isProcessing && ( {isProcessing && (
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '12px', fontStyle: 'italic' }}> <div style={{ color: 'var(--text-secondary, #888)', fontSize: '12px', fontStyle: 'italic' }}>
Processing... {t('Wird verarbeitet…')}
</div> </div>
)} )}
<div ref={bottomRef} /> <div ref={bottomRef} />

View file

@ -64,7 +64,7 @@ export function ContentPreview({
setError(t('Ungültige Datei-ID')); setError(t('Ungültige Datei-ID'));
return; return;
} }
if (!fileName || fileName === 'Unknown Item') { if (!fileName || fileName === 'Unknown Item' || fileName === 'Unbekanntes Element') {
setError(t('Dateiname nicht verfügbar')); setError(t('Dateiname nicht verfügbar'));
return; return;
} }
@ -77,7 +77,7 @@ export function ContentPreview({
} }
setError(null); setError(null);
} }
}, [isOpen, fileId, fileName]); }, [isOpen, fileId, fileName, t]);
const loadPreview = async () => { 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 // If it's text content but MIME type says PDF, we'll handle it in renderPreview
} else { } else {
setError(result.error || 'Failed to load preview'); setError(result.error || t('Vorschau konnte nicht geladen werden.'));
} }
} catch (err) { } catch (err) {
setError(t('Ein unerwarteter Fehler ist aufgetreten, während')); setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
@ -201,7 +201,7 @@ export function ContentPreview({
</div> </div>
<pre className={styles.jsonPreview}> <pre className={styles.jsonPreview}>
<code className={styles.jsonCode}> <code className={styles.jsonCode}>
{previewContent || 'No content available'} {previewContent || t('Kein Inhalt verfügbar')}
</code> </code>
</pre> </pre>
</div> </div>

View file

@ -96,7 +96,9 @@ export function UrlContentPreview({
const warningTimeout = setTimeout(() => { const warningTimeout = setTimeout(() => {
if (isLoading && !hasLoaded) { 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 // Don't set isLoading to false - let it continue
} }
}, WARNING_TIMEOUT); }, WARNING_TIMEOUT);
@ -107,7 +109,7 @@ export function UrlContentPreview({
console.log('PDF loading timeout, switching to PDF.js fallback'); console.log('PDF loading timeout, switching to PDF.js fallback');
setUsePdfJs(true); setUsePdfJs(true);
setIsLoading(true); // Restart loading with PDF.js 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) { } else if (isLoading && !hasLoaded && usePdfJs) {
// PDF.js also failed, show error // PDF.js also failed, show error
setShowPdfAnyway(true); setShowPdfAnyway(true);
@ -121,7 +123,7 @@ export function UrlContentPreview({
clearTimeout(errorTimeout); clearTimeout(errorTimeout);
}; };
} }
}, [isOpen, isLoading, hasLoaded, usePdfJs]); }, [isOpen, isLoading, hasLoaded, usePdfJs, t]);
// Validate URL // Validate URL
useEffect(() => { useEffect(() => {
@ -184,7 +186,7 @@ export function UrlContentPreview({
padding: '0.5rem 1rem' padding: '0.5rem 1rem'
}} }}
> >
In neuem Tab öffnen {t('In neuem Tab öffnen')}
</button> </button>
<button <button
onClick={handleDownload} onClick={handleDownload}
@ -195,7 +197,7 @@ export function UrlContentPreview({
padding: '0.5rem 1rem' padding: '0.5rem 1rem'
}} }}
> >
Download {t('Herunterladen')}
</button> </button>
</div> </div>
</div> </div>
@ -241,7 +243,7 @@ export function UrlContentPreview({
fontWeight: '500' fontWeight: '500'
}} }}
> >
In neuem Tab öffnen {t('In neuem Tab öffnen')}
</button> </button>
<button <button
onClick={handleDownload} onClick={handleDownload}
@ -253,7 +255,7 @@ export function UrlContentPreview({
fontWeight: '500' fontWeight: '500'
}} }}
> >
Download File {t('Datei herunterladen')}
</button> </button>
</div> </div>
</div> </div>
@ -284,7 +286,7 @@ export function UrlContentPreview({
fontWeight: '500' fontWeight: '500'
}} }}
> >
In neuem Tab öffnen {t('In neuem Tab öffnen')}
</button> </button>
<button <button
onClick={handleDownload} onClick={handleDownload}
@ -296,7 +298,7 @@ export function UrlContentPreview({
fontWeight: '500' fontWeight: '500'
}} }}
> >
Download {t('Herunterladen')}
</button> </button>
</div> </div>
</div> </div>
@ -316,7 +318,7 @@ export function UrlContentPreview({
<div className={styles.fileName}>{fileName}</div> <div className={styles.fileName}>{fileName}</div>
<p>{t('Vorschau wird hierfür nicht unterstützt')}</p> <p>{t('Vorschau wird hierfür nicht unterstützt')}</p>
<button onClick={handleDownload} className={styles.retryButton}> <button onClick={handleDownload} className={styles.retryButton}>
Download File {t('Datei herunterladen')}
</button> </button>
</div> </div>
); );

View file

@ -303,7 +303,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
<button <button
className={styles.collapseButton} className={styles.collapseButton}
onClick={() => toggleCollapse(rowPath)} onClick={() => toggleCollapse(rowPath)}
title={isCollapsed ? 'Expand' : 'Collapse'} title={isCollapsed ? t('Aufklappen') : t('Einklappen')}
> >
{isCollapsed ? '▶' : '▼'} {isCollapsed ? '▶' : '▼'}
</button> </button>
@ -479,7 +479,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
); );
} catch (parseError) { } catch (parseError) {
const rawData = { const rawData = {
keys: ['Raw Content'], keys: [t('Rohinhalt')],
values: [previewContent], values: [previewContent],
types: ['string'], types: ['string'],
isNested: [false] isNested: [false]

View file

@ -66,7 +66,7 @@ export function PdfJsRenderer({
} catch (err) { } catch (err) {
console.error('Error loading PDF with PDF.js:', err); console.error('Error loading PDF with PDF.js:', err);
if (isMounted) { 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); setIsLoading(false);
onError(); onError();
} }
@ -116,7 +116,7 @@ export function PdfJsRenderer({
} catch (err) { } catch (err) {
console.error('Error rendering PDF page:', err); console.error('Error rendering PDF page:', err);
if (isMounted) { 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 ( return (
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<div className={styles.errorIcon}></div> <div className={styles.errorIcon}></div>
<p>Fehler beim Laden der PDF: {error}</p> <p>
{t('Fehler beim Laden der PDF:')} {error}
</p>
</div> </div>
); );
} }

View file

@ -57,8 +57,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
const LOG = '[Automation2]'; const LOG = '[Automation2]';
const DEFAULT_INVOCATIONS = (): WorkflowEntryPoint[] => const _buildDefaultInvocations = (runLabel: string): WorkflowEntryPoint[] =>
buildInvocationsForPrimaryKind('manual', [], 'Jetzt ausführen'); buildInvocationsForPrimaryKind('manual', [], runLabel);
interface Automation2FlowEditorProps { interface Automation2FlowEditorProps {
instanceId: string; instanceId: string;
@ -106,7 +106,9 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null); const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null); const [selectedNode, setSelectedNode] = useState<CanvasNode | null>(null);
const [saving, setSaving] = useState(false); 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 [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
const [leftPanelOpen, setLeftPanelOpen] = useState(true); const [leftPanelOpen, setLeftPanelOpen] = useState(true);
const [tracingRunId, setTracingRunId] = useState<string | null>(null); const [tracingRunId, setTracingRunId] = useState<string | null>(null);
@ -176,7 +178,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
const applyGraphWithSync = useCallback( const applyGraphWithSync = useCallback(
(graph: Automation2Graph | null | undefined, wfInvocations: WorkflowEntryPoint[] | undefined) => { (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); setInvocations(inv);
if (!graph?.nodes?.length) { if (!graph?.nodes?.length) {
const synced = syncCanvasStartNode([], [], inv, nodeTypes, language); const synced = syncCanvasStartNode([], [], inv, nodeTypes, language);
@ -189,7 +191,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
setCanvasNodes(synced.nodes); setCanvasNodes(synced.nodes);
setCanvasConnections(synced.connections); setCanvasConnections(synced.connections);
}, },
[nodeTypes, language] [nodeTypes, language, t]
); );
const handleFromApiGraph = useCallback( const handleFromApiGraph = useCallback(
@ -202,7 +204,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
const handleExecute = useCallback(async () => { const handleExecute = useCallback(async () => {
const graph = toApiGraph(canvasNodes, canvasConnections); const graph = toApiGraph(canvasNodes, canvasConnections);
if (graph.nodes.length === 0) { if (graph.nodes.length === 0) {
setExecuteResult({ success: false, error: 'Keine Nodes im Workflow.' }); setExecuteResult({ success: false, error: t('Keine Nodes im Workflow.') });
return; return;
} }
setExecuting(true); setExecuting(true);
@ -222,12 +224,12 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
} finally { } finally {
setExecuting(false); setExecuting(false);
} }
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations]); }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations, t]);
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
const graph = toApiGraph(canvasNodes, canvasConnections); const graph = toApiGraph(canvasNodes, canvasConnections);
if (graph.nodes.length === 0) { if (graph.nodes.length === 0) {
setExecuteResult({ success: false, error: 'Keine Nodes zum Speichern.' }); setExecuteResult({ success: false, error: t('Keine Nodes zum Speichern.') });
return; return;
} }
setSaving(true); setSaving(true);
@ -236,17 +238,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations }); await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations });
setExecuteResult({ success: true } as ExecuteGraphResponse); setExecuteResult({ success: true } as ExecuteGraphResponse);
} else { } else {
const label = await promptInput('Workflow-Name:', { const label = await promptInput(t('Workflow-Name:'), {
title: t('Workflow speichern'), title: t('Workflow speichern'),
defaultValue: 'Neuer Workflow', defaultValue: t('Neuer Workflow'),
placeholder: 'Name des Workflows', placeholder: t('Name des Workflows'),
}); });
if (!label) { if (!label) {
setSaving(false); setSaving(false);
return; return;
} }
const created = await createWorkflow(request, instanceId, { const created = await createWorkflow(request, instanceId, {
label: label.trim() || 'Neuer Workflow', label: label.trim() || t('Neuer Workflow'),
graph, graph,
invocations, invocations,
}); });
@ -260,7 +262,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations]); }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t]);
const handleLoad = useCallback( const handleLoad = useCallback(
async (workflowId: string) => { async (workflowId: string) => {
@ -287,17 +289,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
if (workflowId) handleLoad(workflowId); if (workflowId) handleLoad(workflowId);
else { else {
setExecuteResult(null); setExecuteResult(null);
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS()); applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
} }
}, },
[handleLoad, applyGraphWithSync] [handleLoad, applyGraphWithSync, t]
); );
const handleNew = useCallback(() => { const handleNew = useCallback(() => {
setCurrentWorkflowId(null); setCurrentWorkflowId(null);
setExecuteResult(null); setExecuteResult(null);
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS()); applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
}, [applyGraphWithSync]); }, [applyGraphWithSync, t]);
const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record<string, unknown>) => { const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record<string, unknown>) => {
setCanvasNodes((prev) => setCanvasNodes((prev) =>
@ -401,7 +403,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
if (loading || nodeTypes.length === 0) return; if (loading || nodeTypes.length === 0) return;
if (currentWorkflowId || initialWorkflowId) return; if (currentWorkflowId || initialWorkflowId) return;
if (canvasNodes.length > 0) return; if (canvasNodes.length > 0) return;
applyGraphWithSync({ nodes: [], connections: [] }, DEFAULT_INVOCATIONS()); applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen')));
}, [ }, [
loading, loading,
nodeTypes.length, nodeTypes.length,
@ -409,6 +411,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
initialWorkflowId, initialWorkflowId,
canvasNodes.length, canvasNodes.length,
applyGraphWithSync, applyGraphWithSync,
t,
]); ]);
const toggleCategory = useCallback((id: string) => { const toggleCategory = useCallback((id: string) => {
@ -591,7 +594,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
return ( return (
<div className={styles.sidebar} style={_sidebarStyle}> <div className={styles.sidebar} style={_sidebarStyle}>
<div className={styles.sidebarHeader}> <div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3> <h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
</div> </div>
<div className={styles.loading}> <div className={styles.loading}>
<FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} /> <FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} />
@ -604,12 +607,12 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
return ( return (
<div className={styles.sidebar} style={_sidebarStyle}> <div className={styles.sidebar} style={_sidebarStyle}>
<div className={styles.sidebarHeader}> <div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3> <h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
</div> </div>
<div className={styles.error}> <div className={styles.error}>
<p>{error}</p> <p>{error}</p>
<button className={styles.retryButton} onClick={loadNodeTypes}> <button className={styles.retryButton} onClick={loadNodeTypes}>
Erneut versuchen {t('Erneut versuchen')}
</button> </button>
</div> </div>
</div> </div>
@ -648,7 +651,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`} className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`}
onClick={() => setUdbTab(tab)} onClick={() => setUdbTab(tab)}
> >
{{ chats: 'Chats', files: 'Dateien', sources: 'Quellen' }[tab]} {{ chats: t('Chats'), files: t('Dateien'), sources: t('Quellen') }[tab]}
</button> </button>
))} ))}
</div> </div>
@ -755,13 +758,13 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`} className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
onClick={() => setRightTab('nodes')} onClick={() => setRightTab('nodes')}
> >
Nodes {t('Knoten')}
</button> </button>
<button <button
className={`${styles.rightTab} ${rightTab === 'tracing' ? styles.rightTabActive : ''}`} className={`${styles.rightTab} ${rightTab === 'tracing' ? styles.rightTabActive : ''}`}
onClick={() => { setRightTab('tracing'); if (!tracingRunId) setTracingRunId('select'); }} onClick={() => { setRightTab('tracing'); if (!tracingRunId) setTracingRunId('select'); }}
> >
Tracing {t('Ablaufverfolgung')}
</button> </button>
</div> </div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>

View file

@ -2,7 +2,7 @@
* CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result. * 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 { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa';
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi'; import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
import styles from './Automation2FlowEditor.module.css'; import styles from './Automation2FlowEditor.module.css';
@ -118,7 +118,15 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
return () => document.removeEventListener('mousedown', _handleClickOutside); 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 ( return (
<div className={styles.canvasHeader}> <div className={styles.canvasHeader}>
@ -139,14 +147,14 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.canvasTitle} className={styles.canvasTitle}
style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }} style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }}
onClick={_startNameEdit} onClick={_startNameEdit}
title={onWorkflowRename ? 'Klicken zum Umbenennen' : undefined} title={onWorkflowRename ? t('Klicken zum Umbenennen') : undefined}
> >
{currentWorkflow.label} {currentWorkflow.label}
</h4> </h4>
) )
) : ( ) : (
<h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}> <h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}>
Neuer Workflow {t('Neuer Workflow')}
</h4> </h4>
)} )}
{onWorkflowSettings && ( {onWorkflowSettings && (
@ -154,7 +162,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
type="button" type="button"
className={styles.canvasGearBtn} className={styles.canvasGearBtn}
title={t('Workflowkonfiguration Einstieg/Starts')} title={t('Workflowkonfiguration Einstieg/Starts')}
aria-label="Workflow-Konfiguration" aria-label={t('Workflow-Konfiguration')}
onClick={onWorkflowSettings} onClick={onWorkflowSettings}
> >
<FaCog /> <FaCog />
@ -165,7 +173,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
<div ref={newMenuRef} style={{ position: 'relative', display: 'inline-block' }}> <div ref={newMenuRef} style={{ position: 'relative', display: 'inline-block' }}>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<button type="button" className={styles.retryButton} onClick={onNew} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}> <button type="button" className={styles.retryButton} onClick={onNew} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}>
Neu {t('Neu')}
</button> </button>
<button <button
type="button" type="button"
@ -184,7 +192,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
onClick={() => { onNew(); setNewMenuOpen(false); }} onClick={() => { onNew(); setNewMenuOpen(false); }}
style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '0.85rem' }} 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> </button>
{onNewFromTemplate && ( {onNewFromTemplate && (
<button <button
@ -192,7 +200,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
onClick={() => { onNewFromTemplate(); setNewMenuOpen(false); }} 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)' }} 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> </button>
)} )}
</div> </div>
@ -205,7 +213,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
onClick={onSave} onClick={onSave}
disabled={saving || !hasNodes} disabled={saving || !hasNodes}
> >
{saving ? <FaSpinner className={styles.spinner} /> : 'Speichern'} {saving ? <FaSpinner className={styles.spinner} /> : t('Speichern')}
</button> </button>
{/* Save as template */} {/* Save as template */}
@ -229,7 +237,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
onClick={() => { onSaveAsTemplate(s); setTemplateMenuOpen(false); }} 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 }} 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> </button>
))} ))}
</div> </div>
@ -260,19 +268,19 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
{executing ? ( {executing ? (
<> <>
<FaSpinner className={styles.spinner} style={{ marginRight: '0.5rem', display: 'inline-block' }} /> <FaSpinner className={styles.spinner} style={{ marginRight: '0.5rem', display: 'inline-block' }} />
Ausführen {t('Ausführen…')}
</> </>
) : ( ) : (
<> <>
<FaPlay style={{ marginRight: '0.5rem' }} /> <FaPlay style={{ marginRight: '0.5rem' }} />
Ausführen {t('Ausführen')}
</> </>
)} )}
</button> </button>
{onToggleChat && ( {onToggleChat && (
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('Workspace-Panel: Chats, Dateien, Quellen')}> <button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('Workspace-Panel: Chats, Dateien, Quellen')}>
<FaDatabase style={{ marginRight: '0.4rem' }} /> <FaDatabase style={{ marginRight: '0.4rem' }} />
Workspace {t('Workspace')}
</button> </button>
)} )}
</div> </div>
@ -280,7 +288,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
{/* Version Selector */} {/* Version Selector */}
{currentWorkflowId && versions && versions.length > 0 && ( {currentWorkflowId && versions && versions.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.5rem', flexWrap: 'wrap' }}> <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 <select
value={currentVersionId ?? ''} value={currentVersionId ?? ''}
onChange={(e) => onVersionSelect?.(e.target.value || null)} 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' }} style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
> >
<FaCloudUploadAlt style={{ marginRight: 4 }} /> <FaCloudUploadAlt style={{ marginRight: 4 }} />
Publish {t('Veröffentlichen')}
</button> </button>
)} )}
{currentVersion && currentStatus === 'published' && onUnpublishVersion && ( {currentVersion && currentStatus === 'published' && onUnpublishVersion && (
@ -329,7 +337,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }} style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
> >
<FaCloudDownloadAlt style={{ marginRight: 4 }} /> <FaCloudDownloadAlt style={{ marginRight: 4 }} />
Unpublish {t('Veröffentlichung aufheben')}
</button> </button>
)} )}
{currentVersion && currentStatus !== 'archived' && onArchiveVersion && ( {currentVersion && currentStatus !== 'archived' && onArchiveVersion && (
@ -388,7 +396,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
Task zu bearbeiten. Task zu bearbeiten.
</> </>
) : ( ) : (
<> {executeResult.error ?? 'Unbekannter Fehler'}</> <> {executeResult.error ?? t('Unbekannter Fehler')}</>
)} )}
</div> </div>
)} )}

View file

@ -130,29 +130,29 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
}, },
onComplete: () => { onComplete: () => {
if (!accumulated) { 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?.(); onGraphUpdated?.();
setLoading(false); setLoading(false);
}, },
onError: (event) => { onError: (event) => {
const errText = event.content || 'Request failed'; const errText = event.content || t('Anfrage fehlgeschlagen');
if (!accumulated) { 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); setLoading(false);
}, },
onStopped: () => setLoading(false), onStopped: () => setLoading(false),
}, },
onConnectionError: (err) => { 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); setLoading(false);
}, },
onStreamEnd: () => setLoading(false), onStreamEnd: () => setLoading(false),
}); });
abortRef.current = cleanup; 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) => { const _handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
@ -325,7 +325,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
minWidth: 220, maxHeight: 260, overflowY: 'auto', minWidth: 220, maxHeight: 260, overflowY: 'auto',
}}> }}>
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}> <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> </div>
{dataSources.map(ds => { {dataSources.map(ds => {
const isSelected = attachedDataSourceIds.includes(ds.id); const isSelected = attachedDataSourceIds.includes(ds.id);
@ -354,7 +354,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
{featureDataSources.length > 0 && ( {featureDataSources.length > 0 && (
<> <>
<div style={{ padding: '8px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderTop: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0' }}> <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> </div>
{featureDataSources.map(fds => { {featureDataSources.map(fds => {
const isSelected = attachedFeatureDataSourceIds.includes(fds.id); const isSelected = attachedFeatureDataSourceIds.includes(fds.id);
@ -394,7 +394,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
<button onClick={() => abortRef.current?.()} style={{ <button onClick={() => abortRef.current?.()} style={{
padding: '8px 14px', borderRadius: 8, border: 'none', padding: '8px 14px', borderRadius: 8, border: 'none',
background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600, fontSize: 12, background: '#f44336', color: '#fff', cursor: 'pointer', fontWeight: 600, fontSize: 12,
}}>Stop</button> }}>{t('Stopp')}</button>
) : ( ) : (
<button onClick={_handleSend} disabled={!prompt.trim() || !workflowId} style={{ <button onClick={_handleSend} disabled={!prompt.trim() || !workflowId} style={{
padding: '8px 14px', borderRadius: 8, border: 'none', padding: '8px 14px', borderRadius: 8, border: 'none',

View file

@ -560,17 +560,30 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
> >
{selectedNodeIds.size > 1 && !selectedConnectionId && !connectingFrom && ( {selectedNodeIds.size > 1 && !selectedConnectionId && !connectingFrom && (
<div className={styles.connectionHint}> <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> </div>
)} )}
{connectingFrom && !selectedConnectionId && ( {connectingFrom && !selectedConnectionId && (
<div className={styles.connectionHint}> <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> </div>
)} )}
{selectedConnectionId && ( {selectedConnectionId && (
<div className={styles.connectionHint}> <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>
)} )}
<div <div
@ -840,7 +853,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
)} )}
{nodes.length === 0 && ( {nodes.length === 0 && (
<div className={styles.canvasPlaceholder}> <div className={styles.canvasPlaceholder}>
<p>{t('Nodes aus der Liste links')}</p> <p>{t('Knoten aus der Liste links ziehen')}</p>
</div> </div>
)} )}
</div> </div>

View file

@ -91,7 +91,7 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
placeholder={t('z.B. Kundenformular prüfen, Land')} placeholder={t('z.B. Kundenformular prüfen, Land')}
/> />
<p className={styles.nodeConfigNameHint}> <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> </p>
</div> </div>
)} )}

View file

@ -84,7 +84,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({ nodeTypes,
return ( return (
<div className={styles.sidebar} style={style}> <div className={styles.sidebar} style={style}>
<div className={styles.sidebarHeader}> <div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3> <h3 className={styles.sidebarTitle}>{t('Knoten')}</h3>
<input <input
type="text" type="text"
className={styles.sidebarSearch} className={styles.sidebarSearch}

View file

@ -172,7 +172,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
if (!runId) { if (!runId) {
return ( return (
<div style={{ padding: '16px', color: 'var(--text-secondary, #888)', fontSize: '13px' }}> <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> </div>
); );
} }
@ -180,7 +180,10 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
return ( return (
<div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}> <div style={{ padding: '12px', overflowY: 'auto', height: '100%' }}>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '12px' }}> <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> </div>
{steps.length === 0 && !loading && ( {steps.length === 0 && !loading && (
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('Noch keine Schritte aufgezeichnet')}</div> <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' }}> <span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{step.retryCount > 0 && ( {step.retryCount > 0 && (
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}> <span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}>
{step.retryCount}x retry {step.retryCount}x {t('Wiederholung')}
</span> </span>
)} )}
{step.durationMs != null && ( {step.durationMs != null && (
@ -244,11 +247,13 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div> <div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
)} )}
{step.tokensUsed > 0 && ( {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={t('Eingabe')} content={inputStr} />
<CollapsibleSection label="Output" content={outputStr} /> <CollapsibleSection label={t('Ausgabe')} content={outputStr} />
</div> </div>
); );
})} })}

View file

@ -2,7 +2,7 @@
* TemplatePicker - modal to browse and select a workflow template for creating a new workflow. * 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 { FaSpinner } from 'react-icons/fa';
import { import {
fetchTemplates, fetchTemplates,
@ -11,14 +11,7 @@ import {
type ApiRequestFunction, type ApiRequestFunction,
} from '../../../api/workflowApi'; } from '../../../api/workflowApi';
import styles from './Automation2FlowEditor.module.css'; import styles from './Automation2FlowEditor.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
const SCOPE_LABELS: Record<AutoTemplateScope | 'all', string> = {
all: 'Alle',
user: 'Meine',
instance: 'Instanz',
mandate: 'Mandant',
system: 'System',
};
interface TemplatePickerProps { interface TemplatePickerProps {
open: boolean; open: boolean;
@ -35,6 +28,18 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
instanceId, instanceId,
request, 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 [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [activeScope, setActiveScope] = useState<AutoTemplateScope | 'all'>('all'); 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.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="tpl-picker-title">
<div className={styles.workflowModal} style={{ maxWidth: 600, maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}> <div className={styles.workflowModal} style={{ maxWidth: 600, maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}>
<h3 id="tpl-picker-title" className={styles.workflowModalTitle}> <h3 id="tpl-picker-title" className={styles.workflowModalTitle}>
Neu aus Vorlage {t('Neu aus Vorlage')}
</h3> </h3>
<p className={styles.workflowModalHint}> <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> </p>
<div style={{ display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 6, marginBottom: 12, flexWrap: 'wrap' }}>
@ -91,7 +96,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
onClick={() => setActiveScope(s)} onClick={() => setActiveScope(s)}
style={{ fontSize: '0.8rem', padding: '4px 10px' }} style={{ fontSize: '0.8rem', padding: '4px 10px' }}
> >
{SCOPE_LABELS[s]} {scopeLabels[s]}
</button> </button>
))} ))}
</div> </div>
@ -103,14 +108,14 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
</div> </div>
) : templates.length === 0 ? ( ) : templates.length === 0 ? (
<div style={{ textAlign: 'center', padding: 24, color: 'var(--text-secondary, #888)' }}> <div style={{ textAlign: 'center', padding: 24, color: 'var(--text-secondary, #888)' }}>
Keine Vorlagen gefunden. {t('Keine Vorlagen gefunden.')}
</div> </div>
) : ( ) : (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead> <thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #e0e0e0)', textAlign: 'left' }}> <tr style={{ borderBottom: '2px solid var(--border-color, #e0e0e0)', textAlign: 'left' }}>
<th style={{ padding: '6px 8px' }}>Name</th> <th style={{ padding: '6px 8px' }}>{t('Name')}</th>
<th style={{ padding: '6px 8px', width: 80 }}>Scope</th> <th style={{ padding: '6px 8px', width: 80 }}>{t('Scope')}</th>
<th style={{ padding: '6px 8px', width: 100 }}></th> <th style={{ padding: '6px 8px', width: 100 }}></th>
</tr> </tr>
</thead> </thead>
@ -119,7 +124,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
<tr key={tpl.id} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}> <tr key={tpl.id} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
<td style={{ padding: '8px' }}>{tpl.label}</td> <td style={{ padding: '8px' }}>{tpl.label}</td>
<td style={{ padding: '8px', fontSize: '0.8rem', color: 'var(--text-secondary, #888)' }}> <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>
<td style={{ padding: '8px', textAlign: 'right' }}> <td style={{ padding: '8px', textAlign: 'right' }}>
<button <button
@ -129,7 +134,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
onClick={() => _handleSelect(tpl.id)} onClick={() => _handleSelect(tpl.id)}
disabled={copying !== null} disabled={copying !== null}
> >
{copying === tpl.id ? <FaSpinner className={styles.spinner} /> : 'Übernehmen'} {copying === tpl.id ? <FaSpinner className={styles.spinner} /> : t('Übernehmen')}
</button> </button>
</td> </td>
</tr> </tr>
@ -141,7 +146,7 @@ export const TemplatePicker: React.FC<TemplatePickerProps> = ({
<div className={styles.workflowModalActions} style={{ marginTop: 12 }}> <div className={styles.workflowModalActions} style={{ marginTop: 12 }}>
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}> <button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
Abbrechen {t('Abbrechen')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -64,7 +64,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const label = 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); const next = buildInvocationsForPrimaryKind(kind, invocations, label);
onApply(next); onApply(next);
onClose(); 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.workflowModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="wf-cfg-title">
<div className={styles.workflowModal}> <div className={styles.workflowModal}>
<h3 id="wf-cfg-title" className={styles.workflowModalTitle}> <h3 id="wf-cfg-title" className={styles.workflowModalTitle}>
Workflow-Konfiguration {t('Workflow-Konfiguration')}
</h3> </h3>
<p className={styles.workflowModalHint}> <p className={styles.workflowModalHint}>
Legen Sie fest, wie dieser Workflow gestartet werden soll. Die Start-Node im Editor passt sich dem {t(
gewählten Einstieg an (z.&nbsp;B. Formular-Felder auf der Start-Node bearbeiten). '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> </p>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<label className={styles.workflowModalLabel} htmlFor="wf-start-title"> <label className={styles.workflowModalLabel} htmlFor="wf-start-title">
Titel der Start Node {t('Titel der Start Node')}
</label> </label>
<input <input
id="wf-start-title" id="wf-start-title"
@ -92,7 +93,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
placeholder={t('z.B. Angebot anlegen')} 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) => ( {kindOptions.map((o) => (
<label key={o.value} className={styles.workflowModalRadio}> <label key={o.value} className={styles.workflowModalRadio}>
<input <input
@ -109,10 +110,10 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
<div className={styles.workflowModalActions}> <div className={styles.workflowModalActions}>
<button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}> <button type="button" className={styles.workflowModalBtnSecondary} onClick={onClose}>
Abbrechen {t('Abbrechen')}
</button> </button>
<button type="submit" className={styles.workflowModalBtnPrimary}> <button type="submit" className={styles.workflowModalBtnPrimary}>
Übernehmen {t('Übernehmen')}
</button> </button>
</div> </div>
</form> </form>

View file

@ -57,7 +57,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
return ( return (
<div> <div>
<label>Felder</label> <label>{t('Felder')}</label>
<div className={styles.formFieldsList}> <div className={styles.formFieldsList}>
{fields.map((f, i) => ( {fields.map((f, i) => (
<div <div
@ -87,7 +87,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
</span> </span>
<div className={styles.formFieldInputs}> <div className={styles.formFieldInputs}>
<input <input
placeholder="name" placeholder={t('name')}
value={f.name ?? ''} value={f.name ?? ''}
onChange={(e) => { onChange={(e) => {
const next = [...fields]; const next = [...fields];
@ -96,7 +96,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
}} }}
/> />
<input <input
placeholder="label" placeholder={t('label')}
value={f.label ?? ''} value={f.label ?? ''}
onChange={(e) => { onChange={(e) => {
const next = [...fields]; const next = [...fields];
@ -111,13 +111,13 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
value={f.type ?? 'string'} value={f.type ?? 'string'}
onChange={(e) => { onChange={(e) => {
const next = [...fields]; const next = [...fields];
const t = e.target.value; const fieldType = e.target.value;
next[i] = { next[i] = {
...next[i], ...next[i],
type: t, type: fieldType,
...(t === 'clickup_tasks' ...(fieldType === 'clickup_tasks'
? { clickupStatusOptions: undefined } ? { clickupStatusOptions: undefined }
: t === 'clickup_status' : fieldType === 'clickup_status'
? { clickupConnectionId: undefined, clickupListId: undefined } ? { clickupConnectionId: undefined, clickupListId: undefined }
: { : {
clickupConnectionId: undefined, clickupConnectionId: undefined,
@ -129,10 +129,10 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
}} }}
style={{ width: 'auto', minWidth: 90 }} style={{ width: 'auto', minWidth: 90 }}
> >
<option value="string">Text</option> <option value="string">{t('Text')}</option>
<option value="number">Number</option> <option value="number">{t('Zahl')}</option>
<option value="date">Date</option> <option value="date">{t('Datum')}</option>
<option value="boolean">Checkbox</option> <option value="boolean">{t('Kontrollkästchen')}</option>
<option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option> <option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
<option value="clickup_status">{t('ClickUp-Status Liste')}</option> <option value="clickup_status">{t('ClickUp-Status Liste')}</option>
</select> </select>
@ -146,7 +146,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
updateParam('fields', next); updateParam('fields', next);
}} }}
/> />
Pflichtfeld {t('Pflichtfeld')}
</label> </label>
<button <button
type="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)' }}> <div style={{ marginTop: 8, paddingLeft: 4, width: '100%', fontSize: '0.8rem', color: 'var(--text-secondary, #666)' }}>
{Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? ( {Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? (
<p style={{ margin: '0 0 6px' }}> <p style={{ margin: '0 0 6px' }}>
Dropdown mit {f.clickupStatusOptions.length} Status aus der ClickUp-Liste (Wert = exakter {t(
Status-Name für die API). 'Dropdown mit {count} Status aus der ClickUp-Liste (Wert = exakter Status-Name für die API).',
{ count: String(f.clickupStatusOptions.length) }
)}
</p> </p>
) : ( ) : (
<p style={{ margin: '0 0 6px' }}> <p style={{ margin: '0 0 6px' }}>
Keine Optionen im ClickUp-Knoten Aufgabe erstellen Liste wählen und Formular mit Liste {t(
abgleichen. 'Keine Optionen — im ClickUp-Knoten „Aufgabe erstellen“ Liste wählen und „Formular mit Liste abgleichen“.'
)}
</p> </p>
)} )}
</div> </div>
@ -175,7 +178,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
{f.type === 'clickup_tasks' ? ( {f.type === 'clickup_tasks' ? (
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%' }}> <div style={{ marginTop: 8, paddingLeft: 4, width: '100%' }}>
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}> <label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
ClickUp-Verbindung {t('ClickUp-Verbindung')}
</label> </label>
<select <select
value={f.clickupConnectionId ?? ''} value={f.clickupConnectionId ?? ''}
@ -187,7 +190,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
disabled={connectionsLoading || !instanceId} disabled={connectionsLoading || !instanceId}
style={{ width: '100%', marginBottom: 8 }} 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) => ( {connections.map((c) => (
<option key={c.id} value={c.id}> <option key={c.id} value={c.id}>
{c.externalUsername ?? c.id} {c.externalUsername ?? c.id}
@ -195,7 +198,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
))} ))}
</select> </select>
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}> <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> </label>
<input <input
placeholder={t('z.B. aus ClickUp-URL: list/123456789')} placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
@ -208,9 +211,9 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginTop: 6 }}> <p style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginTop: 6 }}>
Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:{' '} {t('Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:')}{' '}
<code>{'{ add: [taskId], rem: [] }'}</code> im ClickUp-Node per Datenquelle auf das <code>{'{ add: [taskId], rem: [] }'}</code>{' '}
Formularfeld mappen. {t('— im ClickUp-Node per Datenquelle auf das Formularfeld mappen.')}
</p> </p>
</div> </div>
) : null} ) : null}
@ -222,7 +225,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }]) updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }])
} }
> >
+ Feld + {t('Feld')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -180,6 +180,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
}; };
const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => { const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
const { t } = useLanguage();
const dependsOn = param.frontendOptions?.dependsOn as string | undefined; const dependsOn = param.frontendOptions?.dependsOn as string | undefined;
const depValue = dependsOn ? allParams?.[dependsOn] : undefined; const depValue = dependsOn ? allParams?.[dependsOn] : undefined;
const disabled = dependsOn && !depValue; const disabled = dependsOn && !depValue;
@ -191,7 +192,7 @@ const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, al
value={typeof value === 'string' ? value : ''} value={typeof value === 'string' ? value : ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
disabled={!!disabled} 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 }} style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', opacity: disabled ? 0.5 : 1 }}
/> />
</div> </div>
@ -214,9 +215,9 @@ const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }
{cases.map((c: Record<string, unknown>, i: number) => ( {cases.map((c: Record<string, unknown>, i: number) => (
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}> <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' }}> <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="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="gt">{t('größer als')}</option>
<option value="lt">{t('kleiner als')}</option> <option value="lt">{t('kleiner als')}</option>
</select> </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> <label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
{fields.map((f: Record<string, unknown>, i: number) => ( {fields.map((f: Record<string, unknown>, i: number) => (
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}> <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' }}> <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="text">{t('Text')}</option>
<option value="number">Number</option> <option value="number">{t('Zahl')}</option>
<option value="date">Date</option> <option value="date">{t('Datum')}</option>
<option value="checkbox">Checkbox</option> <option value="checkbox">{t('Kontrollkästchen')}</option>
<option value="select">Select</option> <option value="select">{t('Auswahl')}</option>
<option value="textarea">Textarea</option> <option value="textarea">{t('Mehrzeilig')}</option>
</select> </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 }}> <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> </label>
<button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button> <button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div> </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> <label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
{rows.map((r: Record<string, unknown>, i: number) => ( {rows.map((r: Record<string, unknown>, i: number) => (
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}> <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={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="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('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> <button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div> </div>
))} ))}
@ -299,7 +300,7 @@ const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) =
type="text" type="text"
value={typeof value === 'string' ? value : ''} value={typeof value === 'string' ? value : ''}
onChange={(e) => onChange(e.target.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' }} 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> <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> <label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
<div style={{ display: 'flex', gap: 4 }}> <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' }}> <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="neq">{t('ungleich')}</option>
<option value="gt">{t('größer als')}</option> <option value="gt">{t('größer als')}</option>
<option value="lt">{t('kleiner 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="empty">{t('ist leer')}</option>
<option value="not_empty">{t('ist nicht leer')}</option> <option value="not_empty">{t('ist nicht leer')}</option>
<option value="is_true">{t('ist wahr')}</option> <option value="is_true">{t('ist wahr')}</option>
<option value="is_false">{t('ist falsch')}</option> <option value="is_false">{t('ist falsch')}</option>
</select> </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>
</div> </div>
); );
@ -366,18 +367,18 @@ const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, on
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label> <label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
<div style={{ display: 'flex', gap: 4 }}> <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' }}> <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="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="startsWith">{t('beginnt mit')}</option>
<option value="isEmpty">{t('ist leer')}</option> <option value="isEmpty">{t('ist leer')}</option>
<option value="isNotEmpty">{t('ist nicht leer')}</option> <option value="isNotEmpty">{t('ist nicht leer')}</option>
<option value="gt">{t('größer als')}</option> <option value="gt">{t('größer als')}</option>
<option value="lt">{t('kleiner als')}</option> <option value="lt">{t('kleiner als')}</option>
</select> </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>
</div> </div>
); );

View file

@ -99,7 +99,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
return ( return (
<div className={styles.ifElseConditionEditor}> <div className={styles.ifElseConditionEditor}>
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>Datenquelle</label> <label>{t('Datenquelle')}</label>
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} /> <RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
</div> </div>
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
@ -114,7 +114,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
</div> </div>
{needsValue && ( {needsValue && (
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>Wert</label> <label>{t('Wert')}</label>
{mimeTypeOptions.length > 0 ? ( {mimeTypeOptions.length > 0 ? (
<select <select
value={String(value ?? '')} value={String(value ?? '')}

View file

@ -45,15 +45,19 @@ function _buildPathsFromSchema(
return result; return result;
} }
function _buildPathsFromPreview(obj: unknown, basePath: (string | number)[] = []): PickablePath[] { function _buildPathsFromPreview(
const pathLabel = basePath.length ? basePath.map(String).join(' → ') : '(ganze Ausgabe)'; 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') { if (obj == null || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
return [{ path: [...basePath], label: pathLabel }]; return [{ path: [...basePath], label: pathLabel }];
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }]; const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
for (let i = 0; i < Math.min(obj.length, 5); i++) { 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; return result;
} }
@ -61,7 +65,7 @@ function _buildPathsFromPreview(obj: unknown, basePath: (string | number)[] = []
const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }]; const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) { for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
if (k.startsWith('_')) continue; if (k.startsWith('_')) continue;
result.push(..._buildPathsFromPreview(v, [...basePath, k])); result.push(..._buildPathsFromPreview(v, [...basePath, k], wholeOutputLabel));
} }
return result; return result;
} }
@ -160,7 +164,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
onClick={() => setShowSystem(!showSystem)} onClick={() => setShowSystem(!showSystem)}
> >
<span className={styles.dataPickerExpandIcon}>{showSystem ? '▼' : '▶'}</span> <span className={styles.dataPickerExpandIcon}>{showSystem ? '▼' : '▶'}</span>
<span className={styles.dataPickerNodeLabel}>System</span> <span className={styles.dataPickerNodeLabel}>{t('System')}</span>
</button> </button>
{showSystem && ( {showSystem && (
<div className={styles.dataPickerTree}> <div className={styles.dataPickerTree}>
@ -200,7 +204,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
const schemaPaths = _buildPathsFromSchema(resolvedSchema); const schemaPaths = _buildPathsFromSchema(resolvedSchema);
const paths = schemaPaths.length > 0 const paths = schemaPaths.length > 0
? schemaPaths ? schemaPaths
: _buildPathsFromPreview(nodeOutputsPreview[nodeId]); : _buildPathsFromPreview(nodeOutputsPreview[nodeId], [], t('(ganze Ausgabe)'));
return ( return (
<div key={nodeId} className={styles.dataPickerNodeSection}> <div key={nodeId} className={styles.dataPickerNodeSection}>

View file

@ -35,7 +35,8 @@ function buildLoopOptions(
sourceIds: string[], sourceIds: string[],
nodes: Array<{ id: string; type?: string; title?: string; parameters?: Record<string, unknown> }>, nodes: Array<{ id: string; type?: string; title?: string; parameters?: Record<string, unknown> }>,
nodeOutputsPreview: 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[] { ): LoopOption[] {
const options: LoopOption[] = []; const options: LoopOption[] = [];
@ -50,13 +51,13 @@ function buildLoopOptions(
if (node?.type === 'trigger.form') { if (node?.type === 'trigger.form') {
options.push({ options.push({
ref: createRef(nodeId, ['payload']), ref: createRef(nodeId, ['payload']),
label: `Alle Formularfelder (${nodeLabel})`, label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
}); });
const filesVal = getValueAtPath(preview, ['files']); const filesVal = getValueAtPath(preview, ['files']);
if (Array.isArray(filesVal)) { if (Array.isArray(filesVal)) {
options.push({ options.push({
ref: createRef(nodeId, ['files']), ref: createRef(nodeId, ['files']),
label: `Alle Dateien aus Formular (${nodeLabel})`, label: `${translate('Alle Dateien aus Formular')} (${nodeLabel})`,
}); });
} }
continue; continue;
@ -65,7 +66,7 @@ function buildLoopOptions(
if (node?.type === 'input.form') { if (node?.type === 'input.form') {
options.push({ options.push({
ref: createRef(nodeId, []), ref: createRef(nodeId, []),
label: `Alle Formularfelder (${nodeLabel})`, label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
}); });
continue; continue;
} }
@ -73,11 +74,11 @@ function buildLoopOptions(
if (node?.type === 'input.upload') { if (node?.type === 'input.upload') {
options.push({ options.push({
ref: createRef(nodeId, ['files']), ref: createRef(nodeId, ['files']),
label: `Alle hochgeladenen Dateien (${nodeLabel})`, label: `${translate('Alle hochgeladenen Dateien')} (${nodeLabel})`,
}); });
options.push({ options.push({
ref: createRef(nodeId, ['fileIds']), ref: createRef(nodeId, ['fileIds']),
label: `Alle Datei-IDs (${nodeLabel})`, label: `${translate('Alle Datei-IDs')} (${nodeLabel})`,
}); });
continue; continue;
} }
@ -85,7 +86,7 @@ function buildLoopOptions(
if (node?.type === 'flow.loop') { if (node?.type === 'flow.loop') {
options.push({ options.push({
ref: createRef(nodeId, ['items']), ref: createRef(nodeId, ['items']),
label: `Alle Elemente aus Schleife (${nodeLabel})`, label: `${translate('Alle Elemente aus Schleife')} (${nodeLabel})`,
}); });
continue; continue;
} }
@ -93,7 +94,7 @@ function buildLoopOptions(
if (node?.type === 'email.searchEmail') { if (node?.type === 'email.searchEmail') {
options.push({ options.push({
ref: createRef(nodeId, ['data', 'searchResults', 'results']), ref: createRef(nodeId, ['data', 'searchResults', 'results']),
label: `Alle gefundenen E-Mails (${nodeLabel})`, label: `${translate('Alle gefundenen E-Mails')} (${nodeLabel})`,
}); });
continue; continue;
} }
@ -101,7 +102,7 @@ function buildLoopOptions(
if (node?.type === 'email.checkEmail') { if (node?.type === 'email.checkEmail') {
options.push({ options.push({
ref: createRef(nodeId, ['data', 'emails', 'emails']), ref: createRef(nodeId, ['data', 'emails', 'emails']),
label: `Alle E-Mails (${nodeLabel})`, label: `${translate('Alle E-Mails')} (${nodeLabel})`,
}); });
continue; continue;
} }
@ -109,7 +110,7 @@ function buildLoopOptions(
if (node?.type === 'sharepoint.listFiles') { if (node?.type === 'sharepoint.listFiles') {
options.push({ options.push({
ref: createRef(nodeId, ['files']), ref: createRef(nodeId, ['files']),
label: `Alle Dateien (${nodeLabel})`, label: `${translate('Alle Dateien')} (${nodeLabel})`,
}); });
continue; continue;
} }
@ -157,7 +158,7 @@ interface LoopItemsSelectProps {
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value, export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
onChange, onChange,
placeholder = 'Über was soll iteriert werden?', placeholder,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const dataFlow = useAutomation2DataFlow(); const dataFlow = useAutomation2DataFlow();
@ -167,7 +168,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
if (sourceIds.length === 0) { if (sourceIds.length === 0) {
return ( return (
<p className={styles.dynamicValueEmptyHint}> <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> </p>
); );
} }
@ -176,7 +177,8 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
sourceIds, sourceIds,
dataFlow.nodes, dataFlow.nodes,
dataFlow.nodeOutputsPreview, dataFlow.nodeOutputsPreview,
dataFlow.getNodeLabel dataFlow.getNodeLabel,
t
); );
const ref = isRef(value) ? value : null; const ref = isRef(value) ? value : null;
@ -198,7 +200,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
}} }}
className={styles.startsInput} className={styles.startsInput}
> >
<option value="">{placeholder}</option> <option value="">{placeholder ?? t('Über was soll iteriert werden?')}</option>
{options.map((o) => ( {options.map((o) => (
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}> <option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
{o.label} {o.label}
@ -206,7 +208,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
))} ))}
</select> </select>
<p className={styles.nodeConfigNameHint}> <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> </p>
</div> </div>
); );

View file

@ -6,6 +6,7 @@
import React from 'react'; import React from 'react';
import { createRef, isRef, isValue, createValue, type DataRef } from './dataRef'; import { createRef, isRef, isValue, createValue, type DataRef } from './dataRef';
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
import { useLanguage } from '../../../../providers/language/LanguageContext';
/** How to build path options for StatischKontextSelect / RefSourceSelect. */ /** How to build path options for StatischKontextSelect / RefSourceSelect. */
export type PathPickMode = 'default' | 'clickup_task_id' | 'exclude_forms'; export type PathPickMode = 'default' | 'clickup_task_id' | 'exclude_forms';
@ -131,6 +132,11 @@ export function refToOptionValue(ref: DataRef): string {
return JSON.stringify(ref); 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 { export function optionValueToRef(s: string): DataRef | null {
try { try {
const o = JSON.parse(s) as unknown; const o = JSON.parse(s) as unknown;
@ -190,10 +196,11 @@ interface StatischKontextSelectProps {
export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
value, value,
onChange, onChange,
placeholder = '— Quelle wählen —', placeholder,
staticLabel = 'Statisch', staticLabel,
pathPickMode = 'default', pathPickMode = 'default',
}) => { }) => {
const { t } = useLanguage();
const dataFlow = useAutomation2DataFlow(); const dataFlow = useAutomation2DataFlow();
if (!dataFlow) return null; if (!dataFlow) return null;
@ -213,7 +220,8 @@ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId; const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
const paths = pickPathsForNode(node, preview, pathPickMode); const paths = pickPathsForNode(node, preview, pathPickMode);
for (const p of paths) { 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({ options.push({
ref: createRef(nodeId, p.path), ref: createRef(nodeId, p.path),
label: displayLabel, label: displayLabel,
@ -245,8 +253,8 @@ export const StatischKontextSelect: React.FC<StatischKontextSelectProps> = ({
if (ref) onChange(ref); if (ref) onChange(ref);
}} }}
> >
<option value="">{placeholder}</option> <option value="">{placeholder ?? t('— Quelle wählen —')}</option>
<option value={STATIC_SOURCE_VALUE}>{staticLabel}</option> <option value={STATIC_SOURCE_VALUE}>{staticLabel ?? t('Statisch')}</option>
{options.map((o) => ( {options.map((o) => (
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}> <option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
{o.label} {o.label}
@ -267,9 +275,10 @@ interface RefSourceSelectProps {
export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
value, value,
onChange, onChange,
placeholder = 'Datenquelle wählen…', placeholder,
pathPickMode = 'default', pathPickMode = 'default',
}) => { }) => {
const { t } = useLanguage();
const dataFlow = useAutomation2DataFlow(); const dataFlow = useAutomation2DataFlow();
if (!dataFlow) return null; if (!dataFlow) return null;
@ -289,7 +298,8 @@ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId; const nodeLabel = node ? dataFlow.getNodeLabel(node) : nodeId;
const paths = pickPathsForNode(node, preview, pathPickMode); const paths = pickPathsForNode(node, preview, pathPickMode);
for (const p of paths) { 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({ options.push({
ref: createRef(nodeId, p.path), ref: createRef(nodeId, p.path),
label: displayLabel, label: displayLabel,
@ -312,7 +322,7 @@ export const RefSourceSelect: React.FC<RefSourceSelectProps> = ({
if (ref) onChange(ref); if (ref) onChange(ref);
}} }}
> >
<option value="">{placeholder}</option> <option value="">{placeholder ?? t('Datenquelle wählen…')}</option>
{options.map((o) => ( {options.map((o) => (
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}> <option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
{o.label} {o.label}
@ -343,13 +353,13 @@ function getFormFieldType(
if (!fieldName) return null; if (!fieldName) return null;
const field = raw.find((f: unknown) => f && typeof f === 'object' && (f as Record<string, unknown>).name === fieldName); const field = raw.find((f: unknown) => f && typeof f === 'object' && (f as Record<string, unknown>).name === fieldName);
if (!field || typeof field !== 'object') return null; if (!field || typeof field !== 'object') return null;
const t = String((field as Record<string, unknown>).type ?? 'text').toLowerCase(); const rawFieldType = String((field as Record<string, unknown>).type ?? 'text').toLowerCase();
if (t === 'number') return 'number'; if (rawFieldType === 'number') return 'number';
if (t === 'email') return 'email'; if (rawFieldType === 'email') return 'email';
if (t === 'date' || t === 'datetime') return 'date'; if (rawFieldType === 'date' || rawFieldType === 'datetime') return 'date';
if (t === 'boolean' || t === 'checkbox') return 'boolean'; if (rawFieldType === 'boolean' || rawFieldType === 'checkbox') return 'boolean';
if (t === 'clickup_tasks') return 'string'; if (rawFieldType === 'clickup_tasks') return 'string';
if (t === 'clickup_status') return 'string'; if (rawFieldType === 'clickup_status') return 'string';
return 'string'; return 'string';
} }

View file

@ -25,7 +25,7 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
const o = f as Record<string, unknown>; const o = f as Record<string, unknown>;
const fieldType = String(o.type ?? 'text'); const fieldType = String(o.type ?? 'text');
const name = String(o.name ?? `field${i + 1}`); 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 = ( const type = (
FORM_FIELD_TYPES.includes(fieldType as (typeof FORM_FIELD_TYPES)[number]) ? fieldType : 'text' FORM_FIELD_TYPES.includes(fieldType as (typeof FORM_FIELD_TYPES)[number]) ? fieldType : 'text'
) as FormField['type']; ) as FormField['type'];
@ -39,7 +39,7 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
} }
return { name, label, type }; 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 ( return (
<div className={styles.startNodeDoc}> <div className={styles.startNodeDoc}>
<p className={styles.startNodeDocIntro}> <p className={styles.startNodeDocIntro}>
<strong>Formular-Felder</strong> werden beim Start ausgefüllt und liegen unter{' '} <strong>{t('Formular-Felder')}</strong>{' '}
<code>payload.&lt;name&gt;</code> in der Start-Ausgabe. {t('werden beim Start ausgefüllt und liegen unter')}{' '}
<code>payload.&lt;name&gt;</code> {t('in der Start-Ausgabe.')}
</p> </p>
<div className={styles.formFieldsList}> <div className={styles.formFieldsList}>
{fields.map((f, idx) => ( {fields.map((f, idx) => (
@ -72,7 +73,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
/> />
<input <input
className={styles.startsInput} className={styles.startsInput}
placeholder="Beschriftung" placeholder={t('Beschriftung')}
value={f.label} value={f.label}
onChange={(e) => { onChange={(e) => {
const next = [...fields]; const next = [...fields];
@ -94,11 +95,11 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
setFields(next); setFields(next);
}} }}
> >
<option value="text">Text</option> <option value="text">{t('Text')}</option>
<option value="number">Zahl</option> <option value="number">{t('Zahl')}</option>
<option value="email">E-Mail</option> <option value="email">{t('E-Mail')}</option>
<option value="date">Datum</option> <option value="date">{t('Datum')}</option>
<option value="boolean">Ja/Nein</option> <option value="boolean">{t('Ja/Nein')}</option>
<option value="clickup_status">{t('ClickUp-Status Liste')}</option> <option value="clickup_status">{t('ClickUp-Status Liste')}</option>
</select> </select>
<button <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' }]) setFields([...fields, { name: `field${fields.length + 1}`, label: t('Neues Feld'), type: 'text' }])
} }
> >
+ Feld {t('+ Feld')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -209,8 +209,9 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
return ( return (
<div className={styles.schedulePanel}> <div className={styles.schedulePanel}>
<p className={styles.startNodeDocIntro}> <p className={styles.startNodeDocIntro}>
Legen Sie fest, <strong>wann</strong> dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird {t(
unten automatisch erzeugt. 'Legen Sie fest, wann dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird unten automatisch erzeugt.'
)}
</p> </p>
<LayoutGroup> <LayoutGroup>
@ -255,7 +256,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
<div className={styles.scheduleModeConfig}> <div className={styles.scheduleModeConfig}>
{o.value === 'daily' && ( {o.value === 'daily' && (
<label className={styles.scheduleFieldRow}> <label className={styles.scheduleFieldRow}>
<span className={styles.scheduleFieldLabel}>Uhrzeit</span> <span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
<input <input
type="time" type="time"
step={60} step={60}
@ -268,7 +269,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
{o.value === 'weekdays' && ( {o.value === 'weekdays' && (
<label className={styles.scheduleFieldRow}> <label className={styles.scheduleFieldRow}>
<span className={styles.scheduleFieldLabel}>Uhrzeit</span> <span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
<input <input
type="time" type="time"
step={60} step={60}
@ -282,7 +283,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
{o.value === 'weekly' && ( {o.value === 'weekly' && (
<> <>
<div className={styles.scheduleFieldCol}> <div className={styles.scheduleFieldCol}>
<span className={styles.scheduleFieldLabel}>Wochentage</span> <span className={styles.scheduleFieldLabel}>{t('Wochentage')}</span>
<div className={styles.scheduleWeekdayToggles}> <div className={styles.scheduleWeekdayToggles}>
{WEEKDAYS_MO_SO.map(({ cronDow, label }) => ( {WEEKDAYS_MO_SO.map(({ cronDow, label }) => (
<button <button
@ -293,13 +294,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
} }
onClick={() => toggleWeekday(cronDow)} onClick={() => toggleWeekday(cronDow)}
> >
{label} {t(label)}
</button> </button>
))} ))}
</div> </div>
</div> </div>
<label className={styles.scheduleFieldRow}> <label className={styles.scheduleFieldRow}>
<span className={styles.scheduleFieldLabel}>Uhrzeit</span> <span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
<input <input
type="time" type="time"
step={60} step={60}
@ -323,7 +324,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
} }
onClick={() => setCalendarPeriod('monthly')} onClick={() => setCalendarPeriod('monthly')}
> >
Monatlich {t('Monatlich')}
</button> </button>
<button <button
type="button" type="button"
@ -334,13 +335,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
} }
onClick={() => setCalendarPeriod('yearly')} onClick={() => setCalendarPeriod('yearly')}
> >
Jährlich {t('Jährlich')}
</button> </button>
</div> </div>
{spec.calendarPeriod === 'monthly' && ( {spec.calendarPeriod === 'monthly' && (
<label className={styles.scheduleFieldRow}> <label className={styles.scheduleFieldRow}>
<span className={styles.scheduleFieldLabel}>Monatstag</span> <span className={styles.scheduleFieldLabel}>{t('Monatstag')}</span>
<select <select
className={styles.scheduleSelect} className={styles.scheduleSelect}
value={spec.monthDay} value={spec.monthDay}
@ -358,7 +359,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
{spec.calendarPeriod === 'yearly' && ( {spec.calendarPeriod === 'yearly' && (
<div className={styles.scheduleYearlyRow}> <div className={styles.scheduleYearlyRow}>
<label className={styles.scheduleFieldRowGrow}> <label className={styles.scheduleFieldRowGrow}>
<span className={styles.scheduleFieldLabel}>Monat</span> <span className={styles.scheduleFieldLabel}>{t('Monat')}</span>
<select <select
className={styles.scheduleSelect} className={styles.scheduleSelect}
value={spec.monthIndex} value={spec.monthIndex}
@ -366,13 +367,13 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
> >
{MONTH_NAMES_DE.map((name, i) => ( {MONTH_NAMES_DE.map((name, i) => (
<option key={i + 1} value={i + 1}> <option key={i + 1} value={i + 1}>
{name} {t(name)}
</option> </option>
))} ))}
</select> </select>
</label> </label>
<label className={styles.scheduleFieldRowGrow}> <label className={styles.scheduleFieldRowGrow}>
<span className={styles.scheduleFieldLabel}>Tag</span> <span className={styles.scheduleFieldLabel}>{t('Tag')}</span>
<select <select
className={styles.scheduleSelect} className={styles.scheduleSelect}
value={spec.monthDay} value={spec.monthDay}
@ -389,7 +390,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
)} )}
<label className={styles.scheduleFieldRow}> <label className={styles.scheduleFieldRow}>
<span className={styles.scheduleFieldLabel}>Uhrzeit</span> <span className={styles.scheduleFieldLabel}>{t('Uhrzeit')}</span>
<input <input
type="time" type="time"
step={60} step={60}

View file

@ -6,6 +6,7 @@
import React from 'react'; import React from 'react';
import type { NodeConfigRendererProps } from '../shared/types'; import type { NodeConfigRendererProps } from '../shared/types';
import styles from '../../editor/Automation2FlowEditor.module.css'; import styles from '../../editor/Automation2FlowEditor.module.css';
import { useLanguage } from '../../../../providers/language/LanguageContext';
const SCHEMA_EXAMPLE = `{ const SCHEMA_EXAMPLE = `{
"trigger": { "trigger": {
@ -22,17 +23,20 @@ const SCHEMA_EXAMPLE = `{
}`; }`;
export const StartNodeConfig: React.FC<NodeConfigRendererProps> = () => { export const StartNodeConfig: React.FC<NodeConfigRendererProps> = () => {
const { t } = useLanguage();
return ( return (
<div className={styles.startNodeDoc}> <div className={styles.startNodeDoc}>
<p className={styles.startNodeDocIntro}> <p className={styles.startNodeDocIntro}>
Die <strong>Start</strong>-Node liefert beim Ausführen immer dieselbe Struktur. Den <strong>Einstiegstyp</strong>{' '} {t(
(manuell, Formular, Zeitplan, ) wählen Sie über das <strong>Zahnrad</strong> oben in der '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.'
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>
<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.startNodeSchema}>
<div className={styles.startNodeSchemaTitle}>Ausgabe-Schema</div> <div className={styles.startNodeSchemaTitle}>{t('Ausgabe-Schema')}</div>
<pre className={styles.startNodePre}>{SCHEMA_EXAMPLE}</pre> <pre className={styles.startNodePre}>{SCHEMA_EXAMPLE}</pre>
</div> </div>
</div> </div>

View file

@ -117,7 +117,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
<option value="">{t('MIME-Typ wählen')}</option> <option value="">{t('MIME-Typ wählen')}</option>
{mimeTypeOptions.map((o) => ( {mimeTypeOptions.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
{o.label} ({o.value}) {t(o.label)} ({o.value})
</option> </option>
))} ))}
</select> </select>
@ -166,7 +166,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
className={styles.startsInput} className={styles.startsInput}
value={valStr} value={valStr}
onChange={(e) => handleCaseValueChange(index, e.target.value)} 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 ( return (
<div className={styles.ifElseConditionEditor}> <div className={styles.ifElseConditionEditor}>
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>Datenquelle</label> <label>{t('Datenquelle')}</label>
<RefSourceSelect <RefSourceSelect
value={ref} value={ref}
onChange={handleRefChange} onChange={handleRefChange}
@ -221,7 +221,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
> >
{operators.map((o) => ( {operators.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
{o.label} {t(o.label)}
</option> </option>
))} ))}
</select> </select>
@ -241,7 +241,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
); );
})} })}
<button type="button" className={styles.startsAddBtn} onClick={addCase}> <button type="button" className={styles.startsAddBtn} onClick={addCase}>
+ Fall {t('+ Fall')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -162,13 +162,6 @@ const _SCOPE_ICONS: Record<string, string> = {
const _SCOPE_CYCLE: string[] = ['personal', 'featureInstance', 'mandate']; const _SCOPE_CYCLE: string[] = ['personal', 'featureInstance', 'mandate'];
const _SCOPE_LABELS: Record<string, string> = {
personal: 'Persönlich',
featureInstance: 'Instanz',
mandate: 'Mandant',
global: 'Global',
};
interface SelectionCtx { interface SelectionCtx {
selectedItemIds: Set<string>; selectedItemIds: Set<string>;
selectedFileIds: string[]; selectedFileIds: string[];
@ -187,6 +180,12 @@ interface SelectionCtx {
function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) { function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
const { t } = useLanguage(); 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 [dragging, setDragging] = useState(false);
const [renaming, setRenaming] = useState(false); const [renaming, setRenaming] = useState(false);
const [renameValue, setRenameValue] = useState(''); const [renameValue, setRenameValue] = useState('');
@ -257,14 +256,14 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
<span className={styles.rightZone}> <span className={styles.rightZone}>
<span className={styles.actions}> <span className={styles.actions}>
{sel.onRenameFile && !multiSelected && ( {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 /> <FaPen />
</button> </button>
)} )}
{multiSelected && isSelected ? ( {multiSelected && isSelected ? (
<> <>
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && ( {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 /> <FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span> <span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
</button> </button>
@ -300,7 +299,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
const next = _SCOPE_CYCLE[(idx + 1) % _SCOPE_CYCLE.length]; const next = _SCOPE_CYCLE[(idx + 1) % _SCOPE_CYCLE.length];
sel.onScopeChange(file.id, next); 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 }} style={{ fontSize: 14 }}
> >
{_SCOPE_ICONS[file.scope!] || '\uD83D\uDC64'} {_SCOPE_ICONS[file.scope!] || '\uD83D\uDC64'}
@ -381,12 +380,12 @@ function _TreeNode({
const _handleAdd = useCallback(async (e: React.MouseEvent) => { const _handleAdd = useCallback(async (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!onCreateFolder) return; 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()) { if (name?.trim()) {
await onCreateFolder(name.trim(), node.id); await onCreateFolder(name.trim(), node.id);
if (!expandedIds.has(node.id)) onToggle(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) => { const _handleDeleteSingle = useCallback(async (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
@ -753,7 +752,7 @@ export default function FolderTree({
onDrop={_handleRootDrop} onDrop={_handleRootDrop}
> >
<span className={styles.folderIcon}><FaGlobe /></span> <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}> <span className={styles.rootActions}>
{onRefresh && ( {onRefresh && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}> <button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
@ -765,7 +764,7 @@ export default function FolderTree({
className={styles.actionBtn} className={styles.actionBtn}
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); 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); if (name?.trim()) await onCreateFolder(name.trim(), null);
}} }}
title={t('Neuer Ordner')} title={t('Neuer Ordner')}

View file

@ -7,6 +7,7 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa'; import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa';
import styles from './FolderTree.module.css'; import styles from './FolderTree.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
export interface BrowseEntry { export interface BrowseEntry {
name: string; name: string;
@ -100,6 +101,7 @@ function _FolderRow({
onSelectFolder?: (path: string) => void; onSelectFolder?: (path: string) => void;
foldersOnly: boolean; foldersOnly: boolean;
}) { }) {
const { t } = useLanguage();
const isExpanded = expandedPaths.has(entry.path); const isExpanded = expandedPaths.has(entry.path);
const isSelected = selectedPath === entry.path; const isSelected = selectedPath === entry.path;
const children = loadedChildren[entry.path] ?? []; const children = loadedChildren[entry.path] ?? [];
@ -132,7 +134,7 @@ function _FolderRow({
<span <span
className={`${styles.chevron} ${isExpanded ? styles.expanded : ''}`} className={`${styles.chevron} ${isExpanded ? styles.expanded : ''}`}
onClick={handleChevronClick} onClick={handleChevronClick}
title={isExpanded ? 'Einklappen' : 'Erweitern'} title={isExpanded ? t('Einklappen') : t('Erweitern')}
> >
<FaChevronRight /> <FaChevronRight />
</span> </span>
@ -148,7 +150,7 @@ function _FolderRow({
<div className={styles.children}> <div className={styles.children}>
{isLoading ? ( {isLoading ? (
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}> <div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
Wird geladen {t('Wird geladen…')}
</div> </div>
) : ( ) : (
<> <>
@ -177,7 +179,7 @@ function _FolderRow({
))} ))}
{children.length === 0 && ( {children.length === 0 && (
<div style={{ padding: '0.4rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}> <div style={{ padding: '0.4rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
Leer {t('Leer')}
</div> </div>
)} )}
</> </>
@ -199,6 +201,7 @@ export function SharepointBrowseTree({
selectedPath, selectedPath,
initialChildren = [], initialChildren = [],
}: SharepointBrowseTreeProps) { }: SharepointBrowseTreeProps) {
const { t } = useLanguage();
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set([rootPath])); const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set([rootPath]));
const [loadedChildren, setLoadedChildren] = useState<Record<string, BrowseEntry[]>>(() => const [loadedChildren, setLoadedChildren] = useState<Record<string, BrowseEntry[]>>(() =>
initialChildren.length > 0 ? { [rootPath]: initialChildren } : {} initialChildren.length > 0 ? { [rootPath]: initialChildren } : {}
@ -261,11 +264,12 @@ export function SharepointBrowseTree({
<span <span
className={`${styles.chevron} ${isRootExpanded ? styles.expanded : ''}`} className={`${styles.chevron} ${isRootExpanded ? styles.expanded : ''}`}
onClick={() => handleToggle(rootPath)} onClick={() => handleToggle(rootPath)}
title={isRootExpanded ? t('Einklappen') : t('Erweitern')}
> >
<FaChevronRight /> <FaChevronRight />
</span> </span>
<span className={styles.folderIcon}><FaGlobe /></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 && ( {rootLoading && (
<span style={{ fontSize: 10, color: 'var(--color-text-secondary,#999)', marginLeft: 4 }}></span> <span style={{ fontSize: 10, color: 'var(--color-text-secondary,#999)', marginLeft: 4 }}></span>
)} )}
@ -274,7 +278,7 @@ export function SharepointBrowseTree({
<div className={styles.children}> <div className={styles.children}>
{rootLoading ? ( {rootLoading ? (
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}> <div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#666)' }}>
Sites werden geladen {t('Sites werden geladen…')}
</div> </div>
) : ( ) : (
<> <>
@ -303,7 +307,7 @@ export function SharepointBrowseTree({
))} ))}
{rootItems.length === 0 && !rootLoading && ( {rootItems.length === 0 && !rootLoading && (
<div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}> <div style={{ padding: '0.5rem 1rem', fontSize: '0.8rem', color: 'var(--color-text-secondary,#999)' }}>
Keine Einträge {t('Keine Einträge')}
</div> </div>
)} )}
</> </>

View file

@ -131,11 +131,11 @@ export function DeleteActionButton<T = any>({
} else { } else {
// Refetch to restore the item in case of failure // Refetch to restore the item in case of failure
await refetch(); await refetch();
onError?.(row, 'Delete failed'); onError?.(row, t('Löschen fehlgeschlagen'));
} }
} catch (error: any) { } catch (error: any) {
console.error('Delete failed:', error); 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 // Refetch to restore the item in case of failure
await refetch(); await refetch();
} finally { } finally {

View file

@ -100,7 +100,7 @@ export function ViewActionButton<T = any>({
isOpen={isPopupOpen} isOpen={isPopupOpen}
onClose={() => setIsPopupOpen(false)} onClose={() => setIsPopupOpen(false)}
fileId={(row as any)[idField]} fileId={(row as any)[idField]}
fileName={(row as any)[nameField] || 'Unknown Item'} fileName={(row as any)[nameField] || t('Unbekanntes Element')}
mimeType={mimeType} mimeType={mimeType}
/> />
)} )}
@ -115,7 +115,7 @@ export function ViewActionButton<T = any>({
> >
<div style={{ padding: '20px' }}> <div style={{ padding: '20px' }}>
<h3 style={{ marginBottom: '20px', fontSize: '1.2rem', fontWeight: 'bold' }}> <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> </h3>
<div style={{ display: 'grid', gap: '15px' }}> <div style={{ display: 'grid', gap: '15px' }}>
{Object.entries(row as Record<string, any>) {Object.entries(row as Record<string, any>)

View file

@ -20,7 +20,7 @@ const isTextMultilingual = (value: any): boolean => {
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) { if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
return false; 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. // Note: Field types are determined ONLY by the explicit 'type' property.
@ -50,7 +50,7 @@ export interface AttributeDefinition {
export interface AttributeOption { export interface AttributeOption {
value: string | number; value: string | number;
label: string | { [language: string]: string }; label: string;
} }
// FormGeneratorForm props // FormGeneratorForm props
@ -243,7 +243,7 @@ export function FormGeneratorForm<T extends Record<string, any>>({
if (!isTextMultilingual(processedData[attr.name])) { if (!isTextMultilingual(processedData[attr.name])) {
// If it's a string, convert to TextMultilingual // If it's a string, convert to TextMultilingual
if (typeof processedData[attr.name] === 'string') { 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) { if (attr.default !== undefined) {
initialData[attr.name] = attr.default; initialData[attr.name] = attr.default;
} else if (isMultilingual(attr)) { } else if (isMultilingual(attr)) {
// Initialize TextMultilingual fields with empty object initialData[attr.name] = { xx: '' };
initialData[attr.name] = { en: '' };
} else { } else {
initialData[attr.name] = getDefaultValueForType(attr.type); 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') { if (typeof opt === 'string' || typeof opt === 'number') {
return { value: opt, label: String(opt) }; return { value: opt, label: String(opt) };
} }
// Handle multilingual labels return { value: opt.value, label: opt.label || String(opt.value) };
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 };
}); });
} }
@ -436,12 +431,9 @@ export function FormGeneratorForm<T extends Record<string, any>>({
return { value: opt, label: opt }; return { value: opt, label: opt };
} }
if (typeof opt === 'object' && 'value' in 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 { return {
value: opt.value, value: opt.value,
label: labelValue label: opt.label || String(opt.value)
}; };
} }
return { value: String(opt), label: String(opt) }; 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) // Special handling for TextMultilingual fields (by explicit type only)
const isMultilingualField = isMultilingualType(attr.type as AttributeType); const isMultilingualField = isMultilingualType(attr.type as AttributeType);
if (isMultilingualField && isTextMultilingual(value)) { 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', { newErrors[attr.name] = t('{fieldLabel} ist erforderlich', {
fieldLabel: `${attr.label} (Englisch)`, fieldLabel: attr.label,
}); });
return; return;
} }
@ -655,21 +647,17 @@ export function FormGeneratorForm<T extends Record<string, any>>({
} }
}; };
// Build multilingual language list dynamically from availableLanguages. // xx = source/default text (required), then all available languages dynamically.
// 'en' is always first and required; remaining languages follow in DB order.
const multilingualLangs = useMemo(() => { const multilingualLangs = useMemo(() => {
const base: { code: string; uiLabel: string; required: boolean }[] = [ const langs: { code: string; uiLabel: string; required: boolean }[] = [
{ code: 'en', uiLabel: 'EN', required: true }, { code: 'xx', uiLabel: t('Quelltext'), required: true },
]; ];
for (const lang of availableLanguages) { for (const lang of availableLanguages) {
if (lang.code === 'en' || lang.code === 'xx') continue; if (lang.code === 'xx') continue;
base.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false }); langs.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false });
} }
if (base.length === 1) { return langs;
base.push({ code: 'de', uiLabel: 'DE', required: false }); }, [availableLanguages, t]);
}
return base;
}, [availableLanguages]);
const _handleAutoTranslate = async (attrName: string, multilingualValue: Record<string, string>) => { const _handleAutoTranslate = async (attrName: string, multilingualValue: Record<string, string>) => {
const sourceLang = multilingualLangs.find(l => (multilingualValue[l.code] || '').trim())?.code; 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 // Render multilingual field
const renderMultilingualField = (attr: AttributeDefinition) => { const renderMultilingualField = (attr: AttributeDefinition) => {
const value = formData[attr.name] || { en: '' }; const value = formData[attr.name] || { xx: '' };
const hasError = errors[attr.name]; const hasError = errors[attr.name];
const isReadonly = mode === 'display' || attr.readonly || !isFieldEditableInMode(attr, mode); 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 handleMultilingualChange = (langCode: string, langValue: string) => {
const newValue = { ...multilingualValue, [langCode]: langValue }; 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)} onChange={(e) => handleMultilingualChange(lang.code, e.target.value)}
onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)} onFocus={() => handleFieldFocus(`${attr.name}.${lang.code}`, true)}
onBlur={() => handleFieldFocus(`${attr.name}.${lang.code}`, false)} 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])}> <label className={getLabelClass(`${attr.name}.${lang.code}`, multilingualValue[lang.code])}>
{lang.uiLabel} {lang.uiLabel}

View file

@ -40,12 +40,6 @@ const CHART_COLORS = [
'#bab0ac' '#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 // HELPER FUNCTIONS
// ============================================================================= // =============================================================================
@ -54,15 +48,22 @@ function _defaultFormatCurrency(value: number, currencyCode: string): string {
return `${currencyCode} ${value.toFixed(2)}`; return `${currencyCode} ${value.toFixed(2)}`;
} }
function _formatDateLabel(dateStr: string): string { function _createFormatDateLabel(t: (key: string) => string): (dateStr: string) => string {
const parts = dateStr.split('-'); const monthLabels: Record<string, string> = {
if (parts.length === 3) { '01': t('Jan'), '02': t('Feb'), '03': t('Mär'), '04': t('Apr'),
return `${parseInt(parts[2], 10)}.`; '05': t('Mai'), '06': t('Jun'), '07': t('Jul'), '08': t('Aug'),
} '09': t('Sep'), '10': t('Okt'), '11': t('Nov'), '12': t('Dez'),
if (parts.length === 2) { };
return MONTH_LABELS[parts[1]] || parts[1]; return (dateStr: string) => {
} const parts = dateStr.split('-');
return dateStr; if (parts.length === 3) {
return `${parseInt(parts[2], 10)}.`;
}
if (parts.length === 2) {
return monthLabels[parts[1]] || parts[1];
}
return dateStr;
};
} }
// ============================================================================= // =============================================================================
@ -74,12 +75,13 @@ interface CustomTooltipProps {
payload?: any[]; payload?: any[];
label?: string; label?: string;
formatValue?: (value: number) => 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; if (!active || !payload?.length) return null;
const displayLabel = label ? _formatDateLabel(String(label)) : ''; const displayLabel = label ? formatDateLabel(String(label)) : '';
return ( return (
<div className={styles.customTooltip}> <div className={styles.customTooltip}>
@ -115,15 +117,18 @@ const _renderKpiGrid = (section: ReportSectionKpi): React.ReactNode => {
// --- Bar Chart (vertical) --- // --- Bar Chart (vertical) ---
const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string): React.ReactNode => { const _renderBarChart = (
const { t } = useLanguage(); section: ReportSectionBarChart,
currencyCode: string,
formatDateLabel: (dateStr: string) => string,
t: (key: string) => string,
): React.ReactNode => {
if (!section.data?.length) { if (!section.data?.length) {
return <div className={styles.noData}>{t('Keine Daten')}</div>; return <div className={styles.noData}>{t('Keine Daten')}</div>;
} }
const chartData = section.data.map(d => ({ const chartData = section.data.map(d => ({
name: _formatDateLabel(d.key), name: formatDateLabel(d.key),
value: d.value, value: d.value,
rawKey: d.key rawKey: d.key
})); }));
@ -146,12 +151,12 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
tickFormatter={(v) => formatter(v)} tickFormatter={(v) => formatter(v)}
width={70} width={70}
/> />
<Tooltip content={<_CustomTooltip formatValue={formatter} />} /> <Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
<Bar <Bar
dataKey="value" dataKey="value"
fill={section.color || CHART_COLORS[0]} fill={section.color || CHART_COLORS[0]}
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
name="Wert" name={t('Wert')}
/> />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
@ -161,9 +166,7 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
// --- Horizontal Bar Chart --- // --- Horizontal Bar Chart ---
const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string): React.ReactNode => { const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string, t: (key: string) => string): React.ReactNode => {
const { t } = useLanguage();
if (!section.data?.length) { if (!section.data?.length) {
return <div className={styles.noData}>{t('Keine Daten')}</div>; return <div className={styles.noData}>{t('Keine Daten')}</div>;
} }
@ -194,9 +197,12 @@ const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode:
// --- Line Chart --- // --- Line Chart ---
const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string): React.ReactNode => { const _renderLineChart = (
const { t } = useLanguage(); section: ReportSectionLineChart,
currencyCode: string,
formatDateLabel: (dateStr: string) => string,
t: (key: string) => string,
): React.ReactNode => {
if (!section.data?.length) { if (!section.data?.length) {
return <div className={styles.noData}>{t('Keine Daten')}</div>; return <div className={styles.noData}>{t('Keine Daten')}</div>;
} }
@ -212,7 +218,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
dataKey="date" dataKey="date"
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }} tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
axisLine={{ stroke: 'var(--border-color, #333)' }} axisLine={{ stroke: 'var(--border-color, #333)' }}
tickFormatter={_formatDateLabel} tickFormatter={formatDateLabel}
/> />
<YAxis <YAxis
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }} tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
@ -220,7 +226,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
tickFormatter={formatter} tickFormatter={formatter}
width={70} width={70}
/> />
<Tooltip content={<_CustomTooltip formatValue={formatter} />} /> <Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
{section.series.map((s, i) => ( {section.series.map((s, i) => (
<Line <Line
key={s.key} key={s.key}
@ -242,9 +248,12 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
// --- Area Chart --- // --- Area Chart ---
const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string): React.ReactNode => { const _renderAreaChart = (
const { t } = useLanguage(); section: ReportSectionAreaChart,
currencyCode: string,
formatDateLabel: (dateStr: string) => string,
t: (key: string) => string,
): React.ReactNode => {
if (!section.data?.length) { if (!section.data?.length) {
return <div className={styles.noData}>{t('Keine Daten')}</div>; return <div className={styles.noData}>{t('Keine Daten')}</div>;
} }
@ -260,7 +269,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
dataKey="date" dataKey="date"
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }} tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
axisLine={{ stroke: 'var(--border-color, #333)' }} axisLine={{ stroke: 'var(--border-color, #333)' }}
tickFormatter={_formatDateLabel} tickFormatter={formatDateLabel}
/> />
<YAxis <YAxis
tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }} tick={{ fontSize: 11, fill: 'var(--text-secondary, #888)' }}
@ -268,7 +277,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
tickFormatter={formatter} tickFormatter={formatter}
width={70} width={70}
/> />
<Tooltip content={<_CustomTooltip formatValue={formatter} />} /> <Tooltip content={<_CustomTooltip formatValue={formatter} formatDateLabel={formatDateLabel} />} />
{section.series.map((s, i) => ( {section.series.map((s, i) => (
<Area <Area
key={s.key} key={s.key}
@ -290,9 +299,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
// --- Pie Chart --- // --- Pie Chart ---
const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string): React.ReactNode => { const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string, t: (key: string) => string): React.ReactNode => {
const { t } = useLanguage();
if (!section.data?.length) { if (!section.data?.length) {
return <div className={styles.noData}>{t('Keine Daten')}</div>; return <div className={styles.noData}>{t('Keine Daten')}</div>;
} }
@ -418,7 +425,7 @@ const _ReportTableSection: React.FC<ReportTableSectionProps> = ({ section, curre
{hasMore && !showAll && ( {hasMore && !showAll && (
<div className={styles.showMoreRow}> <div className={styles.showMoreRow}>
<button className={styles.showMoreButton} onClick={() => setShowAll(true)}> <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> </button>
</div> </div>
)} )}
@ -436,6 +443,9 @@ interface SectionWrapperProps {
} }
const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode }) => { const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode }) => {
const { t } = useLanguage();
const formatDateLabel = useMemo(() => _createFormatDateLabel(t), [t]);
const spanClass = section.type === 'kpiGrid' || section.span === 'full' const spanClass = section.type === 'kpiGrid' || section.span === 'full'
? styles.sectionFull ? styles.sectionFull
: section.span === 'half' : section.span === 'half'
@ -452,20 +462,18 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
); );
} }
const _renderContent = (): React.ReactNode => { const renderContent = (): React.ReactNode => {
const { t } = useLanguage();
switch (section.type) { switch (section.type) {
case 'barChart': case 'barChart':
return _renderBarChart(section, currencyCode); return _renderBarChart(section, currencyCode, formatDateLabel, t);
case 'horizontalBar': case 'horizontalBar':
return _renderHorizontalBar(section, currencyCode); return _renderHorizontalBar(section, currencyCode, t);
case 'lineChart': case 'lineChart':
return _renderLineChart(section, currencyCode); return _renderLineChart(section, currencyCode, formatDateLabel, t);
case 'areaChart': case 'areaChart':
return _renderAreaChart(section, currencyCode); return _renderAreaChart(section, currencyCode, formatDateLabel, t);
case 'pieChart': case 'pieChart':
return _renderPieChart(section, currencyCode); return _renderPieChart(section, currencyCode, t);
case 'table': case 'table':
return <_ReportTableSection section={section} currencyCode={currencyCode} />; return <_ReportTableSection section={section} currencyCode={currencyCode} />;
default: default:
@ -477,7 +485,7 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
<div className={`${spanClass} ${styles.sectionCard}`}> <div className={`${spanClass} ${styles.sectionCard}`}>
{section.title && <h3 className={styles.sectionTitle}>{section.title}</h3>} {section.title && <h3 className={styles.sectionTitle}>{section.title}</h3>}
{section.description && <p className={styles.sectionDescription}>{section.description}</p>} {section.description && <p className={styles.sectionDescription}>{section.description}</p>}
{_renderContent()} {renderContent()}
</div> </div>
); );
}; };
@ -607,7 +615,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
value={filterState.dateRange?.from?.toISOString().split('T')[0] || ''} value={filterState.dateRange?.from?.toISOString().split('T')[0] || ''}
onChange={(e) => _handleDateRangeChange('from', e.target.value)} onChange={(e) => _handleDateRangeChange('from', e.target.value)}
/> />
<span className={styles.toolbarLabel}>Bis</span> <span className={styles.toolbarLabel}>{t('Bis')}</span>
<input <input
type="date" type="date"
className={styles.dateInput} className={styles.dateInput}
@ -640,7 +648,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
value={(filterState.filters[filter.key] as string) || ''} value={(filterState.filters[filter.key] as string) || ''}
onChange={(e) => _handleFilterChange(filter.key, e.target.value)} 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 => ( {filter.options?.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}

View file

@ -96,7 +96,7 @@ export const UserSection: React.FC = () => {
onClick={handleBilling} onClick={handleBilling}
> >
<span className={styles.menuIcon}>💰</span> <span className={styles.menuIcon}>💰</span>
Guthaben {t('Guthaben')}
</button> </button>
<button <button
@ -104,7 +104,7 @@ export const UserSection: React.FC = () => {
onClick={handleSettings} onClick={handleSettings}
> >
<span className={styles.menuIcon}></span> <span className={styles.menuIcon}></span>
Einstellungen {t('Einstellungen')}
</button> </button>
{onboardingHidden && ( {onboardingHidden && (
@ -113,7 +113,7 @@ export const UserSection: React.FC = () => {
onClick={handleOnboarding} onClick={handleOnboarding}
> >
<span className={styles.menuIcon}>{'\uD83E\uDDED'}</span> <span className={styles.menuIcon}>{'\uD83E\uDDED'}</span>
Onboarding-Assistent {t('Onboarding-Assistent')}
</button> </button>
)} )}
@ -122,7 +122,7 @@ export const UserSection: React.FC = () => {
onClick={handleLegal} onClick={handleLegal}
> >
<span className={styles.menuIcon}>📜</span> <span className={styles.menuIcon}>📜</span>
Rechtliche Hinweise {t('Rechtliche Hinweise')}
</button> </button>
<div className={styles.menuDivider} /> <div className={styles.menuDivider} />
@ -133,7 +133,7 @@ export const UserSection: React.FC = () => {
disabled={isLoggingOut} disabled={isLoggingOut}
> >
<span className={styles.menuIcon}>🚪</span> <span className={styles.menuIcon}>🚪</span>
{isLoggingOut ? 'Abmelden...' : 'Abmelden'} {isLoggingOut ? t('Abmelden...') : t('Abmelden')}
</button> </button>
</div> </div>
)} )}
@ -176,13 +176,13 @@ export const UserSection: React.FC = () => {
<div className={styles.legalLinks}> <div className={styles.legalLinks}>
<a href="/poweron-privacy.html" target="_blank" rel="noopener noreferrer"> <a href="/poweron-privacy.html" target="_blank" rel="noopener noreferrer">
Datenschutzrichtlinie {t('Datenschutzrichtlinie')}
</a> </a>
<a href="/poweron-terms.html" target="_blank" rel="noopener noreferrer"> <a href="/poweron-terms.html" target="_blank" rel="noopener noreferrer">
Nutzungsbedingungen {t('Nutzungsbedingungen')}
</a> </a>
<a href="/poweron-home.html" target="_blank" rel="noopener noreferrer"> <a href="/poweron-home.html" target="_blank" rel="noopener noreferrer">
Über PowerOn {t('Über PowerOn')}
</a> </a>
</div> </div>
</div> </div>

View file

@ -20,32 +20,32 @@ const typeIcons: Record<string, React.ReactNode> = {
mention: <FaExclamationTriangle /> 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 { interface NotificationBellProps {
className?: string; className?: string;
} }
export const NotificationBell: React.FC<NotificationBellProps> = ({ className }) => { export const NotificationBell: React.FC<NotificationBellProps> = ({ className }) => {
const { t } = useLanguage(); 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 { const {
notifications, notifications,
unreadCount, unreadCount,
@ -144,7 +144,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
<button <button
className={styles.bellButton} className={styles.bellButton}
onClick={() => setIsOpen(!isOpen)} 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} /> <FaBell className={styles.bellIcon} />
{unreadCount > 0 && ( {unreadCount > 0 && (
@ -165,7 +165,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
className={styles.markAllRead} className={styles.markAllRead}
onClick={() => markAllAsRead()} onClick={() => markAllAsRead()}
> >
Alle als gelesen markieren {t('Alle als gelesen markieren')}
</button> </button>
)} )}
</div> </div>
@ -201,7 +201,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
{actionSuccess === notification.id && ( {actionSuccess === notification.id && (
<div className={styles.successOverlay}> <div className={styles.successOverlay}>
<FaCheckCircle /> <FaCheckCircle />
<span>{notification.actionResult || 'Erfolgreich'}</span> <span>{notification.actionResult || t('Erfolgreich')}</span>
</div> </div>
)} )}

View file

@ -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 { useNavigate, useLocation } from 'react-router-dom';
import api from '../api'; import api from '../api';
import OnboardingWizard from './OnboardingWizard'; import OnboardingWizard from './OnboardingWizard';
@ -19,13 +19,6 @@ interface OnboardingAssistantProps {
const _STORAGE_KEY = 'onboarding_hidden'; 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 { export function _isOnboardingHidden(): boolean {
try { try {
return localStorage.getItem(_STORAGE_KEY) === 'true'; return localStorage.getItem(_STORAGE_KEY) === 'true';
@ -48,6 +41,12 @@ function _hideOnboarding(): void {
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => { const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
const { t } = useLanguage(); 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 navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [hidden, setHidden] = useState(() => _isOnboardingHidden()); const [hidden, setHidden] = useState(() => _isOnboardingHidden());
@ -99,7 +98,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
id: 'mandate', id: 'mandate',
label: t('Mandat einrichten'), label: t('Mandat einrichten'),
description: hasAdminMandate description: hasAdminMandate
? 'Dein Mandant ist eingerichtet.' ? t('Dein Mandant ist eingerichtet.')
: hasFeature : hasFeature
? t('Du bist Mitglied eines Mandanten') ? t('Du bist Mitglied eines Mandanten')
: t('Erstelle deinen Arbeitsbereich'), : t('Erstelle deinen Arbeitsbereich'),
@ -166,7 +165,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [navigate]); }, [navigate, t]);
useEffect(() => { useEffect(() => {
const state = location.state as { showOnboarding?: number } | null; const state = location.state as { showOnboarding?: number } | null;
@ -215,7 +214,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
<div> <div>
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('Willkommen bei Poweron')}</h3> <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)' }}> <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> </p>
</div> </div>
</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> <span style={{ fontSize: '0.8rem', color: 'var(--accent, #4f46e5)', fontWeight: 500 }}>{'\u2192'}</span>
)} )}
</div> </div>
{isNextStep && _CALLOUTS[step.id] && ( {isNextStep && callouts[step.id as keyof typeof callouts] && (
<div style={{ <div style={{
marginTop: 4, marginLeft: 34, padding: '6px 10px', marginTop: 4, marginLeft: 34, padding: '6px 10px',
fontSize: '0.78rem', color: 'var(--accent, #4f46e5)', fontSize: '0.78rem', color: 'var(--accent, #4f46e5)',
background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6, background: 'rgba(79, 70, 229, 0.06)', borderRadius: 6,
borderLeft: '3px solid var(--accent, #4f46e5)', borderLeft: '3px solid var(--accent, #4f46e5)',
}}> }}>
{_CALLOUTS[step.id]} {callouts[step.id as keyof typeof callouts]}
</div> </div>
)} )}
</div> </div>
@ -290,7 +289,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
onChange={(e) => setDontShowAgain(e.target.checked)} onChange={(e) => setDontShowAgain(e.target.checked)}
style={{ margin: 0 }} style={{ margin: 0 }}
/> />
Nicht wieder anzeigen {t('Nicht wieder anzeigen')}
</label> </label>
<button <button
onClick={_handleDismiss} onClick={_handleDismiss}
@ -304,7 +303,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
borderRadius: 6, borderRadius: 6,
}} }}
> >
Schliessen {t('Schliessen')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -30,7 +30,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
window.dispatchEvent(new CustomEvent('features-changed')); window.dispatchEvent(new CustomEvent('features-changed'));
onComplete(); onComplete();
} catch (err: any) { } catch (err: any) {
setError(err?.response?.data?.detail || 'Fehler bei der Einrichtung'); setError(err?.response?.data?.detail || t('Fehler bei der Einrichtung'));
} finally { } finally {
setLoading(false); 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> <h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>{t('Mandant erstellen')}</h2>
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}> <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> </p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
@ -62,7 +62,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
<div> <div>
<strong>{t('Kostenlos testen')}</strong> <strong>{t('Kostenlos testen')}</strong>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}> <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>
</div> </div>
</label> </label>
@ -77,7 +77,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
<div> <div>
<strong>{t('Standard monatlich')}</strong> <strong>{t('Standard monatlich')}</strong>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}> <div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
Team-Workspace mit vollem Funktionsumfang {t('Team-Workspace mit vollem Funktionsumfang')}
</div> </div>
</div> </div>
</label> </label>
@ -85,7 +85,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontWeight: 500 }}> <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> </label>
<input <input
type="text" value={mandateName} 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)', padding: '10px 20px', borderRadius: '6px', border: '1px solid var(--border, #d1d5db)',
background: 'transparent', cursor: 'pointer', background: 'transparent', cursor: 'pointer',
}}> }}>
Abbrechen {t('Abbrechen')}
</button> </button>
<button onClick={_handleSubmit} disabled={loading} <button onClick={_handleSubmit} disabled={loading}
style={{ style={{

View file

@ -77,8 +77,7 @@ export function _toBackendProviders(
return _resolveProviders(selection, allowedProviders); return _resolveProviders(selection, allowedProviders);
} }
// Provider display names const _PROVIDER_LABEL_KEYS: Record<string, string> = {
const PROVIDER_LABELS: Record<string, string> = {
anthropic: 'Anthropic (Claude)', anthropic: 'Anthropic (Claude)',
openai: 'OpenAI (GPT)', openai: 'OpenAI (GPT)',
mistral: 'Mistral (Le Chat)', mistral: 'Mistral (Le Chat)',
@ -88,6 +87,11 @@ const PROVIDER_LABELS: Record<string, string> = {
internal: 'Internal', 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> = { const PROVIDER_ICONS: Record<string, string> = {
anthropic: '🤖', anthropic: '🤖',
openai: '💬', openai: '💬',
@ -115,10 +119,11 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
onChange, onChange,
disabled = false, disabled = false,
className, className,
label = 'AI-Provider', label,
showLabel = true, showLabel = true,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const resolvedLabel = label ?? t('AI-Provider');
const { allowedProviders, loadAllowedProviders, loading } = useBilling(); const { allowedProviders, loadAllowedProviders, loading } = useBilling();
useEffect(() => { useEffect(() => {
@ -130,13 +135,13 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
const providerOptions = useMemo(() => { const providerOptions = useMemo(() => {
return allowedProviders.map((provider) => ({ return allowedProviders.map((provider) => ({
value: provider, value: provider,
label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`, label: `${PROVIDER_ICONS[provider] || '🔌'} ${_providerLabel(provider, t)}`,
})); }));
}, [allowedProviders]); }, [allowedProviders, t]);
return ( return (
<div className={`${styles.providerSelect} ${className || ''}`}> <div className={`${styles.providerSelect} ${className || ''}`}>
{showLabel && <label className={styles.label}>{label}</label>} {showLabel && <label className={styles.label}>{resolvedLabel}</label>}
<select <select
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
@ -174,12 +179,13 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
onChange, onChange,
disabled = false, disabled = false,
className, className,
label = 'AI-Provider', label,
showLabel = true, showLabel = true,
defaultExpanded = false, defaultExpanded = false,
excludeByDefault = [], excludeByDefault = [],
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const resolvedLabel = label ?? t('AI-Provider');
const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false); const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -264,13 +270,13 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
}, [effectiveSelection, noneSelected]); }, [effectiveSelection, noneSelected]);
const summaryHint = useMemo(() => { const summaryHint = useMemo(() => {
if (noneSelected) return 'Kein Provider ausgewählt'; if (noneSelected) return t('Kein Provider ausgewählt');
if (allSelected) return 'Alle Provider aktiv (dynamisch)'; if (allSelected) return t('Alle Provider aktiv (dynamisch)');
if (selection.include.includes(PROVIDER_ALL)) { 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`; return t('{n} von {total} Provider', { n: String(effectiveSelection.length), total: String(allowedProviders.length) });
}, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders]); }, [noneSelected, allSelected, selection, effectiveSelection, allowedProviders, t]);
return ( return (
<div <div
@ -289,7 +295,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
{isExpanded && ( {isExpanded && (
<div className={styles.dropdownContent}> <div className={styles.dropdownContent}>
{showLabel && <div className={styles.dropdownHeader}>{label}</div>} {showLabel && <div className={styles.dropdownHeader}>{resolvedLabel}</div>}
<div className={styles.selectActions}> <div className={styles.selectActions}>
<button <button
@ -298,7 +304,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
disabled={disabled} disabled={disabled}
className={`${styles.actionButton} ${allSelected ? styles.active : ''}`} className={`${styles.actionButton} ${allSelected ? styles.active : ''}`}
> >
Alle {t('Alle')}
</button> </button>
</div> </div>
@ -319,7 +325,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
/> />
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span> <span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
<span className={styles.providerName}> <span className={styles.providerName}>
{PROVIDER_LABELS[provider] || provider} {_providerLabel(provider, t)}
</span> </span>
</label> </label>
))} ))}
@ -355,7 +361,7 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
<div className={`${styles.providerBadges} ${className || ''}`}> <div className={`${styles.providerBadges} ${className || ''}`}>
{providers.map((provider) => ( {providers.map((provider) => (
<span key={provider} className={styles.badge}> <span key={provider} className={styles.badge}>
{PROVIDER_ICONS[provider] || '🔌'} {PROVIDER_LABELS[provider] || provider} {PROVIDER_ICONS[provider] || '🔌'} {_providerLabel(provider, t)}
</span> </span>
))} ))}
</div> </div>

View file

@ -80,20 +80,20 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
return ( return (
<div className={styles.preview}> <div className={styles.preview}>
<div className={styles.previewHeader}> <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> <button className={styles.closeButton} onClick={onClose}></button>
</div> </div>
<div className={styles.previewContent}> <div className={styles.previewContent}>
<div className={styles.previewSection}> <div className={styles.previewSection}>
<h5>Scope</h5> <h5>{t('Scope')}</h5>
<ul className={styles.previewList}> <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.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> </ul>
</div> </div>
<div className={styles.previewSection}> <div className={styles.previewSection}>
<h5>Rollen ({data.roles.length})</h5> <h5>{t('Rollen ({count})', { count: String(data.roles.length) })}</h5>
<ul className={styles.previewList}> <ul className={styles.previewList}>
{data.roles.slice(0, 5).map((role, i) => ( {data.roles.slice(0, 5).map((role, i) => (
<li key={i}> <li key={i}>
@ -102,21 +102,21 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
</li> </li>
))} ))}
{data.roles.length > 5 && ( {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> </ul>
</div> </div>
<div className={styles.previewSection}> <div className={styles.previewSection}>
<h5>Regeln ({data.accessRules.length})</h5> <h5>{t('Regeln ({count})', { count: String(data.accessRules.length) })}</h5>
<ul className={styles.previewList}> <ul className={styles.previewList}>
{data.accessRules.slice(0, 5).map((rule, i) => ( {data.accessRules.slice(0, 5).map((rule, i) => (
<li key={i}> <li key={i}>
<span className={styles.contextBadge}>{rule.context}</span> <span className={styles.contextBadge}>{rule.context}</span>
<code>{rule.item || '(global)'}</code> <code>{rule.item || t('(global)')}</code>
</li> </li>
))} ))}
{data.accessRules.length > 5 && ( {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> </ul>
</div> </div>
@ -154,7 +154,7 @@ const ImportResult: React.FC<ImportResultProps> = ({ result, onClose }) => {
</div> </div>
<div className={styles.resultContent}> <div className={styles.resultContent}>
<ul className={styles.resultStats}> <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 erstellt')}</strong> {result.rolesCreated}</li>
<li><strong>{t('Rollen aktualisiert')}</strong> {result.rolesUpdated}</li> <li><strong>{t('Rollen aktualisiert')}</strong> {result.rolesUpdated}</li>
<li><strong>{t('Regeln erstellt')}</strong> {result.rulesCreated}</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) { if (result.success && result.data) {
setImportData(result.data); setImportData(result.data);
} else { } else {
setParseError(result.error || 'Fehler beim Parsen'); setParseError(result.error || t('Fehler beim Parsen'));
setImportData(null); setImportData(null);
} }
}; };
@ -289,13 +289,14 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<FaFileExport className={styles.sectionIcon} /> <FaFileExport className={styles.sectionIcon} />
<h3 className={styles.sectionTitle}>Export</h3> <h3 className={styles.sectionTitle}>{t('Export')}</h3>
</div> </div>
<div className={styles.sectionContent}> <div className={styles.sectionContent}>
<p className={styles.sectionDescription}> <p className={styles.sectionDescription}>
Exportiert alle Rollen und Berechtigungen {t('Exportiert alle Rollen und Berechtigungen')}{' '}
{isGlobal ? ' der globalen Templates' : ` des Mandanten "${mandateName || mandateId}"`} {isGlobal ? t('der globalen Templates') : t('des Mandanten "{name}"', { name: String(mandateName || mandateId || '') })}
{featureCode ? ` für Feature "${featureCode}"` : ''} als JSON-Datei. {featureCode ? <> {t('für Feature "{code}"', { code: featureCode })}</> : null}{' '}
{t('als JSON-Datei.')}
</p> </p>
<button <button
className={styles.primaryButton} className={styles.primaryButton}
@ -304,11 +305,11 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
> >
{exporting ? ( {exporting ? (
<> <>
<FaSpinner className="spinning" /> Exportieren... <FaSpinner className="spinning" /> {t('Exportieren...')}
</> </>
) : ( ) : (
<> <>
<FaDownload /> RBAC exportieren <FaDownload /> {t('RBAC exportieren')}
</> </>
)} )}
</button> </button>
@ -319,7 +320,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<FaFileImport className={styles.sectionIcon} /> <FaFileImport className={styles.sectionIcon} />
<h3 className={styles.sectionTitle}>Import</h3> <h3 className={styles.sectionTitle}>{t('Import')}</h3>
</div> </div>
<div className={styles.sectionContent}> <div className={styles.sectionContent}>
{/* File Upload */} {/* File Upload */}
@ -368,14 +369,14 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
<div className={styles.importInfo}> <div className={styles.importInfo}>
<div className={styles.importStats}> <div className={styles.importStats}>
<span><strong>{t('Rollen')}</strong> {importData.roles.length}</span> <span><strong>{t('Rollen')}</strong> {importData.roles.length}</span>
<span><strong>Regeln:</strong> {importData.accessRules.length}</span> <span><strong>{t('Regeln:')}</strong> {importData.accessRules.length}</span>
<span><strong>Quelle:</strong> {importData.scope.type}</span> <span><strong>{t('Quelle:')}</strong> {importData.scope.type}</span>
</div> </div>
<button <button
className={styles.previewButton} className={styles.previewButton}
onClick={() => setShowPreview(true)} onClick={() => setShowPreview(true)}
> >
<FaEye /> Vorschau <FaEye /> {t('Vorschau')}
</button> </button>
</div> </div>
)} )}
@ -383,7 +384,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
{/* Import Mode Selection */} {/* Import Mode Selection */}
{importData && ( {importData && (
<div className={styles.importModeSection}> <div className={styles.importModeSection}>
<h4 className={styles.importModeTitle}>Import-Modus</h4> <h4 className={styles.importModeTitle}>{t('Import-Modus')}</h4>
<div className={styles.importModes}> <div className={styles.importModes}>
{importModes.map(mode => ( {importModes.map(mode => (
<label <label
@ -416,11 +417,11 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
> >
{importing ? ( {importing ? (
<> <>
<FaSpinner className="spinning" /> Importieren... <FaSpinner className="spinning" /> {t('Importieren...')}
</> </>
) : ( ) : (
<> <>
<FaUpload /> RBAC importieren <FaUpload /> {t('RBAC importieren')}
</> </>
)} )}
</button> </button>
@ -430,7 +431,8 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
{importMode === 'replace' && importData && ( {importMode === 'replace' && importData && (
<div className={styles.warningMessage}> <div className={styles.warningMessage}>
<FaExclamationTriangle /> <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>
)} )}
</div> </div>

View file

@ -95,7 +95,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
setAutocompleteError(null); // Clear any previous errors on success setAutocompleteError(null); // Clear any previous errors on success
} catch (err: any) { } catch (err: any) {
console.error('❌ [AddressAutocomplete] Error in performSearch:', err); 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); setAutocompleteError(errorMessage);
setSuggestions([]); setSuggestions([]);
setShowSuggestions(true); // Show dropdown to display error setShowSuggestions(true); // Show dropdown to display error
@ -103,7 +103,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [minQueryLength, maxSuggestions]); }, [minQueryLength, maxSuggestions, t]);
// Handle input change with debouncing // Handle input change with debouncing
const handleInputChange = useCallback((newValue: string) => { const handleInputChange = useCallback((newValue: string) => {
@ -286,7 +286,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
)} )}
{!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && ( {!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
<li className={styles.suggestionItem}> <li className={styles.suggestionItem}>
<span className={styles.noResultsText}>{t('no addresses found')}</span> <span className={styles.noResultsText}>{t('Keine Adressen gefunden')}</span>
</li> </li>
)} )}
{!isLoading && suggestions.map((suggestion, index) => ( {!isLoading && suggestions.map((suggestion, index) => (

View file

@ -30,7 +30,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
<div className={styles.bauvorschriftenHeader} onClick={() => setIsExpanded(!isExpanded)}> <div className={styles.bauvorschriftenHeader} onClick={() => setIsExpanded(!isExpanded)}>
<h4 className={styles.subSectionTitle}> <h4 className={styles.subSectionTitle}>
<FaRuler style={{ marginRight: '8px', display: 'inline' }} /> <FaRuler style={{ marginRight: '8px', display: 'inline' }} />
Bauvorschriften - {bauvorschriften.zonenbezeichnung} {t('Bauvorschriften')} {bauvorschriften.zonenbezeichnung}
</h4> </h4>
<button className={styles.expandButton}> <button className={styles.expandButton}>
{isExpanded ? <FaChevronUp /> : <FaChevronDown />} {isExpanded ? <FaChevronUp /> : <FaChevronDown />}
@ -48,7 +48,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
)} )}
{bauvorschriften.vollgeschosse !== undefined && bauvorschriften.vollgeschosse !== null && ( {bauvorschriften.vollgeschosse !== undefined && bauvorschriften.vollgeschosse !== null && (
<div className={styles.bauvorschriftItem}> <div className={styles.bauvorschriftItem}>
<span className={styles.label}>Vollgeschosse:</span> <span className={styles.label}>{t('Vollgeschosse')}</span>
<span className={styles.value}>{bauvorschriften.vollgeschosse}</span> <span className={styles.value}>{bauvorschriften.vollgeschosse}</span>
</div> </div>
)} )}
@ -60,7 +60,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
)} )}
{bauvorschriften.grenzabstand !== undefined && bauvorschriften.grenzabstand !== null && ( {bauvorschriften.grenzabstand !== undefined && bauvorschriften.grenzabstand !== null && (
<div className={styles.bauvorschriftItem}> <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> <span className={styles.value}>{bauvorschriften.grenzabstand} m</span>
</div> </div>
)} )}
@ -93,7 +93,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
className={styles.sourceLinkButton} className={styles.sourceLinkButton}
> >
<FaFilePdf style={{ marginRight: '8px' }} /> <FaFilePdf style={{ marginRight: '8px' }} />
Nutzungsplan öffnen {t('Nutzungsplan öffnen')}
</a> </a>
</div> </div>
)} )}
@ -101,7 +101,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
{bauvorschriften.extraktionsDatum && ( {bauvorschriften.extraktionsDatum && (
<div className={styles.bauvorschriftenFooter}> <div className={styles.bauvorschriftenFooter}>
<span className={styles.lastUpdated}> <span className={styles.lastUpdated}>
Extrahiert: {new Date(bauvorschriften.extraktionsDatum).toLocaleString('de-CH')} {t('Extrahiert')}: {new Date(bauvorschriften.extraktionsDatum).toLocaleString('de-CH')}
</span> </span>
</div> </div>
)} )}

View file

@ -8,7 +8,7 @@ import { useLanguage } from '../../../../providers/language/LanguageContext';
const CreateButton: React.FC<CreateButtonProps> = ({ const CreateButton: React.FC<CreateButtonProps> = ({
onCreate, onCreate,
fields, fields,
popupTitle = 'Create New Item', popupTitle = 'Neues Element erstellen',
popupSize = 'medium', popupSize = 'medium',
disabled = false, disabled = false,
loading = false, loading = false,
@ -131,7 +131,7 @@ const CreateButton: React.FC<CreateButtonProps> = ({
} catch (error: any) { } catch (error: any) {
console.error('Creation failed:', error); console.error('Creation failed:', error);
if (onError) { if (onError) {
onError(error.message || 'Creation failed'); onError(error.message || t('Erstellung fehlgeschlagen'));
} }
} finally { } finally {
setIsCreating(false); setIsCreating(false);

View file

@ -1,6 +1,7 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { UploadButtonProps } from '../ButtonTypes'; import { UploadButtonProps } from '../ButtonTypes';
import Button from '../Button'; import Button from '../Button';
import { useLanguage } from '../../../../providers/language/LanguageContext';
const UploadButton: React.FC<UploadButtonProps> = ({ const UploadButton: React.FC<UploadButtonProps> = ({
onUpload, onUpload,
@ -71,7 +72,7 @@ const UploadButton: React.FC<UploadButtonProps> = ({
{isUploading && ( {isUploading && (
<div className="spinnerIcon" style={{ marginRight: '8px' }} /> <div className="spinnerIcon" style={{ marginRight: '8px' }} />
)} )}
{children || (isUploading ? 'Uploading...' : 'Upload File')} {children || (isUploading ? t('Wird hochgeladen…') : t('Datei hochladen'))}
</Button> </Button>
<input <input

View file

@ -67,7 +67,7 @@ export function ConnectedFilesList({
previewingFiles = new Set(), previewingFiles = new Set(),
removingFiles = new Set(), removingFiles = new Set(),
workflowId: _workflowId, workflowId: _workflowId,
emptyMessage = 'No files connected to this workflow' emptyMessage = 'Keine Dateien mit diesem Workflow verbunden'
}: ConnectedFilesListProps) { }: ConnectedFilesListProps) {
const { t } = useLanguage(); const { t } = useLanguage();
// Combine workflow files and pending files, deduplicating by fileId // Combine workflow files and pending files, deduplicating by fileId
@ -253,7 +253,7 @@ export function ConnectedFilesList({
)} )}
{isPendingFile && ( {isPendingFile && (
<span style={{ fontSize: '0.75rem', color: '#4CAF50', fontWeight: 500 }}> <span style={{ fontSize: '0.75rem', color: '#4CAF50', fontWeight: 500 }}>
Attached {t('Angehängt')}
</span> </span>
)} )}
</div> </div>

View file

@ -3,6 +3,7 @@ import { IconType } from 'react-icons';
import { IoChevronDown, IoClose } from 'react-icons/io5'; import { IoChevronDown, IoClose } from 'react-icons/io5';
import styles from './DropdownSelect.module.css'; import styles from './DropdownSelect.module.css';
import { ButtonVariant, ButtonSize } from '../Button/ButtonTypes'; import { ButtonVariant, ButtonSize } from '../Button/ButtonTypes';
import { useLanguage } from '../../../providers/language/LanguageContext';
export interface DropdownSelectItem<T = any> { export interface DropdownSelectItem<T = any> {
id: string | number; id: string | number;
@ -37,8 +38,8 @@ function DropdownSelect<T = any>({
items = [], items = [],
selectedItemId, selectedItemId,
onSelect, onSelect,
placeholder = 'Select an item', placeholder,
emptyMessage = 'No items available', emptyMessage,
headerText, headerText,
variant = 'primary', variant = 'primary',
size = 'md', size = 'md',
@ -54,6 +55,9 @@ function DropdownSelect<T = any>({
showClearButton = true, showClearButton = true,
clearButtonLabel clearButtonLabel
}: DropdownSelectProps<T>) { }: 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 [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
@ -116,7 +120,7 @@ function DropdownSelect<T = any>({
return ( return (
<> <>
<div className={styles.buttonSpinner} /> <div className={styles.buttonSpinner} />
<span>{placeholder}</span> <span>{resolvedPlaceholder}</span>
</> </>
); );
} }
@ -134,7 +138,7 @@ function DropdownSelect<T = any>({
return ( return (
<> <>
{Icon && <Icon className={styles.buttonIcon} />} {Icon && <Icon className={styles.buttonIcon} />}
<span className={styles.buttonText}>{placeholder}</span> <span className={styles.buttonText}>{resolvedPlaceholder}</span>
<IoChevronDown className={`${styles.chevronIcon} ${isOpen ? styles.chevronOpen : ''}`} /> <IoChevronDown className={`${styles.chevronIcon} ${isOpen ? styles.chevronOpen : ''}`} />
</> </>
); );
@ -153,7 +157,7 @@ function DropdownSelect<T = any>({
className={buttonClasses} className={buttonClasses}
onClick={handleClear} onClick={handleClear}
disabled={disabled || loading} disabled={disabled || loading}
title={clearButtonLabel || `Clear selection: ${selectedItem.label}`} title={clearButtonLabel || t('Auswahl aufheben: {label}', { label: selectedItem.label })}
> >
{Icon && <Icon className={styles.buttonIcon} />} {Icon && <Icon className={styles.buttonIcon} />}
<span className={styles.buttonText}> <span className={styles.buttonText}>
@ -197,7 +201,7 @@ function DropdownSelect<T = any>({
{items.length === 0 ? ( {items.length === 0 ? (
<div className={styles.dropdownEmpty}> <div className={styles.dropdownEmpty}>
{emptyMessage} {resolvedEmptyMessage}
</div> </div>
) : ( ) : (
<div className={styles.dropdownItems}> <div className={styles.dropdownItems}>

View file

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Button, TextField } from '../index'; import { Button, TextField } from '../index';
import { FaLocationArrow } from 'react-icons/fa'; import { FaLocationArrow } from 'react-icons/fa';
import styles from './LocationInput.module.css'; import styles from './LocationInput.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
export interface LocationInputProps { export interface LocationInputProps {
value: string; value: string;
@ -20,12 +21,15 @@ const LocationInput: React.FC<LocationInputProps> = ({
onChange, onChange,
onUseCurrentLocation, onUseCurrentLocation,
isGettingLocation = false, isGettingLocation = false,
placeholder = 'Kanton, Gemeinde, Adresse oder Parzelle', placeholder,
label = 'Standort', label,
error, error,
helperText, helperText,
disabled = false 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 [isRequestingLocation, setIsRequestingLocation] = useState(false);
const handleUseCurrentLocation = async () => { const handleUseCurrentLocation = async () => {
@ -62,7 +66,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
loading={isGettingLocation || isRequestingLocation} loading={isGettingLocation || isRequestingLocation}
className={styles.locationButton} className={styles.locationButton}
> >
Meine Position verwenden {t('Meine Position verwenden')}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -3,6 +3,7 @@ import { LogProps } from './LogTypes';
import { AutoScroll } from '../AutoScroll'; import { AutoScroll } from '../AutoScroll';
import { formatUnixTimestamp } from '../../../utils/time'; import { formatUnixTimestamp } from '../../../utils/time';
import styles from './Log.module.css'; import styles from './Log.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
// Helper to get status badge class // Helper to get status badge class
const getStatusBadgeClass = (status?: string | null): string => { const getStatusBadgeClass = (status?: string | null): string => {
@ -22,11 +23,13 @@ const getStatusBadgeClass = (status?: string | null): string => {
const Log: React.FC<LogProps> = ({ const Log: React.FC<LogProps> = ({
className = '', className = '',
emptyMessage = 'No log information available', emptyMessage = 'Keine Log-Informationen verfügbar',
dashboardTree, dashboardTree,
onToggleOperationExpanded, onToggleOperationExpanded,
getChildOperations getChildOperations
}) => { }) => {
const { t } = useLanguage();
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage;
const formatLogTimestamp = (timestamp: number): string => { const formatLogTimestamp = (timestamp: number): string => {
try { try {
const formatted = formatUnixTimestamp(timestamp, undefined, { 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 // 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) // Use latest message as status tag (updates with each poll)
const latestMessage = operation.latestMessage || ''; const latestMessage = operation.latestMessage || '';
const operationStatus = operation.latestStatus || 'running'; const operationStatus = operation.latestStatus || 'running';
@ -137,7 +140,7 @@ const Log: React.FC<LogProps> = ({
<button <button
className={styles.expandButton} className={styles.expandButton}
onClick={() => onToggleOperationExpanded?.(operationId)} 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}`}> <span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}>
@ -243,7 +246,7 @@ const Log: React.FC<LogProps> = ({
if (dashboardTree.rootOperations.length === 0) { if (dashboardTree.rootOperations.length === 0) {
return ( return (
<div className={styles.emptyState}>{emptyMessage}</div> <div className={styles.emptyState}>{resolvedEmptyMessage}</div>
); );
} }
@ -260,7 +263,7 @@ const Log: React.FC<LogProps> = ({
if (!hasDashboardLogs) { if (!hasDashboardLogs) {
return ( return (
<div className={`${styles.logContainer} ${className}`}> <div className={`${styles.logContainer} ${className}`}>
<div className={styles.emptyState}>{emptyMessage}</div> <div className={styles.emptyState}>{resolvedEmptyMessage}</div>
</div> </div>
); );
} }

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useLanguage } from '../../../../providers/language/LanguageContext';
import { Message } from '../../Messages/MessagesTypes'; import { Message } from '../../Messages/MessagesTypes';
import { DocumentItem, MessageMetadata, ActionInfo } from '../../Messages/MessageParts'; import { DocumentItem, MessageMetadata, ActionInfo } from '../../Messages/MessageParts';
import { WorkflowFile } from '../../../../hooks/usePlayground'; import { WorkflowFile } from '../../../../hooks/usePlayground';
@ -41,6 +42,7 @@ export const LogMessage: React.FC<LogMessageProps> = ({
downloadingFiles, downloadingFiles,
workflowId workflowId
}) => { }) => {
const { t } = useLanguage();
return ( return (
<div className={`${logStyles.logMessage} ${styles.messageWrapper}`}> <div className={`${logStyles.logMessage} ${styles.messageWrapper}`}>
{/* Metadata row */} {/* Metadata row */}
@ -58,7 +60,7 @@ export const LogMessage: React.FC<LogMessageProps> = ({
{message.summary && message.summary !== message.message && ( {message.summary && message.summary !== message.message && (
<div className={logStyles.logSummary}> <div className={logStyles.logSummary}>
<strong>Summary:</strong> {message.summary} <strong>{t('Zusammenfassung')}:</strong> {message.summary}
</div> </div>
)} )}

View file

@ -223,9 +223,9 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
const polygon = L.polygon(latLngs, SELECTED_STYLE); const polygon = L.polygon(latLngs, SELECTED_STYLE);
polygon.bindPopup(` polygon.bindPopup(`
<div> <div>
<strong>Parzelle ${parcel.number || parcel.id}</strong><br/> <strong>${t('Parzelle')} ${parcel.number || parcel.id}</strong><br/>
${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''} ${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''}
<em>{t('Ausgewählt')}</em> <em>${t('Ausgewählt')}</em>
</div> </div>
`); `);
if (onParcelClick) { if (onParcelClick) {
@ -235,7 +235,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
layersRef.current.push(polygon); layersRef.current.push(polygon);
}); });
} }
}, [parcels, combinedOutline, onParcelClick]); }, [parcels, combinedOutline, onParcelClick, t]);
// Handle map clicks // Handle map clicks
useEffect(() => { useEffect(() => {
@ -361,7 +361,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} /> <div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
{parcels.length === 0 && !center && ( {parcels.length === 0 && !center && (
<div className={styles.emptyStateOverlay}> <div className={styles.emptyStateOverlay}>
<p>{emptyMessage}</p> <p>{typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage}</p>
</div> </div>
)} )}
{showWfsParcels && isWfsLoading && ( {showWfsParcels && isWfsLoading && (

View file

@ -150,7 +150,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
{/* Summary if different from message */} {/* Summary if different from message */}
{message.summary && message.summary !== message.message && ( {message.summary && message.summary !== message.message && (
<div className={styles.messageSummary}> <div className={styles.messageSummary}>
<strong>Summary:</strong> {message.summary} <strong>{t('Zusammenfassung')}:</strong> {message.summary}
</div> </div>
)} )}

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { MessagesProps } from './MessagesTypes'; import { MessagesProps } from './MessagesTypes';
import { ChatMessage } from './ChatMessages/ChatMessage'; import { ChatMessage } from './ChatMessages/ChatMessage';
import { LogMessage } from '../Log/LogMessage/LogMessage'; import { LogMessage } from '../Log/LogMessage/LogMessage';
@ -15,7 +16,7 @@ const Messages: React.FC<MessagesProps> = ({
showProgress = true, showProgress = true,
renderMessage, renderMessage,
renderDocument, renderDocument,
emptyMessage = 'No messages yet', emptyMessage = 'Noch keine Nachrichten',
onFileDelete, onFileDelete,
onFileRemove, onFileRemove,
onFileView, onFileView,
@ -28,10 +29,12 @@ const Messages: React.FC<MessagesProps> = ({
onMessageDelete, onMessageDelete,
deletingMessages deletingMessages
}) => { }) => {
const { t } = useLanguage();
const resolvedEmptyMessage = typeof emptyMessage === 'string' ? t(emptyMessage, emptyMessage) : emptyMessage;
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
return ( return (
<div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}> <div className={`${styles.messagesContainer} ${styles.emptyContainer} ${className}`}>
<div className={styles.emptyState}>{emptyMessage}</div> <div className={styles.emptyState}>{resolvedEmptyMessage}</div>
</div> </div>
); );
} }

View file

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { FaChevronDown, FaChevronUp, FaFilePdf, FaInfoCircle } from 'react-icons/fa'; import { FaChevronDown, FaChevronUp, FaFilePdf, FaInfoCircle } from 'react-icons/fa';
import { UrlContentPreview } from '../../ContentPreview'; import { UrlContentPreview } from '../../ContentPreview';
import styles from './OerebSection.module.css'; import styles from './OerebSection.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
export interface OerebData { export interface OerebData {
extract_url?: string; extract_url?: string;
@ -21,6 +22,7 @@ export interface OerebSectionProps {
} }
export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => { export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
const { t } = useLanguage();
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(true);
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const restrictions = oereb.restrictions || []; const restrictions = oereb.restrictions || [];
@ -34,7 +36,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
<div className={styles.oerebHeader} onClick={() => setIsExpanded(!isExpanded)}> <div className={styles.oerebHeader} onClick={() => setIsExpanded(!isExpanded)}>
<h4 className={styles.subSectionTitle}> <h4 className={styles.subSectionTitle}>
<FaInfoCircle style={{ marginRight: '8px', display: 'inline' }} /> <FaInfoCircle style={{ marginRight: '8px', display: 'inline' }} />
ÖREB-Kataster {t('ÖREB-Kataster')}
{restrictions.length > 0 && ( {restrictions.length > 0 && (
<span className={styles.badge}>({restrictions.length})</span> <span className={styles.badge}>({restrictions.length})</span>
)} )}
@ -54,7 +56,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
type="button" type="button"
> >
<FaFilePdf style={{ marginRight: '8px' }} /> <FaFilePdf style={{ marginRight: '8px' }} />
Vollständigen ÖREB-Auszug öffnen (PDF) {t('Vollständigen ÖREB-Auszug öffnen (PDF)')}
</button> </button>
</div> </div>
)} )}
@ -68,14 +70,14 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
{restriction.law_status && ( {restriction.law_status && (
<span className={styles.restrictionStatus}> <span className={styles.restrictionStatus}>
{restriction.law_status === 'inKraft' || restriction.law_status === 'inForce' {restriction.law_status === 'inKraft' || restriction.law_status === 'inForce'
? 'In Kraft' ? t('In Kraft')
: restriction.law_status} : restriction.law_status}
</span> </span>
)} )}
</div> </div>
{restriction.type && ( {restriction.type && (
<div className={styles.restrictionType}> <div className={styles.restrictionType}>
<span className={styles.label}>Typ:</span> <span className={styles.label}>{t('Typ:')}</span>
<span className={styles.value}>{restriction.type}</span> <span className={styles.value}>{restriction.type}</span>
</div> </div>
)} )}
@ -86,7 +88,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
)} )}
{restriction.documents && restriction.documents.length > 0 && ( {restriction.documents && restriction.documents.length > 0 && (
<div className={styles.restrictionDocuments}> <div className={styles.restrictionDocuments}>
<span className={styles.label}>Dokumente:</span> <span className={styles.label}>{t('Dokumente:')}</span>
{restriction.documents.map((doc, docIndex) => ( {restriction.documents.map((doc, docIndex) => (
<a <a
key={docIndex} key={docIndex}
@ -95,7 +97,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
rel="noopener noreferrer" rel="noopener noreferrer"
className={styles.documentLink} className={styles.documentLink}
> >
Dokument {docIndex + 1} {t('Dokument {nr}', { nr: String(docIndex + 1) })}
</a> </a>
))} ))}
</div> </div>
@ -105,14 +107,14 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
</div> </div>
) : ( ) : (
<div className={styles.noRestrictions}> <div className={styles.noRestrictions}>
Keine öffentlich-rechtlichen Beschränkungen gefunden. {t('Keine öffentlich-rechtlichen Beschränkungen gefunden.')}
</div> </div>
)} )}
{oereb.last_updated && ( {oereb.last_updated && (
<div className={styles.oerebFooter}> <div className={styles.oerebFooter}>
<span className={styles.lastUpdated}> <span className={styles.lastUpdated}>
Aktualisiert: {new Date(oereb.last_updated).toLocaleString('de-CH')} {t('Aktualisiert')}: {new Date(oereb.last_updated).toLocaleString('de-CH')}
</span> </span>
</div> </div>
)} )}
@ -124,7 +126,7 @@ export const OerebSection: React.FC<OerebSectionProps> = ({ oereb }) => {
isOpen={isPreviewOpen} isOpen={isPreviewOpen}
onClose={() => setIsPreviewOpen(false)} onClose={() => setIsPreviewOpen(false)}
url={oereb.extract_url} url={oereb.extract_url}
fileName="ÖREB-Auszug.pdf" fileName={t('ÖREB-Auszug.pdf')}
mimeType="application/pdf" mimeType="application/pdf"
/> />
)} )}

View file

@ -147,12 +147,12 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
} catch (e: any) { } catch (e: any) {
setDocsError(prev => ({ setDocsError(prev => ({
...prev, ...prev,
[parcelId]: e?.response?.data?.detail || e?.message || 'Fehler beim Laden' [parcelId]: e?.response?.data?.detail || e?.message || t('Fehler beim Laden')
})); }));
} finally { } finally {
setDocsLoading(prev => ({ ...prev, [parcelId]: false })); setDocsLoading(prev => ({ ...prev, [parcelId]: false }));
} }
}, [instanceId]); }, [instanceId, t]);
const runExtraction = useCallback(async ( const runExtraction = useCallback(async (
parcelId: string, parcelId: string,
@ -174,12 +174,12 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
} catch (e: any) { } catch (e: any) {
setExtractError(prev => ({ setExtractError(prev => ({
...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 { } finally {
setExtractLoading(prev => ({ ...prev, [parcelId]: false })); setExtractLoading(prev => ({ ...prev, [parcelId]: false }));
} }
}, [instanceId]); }, [instanceId, t]);
useEffect(() => { useEffect(() => {
if (!isOpen || !instanceId) return; if (!isOpen || !instanceId) return;
@ -218,7 +218,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
className={styles.panel} className={styles.panel}
> >
<div className={styles.header}> <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}> <button className={styles.closeButton} onClick={onClose}>
<FaTimes /> <FaTimes />
</button> </button>
@ -248,7 +248,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
return [ return [
<section key={`h-${bz.bauzone}`} className={styles.bauzoneSection}> <section key={`h-${bz.bauzone}`} className={styles.bauzoneSection}>
<h4 className={styles.bauzoneTitle}> <h4 className={styles.bauzoneTitle}>
Bauzone {bz.bauzone} {t('Bauzone')} {bz.bauzone}
{bz.area_m2 != null && ( {bz.area_m2 != null && (
<span className={styles.bauzoneArea}> {bz.area_m2.toFixed(2)} m²</span> <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}> <section key={parcelData.parcel.id} className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}> <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> </h3>
{onRemoveParcel && ( {onRemoveParcel && (
<button <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) && ( {instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
<div className={styles.bzoSection}> <div className={styles.bzoSection}>
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4> <h4 className={styles.subSectionTitle}>{t('Bauzonenverordnung')}</h4>
{docsLoading[parcelData.parcel.id] && ( {docsLoading[parcelData.parcel.id] && (
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p> <p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
)} )}
@ -364,7 +364,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
})} })}
title={t('Dokument öffnen')} title={t('Dokument öffnen')}
> >
<FaEye /> Öffnen <FaEye /> {t('Öffnen')}
</button> </button>
</div> </div>
))} ))}
@ -387,7 +387,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
) : ( ) : (
<FaFileAlt /> <FaFileAlt />
)} )}
Inhalt extrahieren (LangGraph) {t('Inhalt extrahieren (LangGraph)')}
</button> </button>
)} )}
{extractError[parcelData.parcel.id] && ( {extractError[parcelData.parcel.id] && (
@ -470,7 +470,9 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<div className={styles.infoItem}> <div className={styles.infoItem}>
<span className={styles.label}>Zone:</span> <span className={styles.label}>Zone:</span>
<span className={styles.value}> <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 // Extract zone types from zone array
const zoneTypes = parcelData.parcel.zone const zoneTypes = parcelData.parcel.zone
@ -509,7 +511,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
rel="noopener noreferrer" rel="noopener noreferrer"
className={styles.link} className={styles.link}
> >
Link öffnen {t('Link öffnen')}
</a> </a>
</div> </div>
)} )}
@ -526,7 +528,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<section key={parcelData.parcel.id || index} className={styles.section}> <section key={parcelData.parcel.id || index} className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}> <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> </h3>
{onRemoveParcel && ( {onRemoveParcel && (
<button <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) && ( {instanceId && parcelData.parcel.municipality_name && parcelData.parcel.bauzone && (docsLoading[parcelData.parcel.id] || (parcelDocs[parcelData.parcel.id] || []).length > 0) && (
<div className={styles.bzoSection}> <div className={styles.bzoSection}>
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4> <h4 className={styles.subSectionTitle}>{t('Bauzonenverordnung')}</h4>
{docsLoading[parcelData.parcel.id] && ( {docsLoading[parcelData.parcel.id] && (
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p> <p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
)} )}
@ -630,7 +632,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
})} })}
title={t('Dokument öffnen')} title={t('Dokument öffnen')}
> >
<FaEye /> Öffnen <FaEye /> {t('Öffnen')}
</button> </button>
</div> </div>
))} ))}
@ -653,7 +655,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
) : ( ) : (
<FaFileAlt /> <FaFileAlt />
)} )}
Inhalt extrahieren (LangGraph) {t('Inhalt extrahieren (LangGraph)')}
</button> </button>
)} )}
{extractError[parcelData.parcel.id] && ( {extractError[parcelData.parcel.id] && (
@ -736,7 +738,9 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<div className={styles.infoItem}> <div className={styles.infoItem}>
<span className={styles.label}>Zone:</span> <span className={styles.label}>Zone:</span>
<span className={styles.value}> <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 const zoneTypes = parcelData.parcel.zone
.map((z: any) => { .map((z: any) => {
@ -773,7 +777,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
rel="noopener noreferrer" rel="noopener noreferrer"
className={styles.link} className={styles.link}
> >
Link öffnen {t('Link öffnen')}
</a> </a>
</div> </div>
)} )}

View file

@ -46,6 +46,17 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
latestStats latestStats
}) => { }) => {
const { t } = useLanguage(); 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 // Use workflow status and round from API response, fallback to extracting from logs
const workflowStatus = useMemo(() => { const workflowStatus = useMemo(() => {
if (workflowStatusFromApi) { if (workflowStatusFromApi) {
@ -73,11 +84,11 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
)} )}
{workflowStatus.status && ( {workflowStatus.status && (
<span className={styles.statusBadge} data-status={workflowStatus.status}> <span className={styles.statusBadge} data-status={workflowStatus.status}>
{workflowStatus.status.charAt(0).toUpperCase() + workflowStatus.status.slice(1)} {statusLabel(workflowStatus.status)}
</span> </span>
)} )}
{workflowStatus.round !== null && ( {workflowStatus.round !== null && (
<span className={styles.roundBadge}>Round {workflowStatus.round}</span> <span className={styles.roundBadge}>{t('Runde {nr}', { nr: String(workflowStatus.round) })}</span>
)} )}
</div> </div>
@ -85,7 +96,7 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
{latestStats && latestStats.priceCHF !== undefined && ( {latestStats && latestStats.priceCHF !== undefined && (
<div className={styles.statsContainer}> <div className={styles.statsContainer}>
<div className={styles.statItem}> <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> <span className={styles.statValue}>{_formatCurrency(latestStats.priceCHF)}</span>
</div> </div>
</div> </div>

View file

@ -103,7 +103,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
} }
groupMap.get(fiId)!.chats.push({ groupMap.get(fiId)!.chats.push({
id: wf.id, 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, updatedAt: wf.updatedAt || wf.lastActivity || wf.startedAt,
lastMessageAt: wf.lastMessageAt, lastMessageAt: wf.lastMessageAt,
featureInstanceId: fiId, featureInstanceId: fiId,
@ -132,7 +132,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [context.instanceId]); }, [context.instanceId, t]);
useEffect(() => { _loadChats(); }, [_loadChats]); useEffect(() => { _loadChats(); }, [_loadChats]);
@ -283,7 +283,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
<button <button
className={styles.actionBtn} className={styles.actionBtn}
onClick={(e) => { e.stopPropagation(); _startEditing(chat); }} onClick={(e) => { e.stopPropagation(); _startEditing(chat); }}
title="Umbenennen" title={t('Umbenennen')}
> >
</button> </button>
@ -292,7 +292,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
<button <button
className={styles.actionBtn} className={styles.actionBtn}
onClick={(e) => { e.stopPropagation(); _restoreChat(chat.id); }} onClick={(e) => { e.stopPropagation(); _restoreChat(chat.id); }}
title="Wiederherstellen" title={t('Wiederherstellen')}
> >
</button> </button>
@ -300,7 +300,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
<button <button
className={styles.actionBtn} className={styles.actionBtn}
onClick={(e) => { e.stopPropagation(); _archiveChat(chat.id); }} onClick={(e) => { e.stopPropagation(); _archiveChat(chat.id); }}
title="Archivieren" title={t('Archivieren')}
> >
📦 📦
</button> </button>
@ -323,10 +323,10 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
const _featureCodeLabel = (code: string): string => { const _featureCodeLabel = (code: string): string => {
const labels: Record<string, string> = { const labels: Record<string, string> = {
workspace: 'AI Workspace', workspace: t('KI-Arbeitsbereich'),
commcoach: 'CommCoach', commcoach: t('CommCoach'),
trustee: 'Trustee', trustee: t('Trustee'),
automation: 'Automation', automation: t('Automation'),
}; };
return labels[code] || code; return labels[code] || code;
}; };
@ -351,7 +351,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
<button <button
className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`} className={`${styles.modeToggle} ${flatMode ? styles.modeActive : ''}`}
onClick={() => setFlatMode(!flatMode)} onClick={() => setFlatMode(!flatMode)}
title={flatMode ? 'Baumansicht' : 'Listenansicht'} title={flatMode ? t('Baumansicht') : t('Listenansicht')}
> >
{flatMode ? '\uD83C\uDF33' : '\uD83D\uDCCB'} {flatMode ? '\uD83C\uDF33' : '\uD83D\uDCCB'}
</button> </button>
@ -362,13 +362,13 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
className={`${styles.filterTab} ${filter === 'active' ? styles.filterTabActive : ''}`} className={`${styles.filterTab} ${filter === 'active' ? styles.filterTabActive : ''}`}
onClick={() => setFilter('active')} onClick={() => setFilter('active')}
> >
Aktiv ({_activeCount}) {t('Aktiv')} ({_activeCount})
</button> </button>
<button <button
className={`${styles.filterTab} ${filter === 'archived' ? styles.filterTabActive : ''}`} className={`${styles.filterTab} ${filter === 'archived' ? styles.filterTabActive : ''}`}
onClick={() => setFilter('archived')} onClick={() => setFilter('archived')}
> >
Archiv ({_archivedCount}) {t('Archiv')} ({_archivedCount})
</button> </button>
</div> </div>

View file

@ -250,12 +250,12 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 13, fontWeight: 600, color: '#F25843', fontSize: 13, fontWeight: 600, color: '#F25843',
}}> }}>
Dateien hier ablegen {t('Dateien hier ablegen')}
</div> </div>
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '4px 8px' }}> <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 }}> <div style={{ display: 'flex', gap: 4 }}>
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
@ -331,10 +331,10 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
</div> </div>
<div className={styles.legend}> <div className={styles.legend}>
<span>{'\uD83D\uDC64'} Persönlich</span> <span>{'\uD83D\uDC64'} {t('Persönlich')}</span>
<span>{'\uD83D\uDC65'} Instanz</span> <span>{'\uD83D\uDC65'} {t('Instanz')}</span>
<span>{'\uD83C\uDFE2'} Mandant</span> <span>{'\uD83C\uDFE2'} {t('Mandant')}</span>
<span>{'\uD83D\uDD12'} Neutralisiert</span> <span>{'\uD83D\uDD12'} {t('Neutralisiert')}</span>
</div> </div>
</div> </div>
); );

View file

@ -85,7 +85,7 @@ interface MandateGroupNode {
interface FeatureTableNode { interface FeatureTableNode {
objectKey: string; objectKey: string;
tableName: string; tableName: string;
label: Record<string, string>; label: string;
fields: string[]; fields: string[];
isParent?: boolean; isParent?: boolean;
parentTable?: string; parentTable?: string;
@ -185,13 +185,6 @@ const _SCOPE_ICONS: Record<string, string> = {
global: '\uD83C\uDF10', global: '\uD83C\uDF10',
}; };
const _SCOPE_LABELS: Record<string, string> = {
personal: 'Personal',
featureInstance: 'Feature Instance',
mandate: 'Mandate',
global: 'Global',
};
function _nextScope(current: string): string { function _nextScope(current: string): string {
const idx = _SCOPE_ORDER.indexOf(current); const idx = _SCOPE_ORDER.indexOf(current);
if (idx === -1) return _SCOPE_ORDER[0]; if (idx === -1) return _SCOPE_ORDER[0];
@ -348,6 +341,14 @@ function _Spinner(): React.ReactElement {
const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => { const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
const { t } = useLanguage(); 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; const instanceId = context.instanceId;
/* ── Active sources (fetched internally) ── */ /* ── Active sources (fetched internally) ── */
@ -663,7 +664,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
featureCode: node.featureCode, featureCode: node.featureCode,
tableName: table.tableName, tableName: table.tableName,
objectKey: table.objectKey, objectKey: table.objectKey,
label: table.label?.en || table.label?.de || table.tableName, label: table.label || table.tableName,
}); });
_fetchFeatureDataSources(); _fetchFeatureDataSources();
onSourcesChanged?.(); onSourcesChanged?.();
@ -764,7 +765,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName); const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName);
if (parentTable) { 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`, { await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
featureInstanceId: node.featureInstanceId, featureInstanceId: node.featureInstanceId,
featureCode: node.featureCode, featureCode: node.featureCode,
@ -776,7 +777,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
} }
for (const child of childTables) { 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`, { await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
featureInstanceId: node.featureInstanceId, featureInstanceId: node.featureInstanceId,
featureCode: node.featureCode, featureCode: node.featureCode,
@ -813,7 +814,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{dataSources.length > 0 && ( {dataSources.length > 0 && (
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}> <div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
Active Personal Sources {t('Aktive persönliche Quellen')}
</div> </div>
{[...dataSources].sort((a, b) => { {[...dataSources].sort((a, b) => {
const aKey = `${a.sourceType}|${a.label || a.path || ''}`; 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', background: 'none', border: 'none', cursor: 'pointer',
fontSize: 13, padding: '0 2px', lineHeight: 1, 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} {_SCOPE_ICONS[ds.scope] || _SCOPE_ICONS.personal}
</button> </button>
@ -874,7 +875,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{/* ── Browse Sources header ── */} {/* ── Browse Sources header ── */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}> <span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
Browse Sources {t('Quellen durchsuchen')}
</span> </span>
<button <button
onClick={_loadConnections} onClick={_loadConnections}
@ -888,13 +889,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{/* ── Browse Sources tree ── */} {/* ── Browse Sources tree ── */}
{loadingRoot && tree.length === 0 && ( {loadingRoot && tree.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}> <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
Loading connections... {t('Verbindungen werden geladen…')}
</div> </div>
)} )}
{!loadingRoot && tree.length === 0 && ( {!loadingRoot && tree.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}> <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
No active connections found. {t('Keine aktiven Verbindungen.')}
</div> </div>
)} )}
@ -917,7 +918,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{featureDataSources.length > 0 && ( {featureDataSources.length > 0 && (
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}> <div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
Active Feature Sources {t('Aktive Feature-Quellen')}
</div> </div>
{(() => { {(() => {
const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || '')); 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 <button
onClick={() => _cycleFeatureScope(fds)} onClick={() => _cycleFeatureScope(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1 }} 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} {_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
</button> </button>
@ -1022,7 +1023,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button <button
onClick={() => _cycleFeatureScope(fds)} onClick={() => _cycleFeatureScope(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1 }} 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} {_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
</button> </button>
@ -1053,7 +1054,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{/* ── Feature Data header ── */} {/* ── Feature Data header ── */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}> <span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
Feature Data {t('Feature-Daten')}
</span> </span>
<button <button
onClick={_loadFeatureConnections} onClick={_loadFeatureConnections}
@ -1067,13 +1068,13 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
{/* ── Feature Data tree ── */} {/* ── Feature Data tree ── */}
{loadingFeatures && featureTree.length === 0 && ( {loadingFeatures && featureTree.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}> <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
Loading feature instances... {t('Feature-Instanzen werden geladen…')}
</div> </div>
)} )}
{!loadingFeatures && featureTree.length === 0 && ( {!loadingFeatures && featureTree.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}> <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
No feature instances found. {t('Keine Feature-Instanzen gefunden.')}
</div> </div>
)} )}
@ -1169,7 +1170,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
}} }}
title={t('Als Datenquelle hinzufügen')} title={t('Als Datenquelle hinzufügen')}
> >
{isAdding ? '...' : '+ Add'} {isAdding ? '...' : `+ ${t('Hinzufügen')}`}
</button> </button>
)} )}
{canAdd && alreadyAdded && ( {canAdd && alreadyAdded && (
@ -1197,7 +1198,7 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
{node.expanded && node.children && node.children.length === 0 && !node.loading && ( {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' }}> <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>
)} )}
</div> </div>
@ -1299,6 +1300,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded, onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
expandedParentGroups, loadingParentGroup, addingParentKey, expandedParentGroups, loadingParentGroup, addingParentKey,
}) => { }) => {
const { t } = useLanguage();
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const chevron = node.expanded ? '\u25BE' : '\u25B8'; const chevron = node.expanded ? '\u25BE' : '\u25B8';
@ -1329,7 +1331,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
{node.label} {node.label}
</span> </span>
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}> <span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
{node.tableCount} tables {node.tableCount} {t('Tabellen')}
</span> </span>
</div> </div>
@ -1342,7 +1344,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
const isGroupLoading = loadingParentGroup === groupKey; const isGroupLoading = loadingParentGroup === groupKey;
const records = node.parentRecords[pt.tableName]; const records = node.parentRecords[pt.tableName];
const childTables = (node.tables || []).filter(t => t.parentTable === 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 ( return (
<_ParentGroupView <_ParentGroupView
@ -1380,7 +1382,7 @@ const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
{node.expanded && node.tables && node.tables.length === 0 && !node.loading && ( {node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
<div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}> <div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}>
(no tables) {t('(keine Tabellen)')}
</div> </div>
)} )}
</div> </div>
@ -1402,7 +1404,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const tableLabel = table.label?.en || table.label?.de || table.tableName; const tableLabel = table.label || table.tableName;
return ( return (
<div <div
@ -1433,7 +1435,7 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
}} }}
title={t('Als Feature-Datenquelle hinzufügen')} title={t('Als Feature-Datenquelle hinzufügen')}
> >
{isAdding ? '...' : '+ Add'} {isAdding ? '...' : `+ ${t('Hinzufügen')}`}
</button> </button>
)} )}
{isAdded && ( {isAdded && (
@ -1467,6 +1469,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables, featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables,
onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey, onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey,
}) => { }) => {
const { t } = useLanguage();
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const chevron = expanded ? '\u25BE' : '\u25B8'; const chevron = expanded ? '\u25BE' : '\u25B8';
@ -1493,7 +1496,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
</span> </span>
{childTables.length > 0 && ( {childTables.length > 0 && (
<span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}> <span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
+{childTables.length} tables +{childTables.length} {t('Tabellen')}
</span> </span>
)} )}
</div> </div>
@ -1518,7 +1521,7 @@ const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
{expanded && records && records.length === 0 && !loading && ( {expanded && records && records.length === 0 && !loading && (
<div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}> <div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}>
(no records) {t('(keine Einträge)')}
</div> </div>
)} )}
</div> </div>
@ -1580,7 +1583,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
}} }}
title={t('Alle Tabellen für diese Quelle hinzufügen')} title={t('Alle Tabellen für diese Quelle hinzufügen')}
> >
{isAdding ? '...' : '+ Add'} {isAdding ? '...' : `+ ${t('Hinzufügen')}`}
</button> </button>
)} )}
{isAdded && ( {isAdded && (
@ -1593,7 +1596,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
{record.expanded && ( {record.expanded && (
<div style={{ paddingLeft: 64 }}> <div style={{ paddingLeft: 64 }}>
{childTables.map(ct => { {childTables.map(ct => {
const ctLabel = ct.label?.en || ct.label?.de || ct.tableName; const ctLabel = ct.label || ct.tableName;
return ( return (
<div key={ct.objectKey} style={{ <div key={ct.objectKey} style={{
display: 'flex', alignItems: 'center', gap: 4, display: 'flex', alignItems: 'center', gap: 4,

View file

@ -31,7 +31,7 @@ interface UnifiedDataBarProps {
} }
const _TAB_KEYS: Record<UdbTab, string> = { const _TAB_KEYS: Record<UdbTab, string> = {
chats: 'Chats', chats: 'Chatverläufe',
files: 'Dateien', files: 'Dateien',
sources: 'Quellen', sources: 'Quellen',
}; };

View file

@ -83,8 +83,6 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
// Helper function to resolve node name // Helper function to resolve node name
const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => { const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => {
const { t } = useLanguage();
if (page) { if (page) {
return resolveLanguageText(page.name, t); return resolveLanguageText(page.name, t);
} }
@ -460,7 +458,7 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
setSidebarItems(items); setSidebarItems(items);
} catch (err) { } catch (err) {
console.error('❌ SidebarProvider: Error refreshing sidebar:', 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 { } finally {
setLoading(false); setLoading(false);
} }

View file

@ -34,16 +34,11 @@ export interface GenericPageData {
type TranslationFunction = (key: string, fallback?: string) => string; 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( export function resolveLanguageText(
name: string | { de?: string; en?: string }, name: string,
t: TranslationFunction t: TranslationFunction
): string { ): string {
if (typeof name === 'string') { return t(name);
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 ?? '';
} }

View file

@ -7,6 +7,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import api from '../api'; import api from '../api';
import { useLanguage } from '../providers/language/LanguageContext';
// ============================================================================= // =============================================================================
// TYPES // TYPES
@ -78,6 +79,7 @@ interface SaveResult {
// ============================================================================= // =============================================================================
export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) { export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac', mandateId?: string) {
const { t } = useLanguage();
const [rules, setRules] = useState<AccessRule[]>([]); const [rules, setRules] = useState<AccessRule[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -113,14 +115,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac'
setRules(fetchedRules); setRules(fetchedRules);
return fetchedRules; return fetchedRules;
} catch (err: any) { } 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); setError(errorMsg);
console.error('Error fetching rules:', err); console.error('Error fetching rules:', err);
return []; return [];
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [roleId, apiBasePath, isInstanceApi, getHeaders]); }, [roleId, apiBasePath, isInstanceApi, getHeaders, t]);
/** /**
* Save all rules for the role * Save all rules for the role
@ -196,14 +198,14 @@ export function useAccessRules(roleId: string, apiBasePath: string = '/api/rbac'
return { success: true }; return { success: true };
} catch (err: any) { } 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); setError(errorMsg);
console.error('Error saving rules:', err); console.error('Error saving rules:', err);
return { success: false, error: errorMsg }; return { success: false, error: errorMsg };
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders]); }, [roleId, apiBasePath, isInstanceApi, fetchRules, getHeaders, t]);
/** /**
* Get rules grouped by context * Get rules grouped by context

View file

@ -188,15 +188,10 @@ export function useMandates() {
// Handle options - can be array or string reference // Handle options - can be array or string reference
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attrOptions; optionsReference = attrOptions;
@ -206,15 +201,10 @@ export function useMandates() {
// Handle options - can be array or string reference // Handle options - can be array or string reference
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attrOptions; optionsReference = attrOptions;
@ -327,15 +317,10 @@ export function useMandates() {
fieldType = 'enum'; fieldType = 'enum';
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
optionsReference = attrOptions; optionsReference = attrOptions;
} }
@ -343,15 +328,10 @@ export function useMandates() {
fieldType = 'multiselect'; fieldType = 'multiselect';
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
optionsReference = attrOptions; optionsReference = attrOptions;
} }

View file

@ -229,15 +229,10 @@ export function useRbacRoles() {
// Handle options - can be array or string reference // Handle options - can be array or string reference
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attrOptions; optionsReference = attrOptions;
@ -247,15 +242,10 @@ export function useRbacRoles() {
// Handle options - can be array or string reference // Handle options - can be array or string reference
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attrOptions; optionsReference = attrOptions;
@ -368,15 +358,10 @@ export function useRbacRoles() {
fieldType = 'enum'; fieldType = 'enum';
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
optionsReference = attrOptions; optionsReference = attrOptions;
} }
@ -384,15 +369,10 @@ export function useRbacRoles() {
fieldType = 'multiselect'; fieldType = 'multiselect';
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
optionsReference = attrOptions; optionsReference = attrOptions;
} }

View file

@ -205,15 +205,10 @@ export function useRbacRules() {
// Handle options - can be array or string reference // Handle options - can be array or string reference
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attrOptions; optionsReference = attrOptions;
@ -223,15 +218,10 @@ export function useRbacRules() {
// Handle options - can be array or string reference // Handle options - can be array or string reference
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attrOptions; optionsReference = attrOptions;
@ -344,15 +334,10 @@ export function useRbacRules() {
fieldType = 'enum'; fieldType = 'enum';
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
optionsReference = attrOptions; optionsReference = attrOptions;
} }
@ -360,15 +345,10 @@ export function useRbacRules() {
fieldType = 'multiselect'; fieldType = 'multiselect';
const attrOptions = (attr as any).options; const attrOptions = (attr as any).options;
if (Array.isArray(attrOptions)) { if (Array.isArray(attrOptions)) {
options = attrOptions.map((opt: any) => { options = attrOptions.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attrOptions === 'string') { } else if (typeof attrOptions === 'string') {
optionsReference = attrOptions; optionsReference = attrOptions;
} }

View file

@ -26,7 +26,7 @@ export interface PaginationMetadata {
export interface Feature { export interface Feature {
code: string; code: string;
label: string | { [key: string]: string }; label: string;
icon?: string; icon?: string;
enabled?: boolean; enabled?: boolean;
} }
@ -62,7 +62,7 @@ export interface FeatureAccessUser {
export interface FeatureInstanceRole { export interface FeatureInstanceRole {
id: string; id: string;
roleLabel: string; roleLabel: string;
description?: { [key: string]: string }; description?: string;
featureCode?: string; featureCode?: string;
isSystemRole?: boolean; isSystemRole?: boolean;
} }
@ -312,7 +312,7 @@ export function useFeatureAccess() {
name: string; name: string;
features: Array<{ features: Array<{
code: string; code: string;
label: string | { [key: string]: string }; label: string;
instances: Array<{ instances: Array<{
id: string; id: string;
featureCode: string; featureCode: string;

View file

@ -12,7 +12,7 @@ import api from '../api';
export interface Role { export interface Role {
id: string; id: string;
roleLabel: string; roleLabel: string;
description?: string | { [key: string]: string }; description?: string;
mandateId?: string; mandateId?: string;
featureInstanceId?: string; featureInstanceId?: string;
featureCode?: string; featureCode?: string;
@ -24,7 +24,7 @@ export interface Role {
export interface RoleCreate { export interface RoleCreate {
roleLabel: string; roleLabel: string;
description?: string | { [key: string]: string }; description?: string;
mandateId?: string; mandateId?: string;
featureInstanceId?: string; featureInstanceId?: string;
featureCode?: string; featureCode?: string;
@ -32,7 +32,7 @@ export interface RoleCreate {
export interface RoleUpdate { export interface RoleUpdate {
roleLabel?: string; roleLabel?: string;
description?: string | { [key: string]: string }; description?: string;
mandateId?: string | null; mandateId?: string | null;
} }

View file

@ -33,7 +33,7 @@ export interface AttributeDefinition {
description?: string; description?: string;
required?: boolean; required?: boolean;
default?: any; default?: any;
options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; options?: Array<{ value: string | number; label: string }> | string;
sortable?: boolean; sortable?: boolean;
filterable?: boolean; filterable?: boolean;
searchable?: boolean; searchable?: boolean;

View file

@ -20,7 +20,7 @@ export type { Prompt, AttributeDefinition, PaginationParams };
// Re-export AttributeOption for backward compatibility // Re-export AttributeOption for backward compatibility
export interface AttributeOption { export interface AttributeOption {
value: string | number; value: string | number;
label: string | { [key: string]: string }; label: string;
} }
// Prompts list hook // Prompts list hook
@ -185,15 +185,10 @@ export function usePrompts() {
fieldType = 'enum'; fieldType = 'enum';
// Handle options - can be array or string reference // Handle options - can be array or string reference
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { options = attr.options.map(opt => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: String(opt.label ?? opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }
@ -201,15 +196,10 @@ export function usePrompts() {
fieldType = 'multiselect'; fieldType = 'multiselect';
// Handle options - can be array or string reference // Handle options - can be array or string reference
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { options = attr.options.map(opt => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: String(opt.label ?? opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -25,7 +25,7 @@ export interface RbacExportScope {
export interface RbacExportRole { export interface RbacExportRole {
roleLabel: string; roleLabel: string;
description?: { [key: string]: string }; description?: string;
featureCode?: string; featureCode?: string;
} }

View file

@ -181,7 +181,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = (attr.options as any[]).map((opt: any) => ({ options = (attr.options as any[]).map((opt: any) => ({
value: opt.value, 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 (typeof attr.options === 'string') optionsReference = attr.options;
} else if (attr.type === 'multiselect') { } else if (attr.type === 'multiselect') {
@ -189,7 +189,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = (attr.options as any[]).map((opt: any) => ({ options = (attr.options as any[]).map((opt: any) => ({
value: opt.value, 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 (typeof attr.options === 'string') optionsReference = attr.options;
} else if (attr.type === 'textarea') fieldType = 'textarea'; } else if (attr.type === 'textarea') fieldType = 'textarea';
@ -224,7 +224,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = (attr.options as any[]).map((opt: any) => ({ options = (attr.options as any[]).map((opt: any) => ({
value: opt.value, 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 (typeof attr.options === 'string') optionsReference = attr.options;
} else if (attr.type === 'multiselect') { } else if (attr.type === 'multiselect') {
@ -232,7 +232,7 @@ function _createRealEstateEntityHook<T extends { id: string }>(config: RealEstat
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = (attr.options as any[]).map((opt: any) => ({ options = (attr.options as any[]).map((opt: any) => ({
value: opt.value, 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 (typeof attr.options === 'string') optionsReference = attr.options;
} else if (attr.type === 'textarea') fieldType = 'textarea'; } else if (attr.type === 'textarea') fieldType = 'textarea';

View file

@ -237,24 +237,18 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value, label: opt.label || String(opt.value)
? opt.label }));
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
return { value: opt.value, label: labelValue };
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }
} else if (attr.type === 'multiselect') { } else if (attr.type === 'multiselect') {
fieldType = 'multiselect'; fieldType = 'multiselect';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value, label: opt.label || String(opt.value)
? opt.label }));
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
return { value: opt.value, label: labelValue };
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }
@ -303,24 +297,18 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value, label: opt.label || String(opt.value)
? opt.label }));
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
return { value: opt.value, label: labelValue };
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }
} else if (attr.type === 'multiselect') { } else if (attr.type === 'multiselect') {
fieldType = 'multiselect'; fieldType = 'multiselect';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value, label: opt.label || String(opt.value)
? opt.label }));
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value);
return { value: opt.value, label: labelValue };
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -193,15 +193,10 @@ export function useTrusteeAccess() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -193,15 +193,10 @@ export function useTrusteeContracts() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -194,15 +194,10 @@ export function useTrusteeDocuments() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -191,30 +191,20 @@ export function useTrusteeOrganisations() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }
} else if (attr.type === 'multiselect') { } else if (attr.type === 'multiselect') {
fieldType = 'multiselect'; fieldType = 'multiselect';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -181,15 +181,10 @@ export function useTrusteePositionDocuments() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -207,15 +207,10 @@ export function useTrusteePositions() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -201,15 +201,10 @@ export function useTrusteeRoles() {
} else if (attr.type === 'select') { } else if (attr.type === 'select') {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map((opt: any) => { options = attr.options.map((opt: any) => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -51,7 +51,7 @@ export interface UserMandateResponse {
export interface Role { export interface Role {
id: string; id: string;
roleLabel: string; roleLabel: string;
description?: string | { [key: string]: string }; description?: string;
mandateId?: string; mandateId?: string;
featureInstanceId?: string; featureInstanceId?: string;
featureCode?: string; featureCode?: string;
@ -60,7 +60,7 @@ export interface Role {
export interface Mandate { export interface Mandate {
id: string; id: string;
name: string | { [key: string]: string }; name: string;
label?: string; label?: string;
code?: string; code?: string;
language?: string; language?: string;

View file

@ -253,7 +253,7 @@ export function useCurrentUser() {
// Re-export AttributeOption for backward compatibility // Re-export AttributeOption for backward compatibility
export interface AttributeOption { export interface AttributeOption {
value: string | number; value: string | number;
label: string | { [key: string]: string }; label: string;
} }
// Organization users hook (list, update, delete) - following prompts/workflows pattern // Organization users hook (list, update, delete) - following prompts/workflows pattern
@ -436,15 +436,10 @@ export function useOrgUsers() {
fieldType = 'enum'; fieldType = 'enum';
// Handle options - can be array or string reference // Handle options - can be array or string reference
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { options = attr.options.map(opt => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attr.options; optionsReference = attr.options;
@ -453,15 +448,10 @@ export function useOrgUsers() {
fieldType = 'multiselect'; fieldType = 'multiselect';
// Handle options - can be array or string reference // Handle options - can be array or string reference
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { options = attr.options.map(opt => ({
const labelValue = typeof opt.label === 'string' value: opt.value,
? opt.label label: opt.label || String(opt.value)
: opt.label?.en || opt.label?.[Object.keys(opt.label)[0]] || String(opt.value); }));
return {
value: opt.value,
label: labelValue
};
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
// Options reference (e.g., "user.role", "auth.authority") // Options reference (e.g., "user.role", "auth.authority")
optionsReference = attr.options; optionsReference = attr.options;
@ -582,12 +572,9 @@ export function useOrgUsers() {
fieldType = 'enum'; fieldType = 'enum';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { 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 { return {
value: opt.value, value: opt.value,
label: labelValue label: opt.label || String(opt.value)
}; };
}); });
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
@ -596,15 +583,10 @@ export function useOrgUsers() {
} else if (attrType === 'multiselect') { } else if (attrType === 'multiselect') {
fieldType = 'multiselect'; fieldType = 'multiselect';
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { 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, value: opt.value,
label: labelValue label: opt.label || String(opt.value)
}; }));
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -93,7 +93,7 @@ export type { AttributeDefinition } from '../api/attributesApi';
// Attribute option interface (from backend) // Attribute option interface (from backend)
export interface AttributeOption { export interface AttributeOption {
value: string | number; value: string | number;
label: string | { [key: string]: string }; // Can be string or object with language keys label: string;
} }
// Pagination parameters // Pagination parameters
@ -300,15 +300,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
fieldType = 'enum'; fieldType = 'enum';
// Handle options - can be array or string reference // Handle options - can be array or string reference
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { 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, value: opt.value,
label: labelValue label: opt.label || String(opt.value)
}; }));
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }
@ -316,15 +311,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
fieldType = 'multiselect'; fieldType = 'multiselect';
// Handle options - can be array or string reference // Handle options - can be array or string reference
if (Array.isArray(attr.options)) { if (Array.isArray(attr.options)) {
options = attr.options.map(opt => { 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, value: opt.value,
label: labelValue label: opt.label || String(opt.value)
}; }));
});
} else if (typeof attr.options === 'string') { } else if (typeof attr.options === 'string') {
optionsReference = attr.options; optionsReference = attr.options;
} }

View file

@ -5,10 +5,11 @@
* Stellt den Instanz-Kontext bereit und rendert Sidebar + Content. * 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 { Outlet, Navigate, useLocation } from 'react-router-dom';
import { useCurrentInstance } from '../hooks/useCurrentInstance'; import { useCurrentInstance } from '../hooks/useCurrentInstance';
import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore'; import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore';
import useNavigation from '../hooks/useNavigation';
import styles from './FeatureLayout.module.css'; import styles from './FeatureLayout.module.css';
import { useLanguage } from '../providers/language/LanguageContext'; import { useLanguage } from '../providers/language/LanguageContext';
@ -44,7 +45,7 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
<h2>{t('Zugriff nicht möglich')}</h2> <h2>{t('Zugriff nicht möglich')}</h2>
<p>{message}</p> <p>{message}</p>
<a href={returnPath} className={styles.errorLink}> <a href={returnPath} className={styles.errorLink}>
Zurück zur Übersicht {t('Zurück zur Übersicht')}
</a> </a>
</div> </div>
); );
@ -65,10 +66,27 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
* Bei Erfolg: Rendert <Outlet /> für die verschachtelten Routes * Bei Erfolg: Rendert <Outlet /> für die verschachtelten Routes
*/ */
export const FeatureLayout: React.FC = () => { export const FeatureLayout: React.FC = () => {
const { t } = useLanguage();
const location = useLocation(); const location = useLocation();
const initialized = useFeaturesInitialized(); const initialized = useFeaturesInitialized();
const loading = useFeaturesLoading(); 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 // Warten bis Features geladen sind
if (!initialized || loading || isLoading) { if (!initialized || loading || isLoading) {
@ -86,7 +104,7 @@ export const FeatureLayout: React.FC = () => {
return ( return (
<ErrorScreen <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 mit Instanz-Info */}
<header className={styles.featureHeader}> <header className={styles.featureHeader}>
<div className={styles.breadcrumb}> <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.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.separator}>/</span>
<span className={styles.instanceName}>{instance?.instanceLabel}</span> <span className={styles.instanceName}>{navLabels?.instance || instance?.instanceLabel}</span>
</div> </div>
<div className={styles.roleIndicator}> <div className={styles.roleIndicator}>
<span className={styles.roleBadge}>{instance?.userRoles?.join(', ') || '-'}</span> <span className={styles.roleBadge}>{instance?.userRoles?.join(', ') || '-'}</span>
@ -133,6 +151,7 @@ export const ProtectedFeatureRoute: React.FC<ProtectedFeatureRouteProps> = ({
requiredView, requiredView,
children, children,
}) => { }) => {
const { t } = useLanguage();
const { instance, isValid } = useCurrentInstance(); const { instance, isValid } = useCurrentInstance();
if (!isValid) { if (!isValid) {
@ -146,7 +165,7 @@ export const ProtectedFeatureRoute: React.FC<ProtectedFeatureRouteProps> = ({
if (!hasViewAccess) { if (!hasViewAccess) {
return ( return (
<ErrorScreen <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}`} returnPath={`/mandates/${instance?.mandateId}/${instance?.featureCode}/${instance?.id}`}
/> />
); );

View file

@ -84,13 +84,13 @@ const MainLayoutInner: React.FC = () => {
<nav className={styles.navigation}> <nav className={styles.navigation}>
{loading && ( {loading && (
<div className={styles.loadingNav}> <div className={styles.loadingNav}>
Lade Navigation... {t('Lade Navigation…')}
</div> </div>
)} )}
{error && ( {error && (
<div className={styles.errorNav}> <div className={styles.errorNav}>
Fehler: {error} {t('Fehler')}: {error}
</div> </div>
)} )}

View file

@ -170,8 +170,8 @@ export const AutomationsDashboardPage: React.FC = () => {
sortable: true, sortable: true,
filterable: true, filterable: true,
formatter: (v: string) => ( formatter: (v: string) => (
<span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600, textTransform: 'capitalize' }}> <span style={{ color: _STATUS_COLORS[v] || 'inherit', fontWeight: 600 }}>
{v} {t(v === 'completed' ? 'Abgeschlossen' : v === 'failed' ? 'Fehlgeschlagen' : v === 'running' ? 'Laufend' : v)}
</span> </span>
), ),
}, },

View file

@ -85,7 +85,12 @@ export const DashboardPage: React.FC = () => {
<h1>{t('Übersicht')}</h1> <h1>{t('Übersicht')}</h1>
{totalInstances > 0 && ( {totalInstances > 0 && (
<p className={styles.subtitle}> <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> </p>
)} )}
</header> </header>

View file

@ -73,12 +73,12 @@ const ChatworkflowDashboard: React.FC = () => {
const ChatworkflowRuns: React.FC = () => { const ChatworkflowRuns: React.FC = () => {
const { t } = useLanguage(); 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 ChatworkflowFiles: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
return <PlaceholderView title={t('Dateien')} description="Workflow-Dateien" />; return <PlaceholderView title={t('Dateien')} description={t('Workflow-Dateien')} />;
}; };
// Chatbot Views // Chatbot Views

View file

@ -68,7 +68,7 @@ export const GDPRPage: React.FC = () => {
} catch (error: any) { } catch (error: any) {
console.error('Failed to load GDPR consent info:', error); console.error('Failed to load GDPR consent info:', error);
if (isActive) { if (isActive) {
setConsentError('Consent information could not be loaded.'); setConsentError(t('Einwilligungsinformationen konnten nicht geladen werden.'));
} }
} finally { } finally {
if (isActive) { if (isActive) {
@ -82,7 +82,7 @@ export const GDPRPage: React.FC = () => {
return () => { return () => {
isActive = false; isActive = false;
}; };
}, []); }, [t]);
const handleDataExport = async () => { const handleDataExport = async () => {
if (isActionLocked) return; if (isActionLocked) return;
@ -91,10 +91,10 @@ export const GDPRPage: React.FC = () => {
try { try {
const response = await api.get('/api/user/me/data-export'); const response = await api.get('/api/user/me/data-export');
downloadJson(response.data, 'gdpr-data-export.json'); downloadJson(response.data, 'gdpr-data-export.json');
setActionMessage({ type: 'success', text: 'Data export downloaded.' }); setActionMessage({ type: 'success', text: t('Datenexport heruntergeladen.') });
} catch (error: any) { } catch (error: any) {
console.error('GDPR export failed:', error); 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 { } finally {
setIsExporting(false); setIsExporting(false);
} }
@ -109,10 +109,10 @@ export const GDPRPage: React.FC = () => {
headers: { Accept: 'application/ld+json' } headers: { Accept: 'application/ld+json' }
}); });
downloadJson(response.data, 'gdpr-data-portability.json', '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) { } catch (error: any) {
console.error('GDPR portability export failed:', error); 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 { } finally {
setIsPortabilityExporting(false); setIsPortabilityExporting(false);
} }
@ -121,7 +121,7 @@ export const GDPRPage: React.FC = () => {
const handleDeleteAccount = async () => { const handleDeleteAccount = async () => {
setActionMessage(null); setActionMessage(null);
if (deleteConfirmText !== 'LOESCHEN') { 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; return;
} }
@ -132,11 +132,11 @@ export const GDPRPage: React.FC = () => {
sessionStorage.removeItem('auth_authority'); sessionStorage.removeItem('auth_authority');
clearUserDataCache(); clearUserDataCache();
setIsDeleted(true); 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'); window.location.replace('/login');
} catch (error: any) { } catch (error: any) {
console.error('GDPR deletion failed:', error); 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 { } finally {
setIsDeleting(false); setIsDeleting(false);
} }
@ -148,14 +148,14 @@ export const GDPRPage: React.FC = () => {
<div> <div>
<h1 className={styles.title}> <h1 className={styles.title}>
<FaShieldAlt className={styles.titleIcon} /> <FaShieldAlt className={styles.titleIcon} />
GDPR / Privacy {t('DSGVO / Datenschutz')}
</h1> </h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Manage your personal data exports and account deletion. {t('Verwalten Sie Ihre personenbezogenen Datenexporte und Kontolöschung.')}
</p> </p>
</div> </div>
<Link to="/settings" className={styles.backLink}> <Link to="/settings" className={styles.backLink}>
Back to Settings {t('Zurück zu Einstellungen')}
</Link> </Link>
</header> </header>
@ -174,12 +174,12 @@ export const GDPRPage: React.FC = () => {
{isExporting ? ( {isExporting ? (
<span className={styles.buttonSpinner}> <span className={styles.buttonSpinner}>
<FaSpinner /> <FaSpinner />
Exporting... {t('Export wird erstellt…')}
</span> </span>
) : ( ) : (
<> <>
<FaDownload /> <FaDownload />
Export data {t('Daten exportieren')}
</> </>
)} )}
</button> </button>
@ -196,12 +196,12 @@ export const GDPRPage: React.FC = () => {
{isPortabilityExporting ? ( {isPortabilityExporting ? (
<span className={styles.buttonSpinner}> <span className={styles.buttonSpinner}>
<FaSpinner /> <FaSpinner />
Exporting... {t('Export wird erstellt…')}
</span> </span>
) : ( ) : (
<> <>
<FaFileExport /> <FaFileExport />
Export portable data {t('Portabler Datenexport')}
</> </>
)} )}
</button> </button>
@ -217,13 +217,15 @@ export const GDPRPage: React.FC = () => {
disabled={isActionLocked} disabled={isActionLocked}
> >
<FaTrash /> <FaTrash />
Start deletion {t('Löschung starten')}
</button> </button>
)} )}
{showDeleteConfirm && ( {showDeleteConfirm && (
<div className={styles.deleteConfirm}> <div className={styles.deleteConfirm}>
<p className={styles.deleteWarning}> <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> </p>
<input <input
className={styles.deleteInput} className={styles.deleteInput}
@ -241,7 +243,7 @@ export const GDPRPage: React.FC = () => {
}} }}
disabled={isDeleting} disabled={isDeleting}
> >
Cancel {t('Abbrechen')}
</button> </button>
<button <button
className={styles.dangerButton} className={styles.dangerButton}
@ -251,12 +253,12 @@ export const GDPRPage: React.FC = () => {
{isDeleting ? ( {isDeleting ? (
<span className={styles.buttonSpinner}> <span className={styles.buttonSpinner}>
<FaSpinner /> <FaSpinner />
Deleting... {t('Wird gelöscht…')}
</span> </span>
) : ( ) : (
<> <>
<FaTrash /> <FaTrash />
Confirm deletion {t('Löschung bestätigen')}
</> </>
)} )}
</button> </button>
@ -314,7 +316,7 @@ export const GDPRPage: React.FC = () => {
</ul> </ul>
</div> </div>
<div className={styles.infoBlock}> <div className={styles.infoBlock}>
<h3>Contact</h3> <h3>{t('Kontakt')}</h3>
<ul> <ul>
{Object.entries({ {Object.entries({
...(consentInfo.contact || {}), ...(consentInfo.contact || {}),

View file

@ -126,7 +126,7 @@ export const InvitePage: React.FC = () => {
sessionStorage.removeItem('auth_authority'); sessionStorage.removeItem('auth_authority');
handleLoginRedirect(); handleLoginRedirect();
} else { } else {
setError(result.error || 'Fehler beim Annehmen der Einladung'); setError(result.error || t('Fehler beim Annehmen der Einladung'));
} }
setAccepting(false); setAccepting(false);

View file

@ -168,7 +168,7 @@ function Login() {
}} }}
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`} 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>
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
<input <input
@ -190,7 +190,7 @@ function Login() {
</div> </div>
<div className={styles.disclaimer}> <div className={styles.disclaimer}>
<p> <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> </p>
</div> </div>
<button <button
@ -198,7 +198,7 @@ function Login() {
onClick={handleCredentialLogin} onClick={handleCredentialLogin}
disabled={isLoginLoading} disabled={isLoginLoading}
> >
{isLoginLoading ? "wird geladen..." : "Anmelden"} {isLoginLoading ? t('wird geladen…') : t('Anmelden')}
</button> </button>
<div className={styles.passwordResetLink}> <div className={styles.passwordResetLink}>
@ -206,12 +206,12 @@ function Login() {
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/password-reset-request")} onClick={() => navigate("/password-reset-request")}
> >
Passwort vergessen? {t('Passwort vergessen?')}
</button> </button>
</div> </div>
<div className={styles.divider}> <div className={styles.divider}>
<span>oder</span> <span>{t('oder')}</span>
</div> </div>
<button <button
@ -221,7 +221,7 @@ function Login() {
> >
<div className={styles.buttonContent}> <div className={styles.buttonContent}>
<FaMicrosoft /> <FaMicrosoft />
{isMsalLoading ? "Signing in..." : "Mit Microsoft anmelden"} {isMsalLoading ? t('Anmeldung läuft…') : t('Mit Microsoft anmelden')}
</div> </div>
</button> </button>
@ -232,7 +232,7 @@ function Login() {
> >
<div className={styles.buttonContent}> <div className={styles.buttonContent}>
<FaGoogle /> <FaGoogle />
{isGoogleLoading ? "Signing in..." : "Mit Google anmelden"} {isGoogleLoading ? t('Anmeldung läuft…') : t('Mit Google anmelden')}
</div> </div>
</button> </button>
@ -245,7 +245,7 @@ function Login() {
className={styles.ctaPrimary} className={styles.ctaPrimary}
onClick={() => navigate('/register', { state: location.state })} onClick={() => navigate('/register', { state: location.state })}
> >
Kostenlos registrieren {t('Kostenlos registrieren')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -28,26 +28,26 @@ function PasswordResetRequest() {
setValidationError(null); setValidationError(null);
if (!username.trim()) { if (!username.trim()) {
setValidationError('Bitte geben Sie Ihren Benutzernamen ein.'); setValidationError(t('Bitte geben Sie Ihren Benutzernamen ein.'));
return; return;
} }
try { try {
await requestReset(username.trim()); 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 // Redirect to login after delay
setTimeout(() => { setTimeout(() => {
navigate('/login', { navigate('/login', {
state: { state: {
passwordResetRequested: true, 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); }, 5000);
} catch (err) { } catch (err) {
// For security, still show success message even on error // 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(() => { setTimeout(() => {
navigate('/login'); navigate('/login');
}, 5000); }, 5000);
@ -97,7 +97,7 @@ function PasswordResetRequest() {
}} }}
className={`${styles.input} ${usernameFocused || username ? styles.focused : ''}`} 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>
<div className={styles.infoMessage}> <div className={styles.infoMessage}>
@ -109,7 +109,7 @@ function PasswordResetRequest() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? "Wird gesendet..." : "Reset-Link anfordern"} {isLoading ? t('Wird gesendet…') : t('Reset-Link anfordern')}
</button> </button>
</> </>
)} )}
@ -120,7 +120,7 @@ function PasswordResetRequest() {
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
> >
Login {t('Login')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -58,12 +58,12 @@ function Register() {
const _validateForm = (): boolean => { const _validateForm = (): boolean => {
if (!formData.username || !formData.email || !formData.fullName) { 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; return false;
} }
if (!formData.email.includes('@')) { 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; return false;
} }
@ -83,19 +83,19 @@ function Register() {
if (!availabilityResult.available) { if (!availabilityResult.available) {
const errorMessage = availabilityResult.message || 'Username is not available'; const errorMessage = availabilityResult.message || 'Username is not available';
if (errorMessage === 'Username is already taken') { if (errorMessage === 'Username is already taken') {
setValidationError('Benutzername ist bereits vergeben'); setValidationError(t('Benutzername ist bereits vergeben'));
setUsernameHighlight(true); setUsernameHighlight(true);
} else { } else {
setValidationError('Benutzername ist nicht verfügbar'); setValidationError(t('Benutzername ist nicht verfügbar'));
} }
return; return;
} }
await register({ ...formData, registrationType: 'personal' }); 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) { 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); setSuccessMessage(message);
@ -104,7 +104,7 @@ function Register() {
navigate('/login', { navigate('/login', {
state: { state: {
registered: true, 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 || {}) ...(location.state || {})
} }
}); });
@ -116,9 +116,9 @@ function Register() {
const _getErrorMessage = () => { const _getErrorMessage = () => {
if (validationError) return validationError; if (validationError) return validationError;
if (registerError) return typeof registerError === 'string' ? registerError : 'Registration failed'; if (registerError) return typeof registerError === 'string' ? registerError : t('Registrierung fehlgeschlagen');
if (msalError) return typeof msalError === 'string' ? msalError : 'Microsoft registration failed'; if (msalError) return typeof msalError === 'string' ? msalError : t('Microsoft-Registrierung fehlgeschlagen');
if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : 'Username availability check failed'; if (availabilityError) return typeof availabilityError === 'string' ? availabilityError : t('Benutzernamen-Prüfung fehlgeschlagen');
return null; return null;
}; };
@ -163,7 +163,7 @@ function Register() {
onBlur={() => setUsernameFocused(false)} onBlur={() => setUsernameFocused(false)}
className={`${styles.input} ${usernameFocused || formData.username ? styles.focused : ''} ${usernameHighlight ? styles.usernameError : ''}`} 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>
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
@ -177,7 +177,7 @@ function Register() {
onBlur={() => setEmailFocused(false)} onBlur={() => setEmailFocused(false)}
className={`${styles.input} ${emailFocused || formData.email ? styles.focused : ''}`} 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>
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
@ -200,7 +200,7 @@ function Register() {
<div className={styles.disclaimer}> <div className={styles.disclaimer}>
<p> <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> </p>
</div> </div>
@ -209,7 +209,7 @@ function Register() {
onClick={handleSubmit} onClick={handleSubmit}
disabled={isLoading || isChecking} 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> </button>
</> </>
)} )}
@ -220,7 +220,7 @@ function Register() {
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/login", { state: location.state })} onClick={() => navigate("/login", { state: location.state })}
> >
Jetzt anmelden {t('Jetzt anmelden')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -31,11 +31,11 @@ function Reset() {
// Validate token exists and format // Validate token exists and format
if (!token) { 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)) { } 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 _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; 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 => { const validateForm = (): boolean => {
if (!password || password.length < 8) { if (!password || password.length < 8) {
setValidationError('Passwort muss mindestens 8 Zeichen lang sein.'); setValidationError(t('Passwort muss mindestens 8 Zeichen lang sein.'));
return false; return false;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
setValidationError('Die Passwörter stimmen nicht überein.'); setValidationError(t('Die Passwörter stimmen nicht überein.'));
return false; return false;
} }
@ -65,28 +65,28 @@ function Reset() {
} }
if (!token) { 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; return;
} }
try { try {
await resetPassword(token, password); 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 // Redirect to login after delay
setTimeout(() => { setTimeout(() => {
navigate('/login', { navigate('/login', {
state: { state: {
passwordReset: true, passwordReset: true,
message: 'Passwort erfolgreich geändert. Bitte melden Sie sich an.' message: t('Passwort erfolgreich geändert. Bitte melden Sie sich an.')
} }
}); });
}, 3000); }, 3000);
} catch (err: any) { } catch (err: any) {
// Error is already set by the hook // 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')) { 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 { } else {
setValidationError(errorMessage); setValidationError(errorMessage);
} }
@ -115,7 +115,7 @@ function Reset() {
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/password-reset-request")} onClick={() => navigate("/password-reset-request")}
> >
Neuen Reset-Link anfordern {t('Neuen Reset-Link anfordern')}
</button> </button>
</div> </div>
<div className={styles.registerLink}> <div className={styles.registerLink}>
@ -124,7 +124,7 @@ function Reset() {
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
> >
Login {t('Login')}
</button> </button>
</div> </div>
</div> </div>
@ -200,7 +200,7 @@ function Reset() {
className={`${styles.button} ${styles.loginButton}`} className={`${styles.button} ${styles.loginButton}`}
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? "Wird gespeichert..." : "Passwort setzen"} {isLoading ? t('Wird gespeichert…') : t('Passwort setzen')}
</button> </button>
</form> </form>
)} )}
@ -211,7 +211,7 @@ function Reset() {
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
> >
Login {t('Login')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -60,7 +60,7 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
await onSave(formData); await onSave(formData);
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Fehler beim Speichern des Profils'); setError(err.message || t('Fehler beim Speichern des Profils'));
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@ -520,17 +520,17 @@ export const SettingsPage: React.FC = () => {
</div> </div>
{currentUser && ( {currentUser && (
<div className={styles.userInfoCard}> <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}>{t('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}>{t('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('E-Mail')}</span><span className={styles.userInfoValue}>{currentUser.email || '-'}</span></div>
</div> </div>
)} )}
</section> </section>
<section className={styles.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.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}>{t('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('Build')}</span><span className={styles.infoValue}>2026.03.23</span></div>
</div> </div>
</section> </section>
</> </>

View file

@ -24,18 +24,13 @@ import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard';
import { InstanceHierarchyView } from './InstanceHierarchyView'; import { InstanceHierarchyView } from './InstanceHierarchyView';
import { useLanguage } from '../../providers/language/LanguageContext'; import { useLanguage } from '../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../types/mandate';
function getMandateName(mandate: Mandate): string { function getMandateName(mandate: Mandate): string {
if (mandate.label) return mandate.label; return mandate.label || mandate.name || mandate.id;
if (typeof mandate.name === 'object') {
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
}
return mandate.name || mandate.id;
} }
function getFeatureLabel(feature: Feature, t: (k: string) => string): string { 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 { export interface InstanceWithStats extends FeatureInstance {
@ -146,15 +141,19 @@ export const AccessManagementHub: React.FC = () => {
const result = await syncInstanceRoles(selectedMandateId, instance.id, true); const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
if (result.success && result.data) { if (result.success && result.data) {
showSuccess( showSuccess(
'Rollen synchronisiert', t('Rollen synchronisiert'),
`Hinzugefügt: ${result.data.added}, Entfernt: ${result.data.removed}, Unverändert: ${result.data.unchanged}` 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); fetchInstances(selectedMandateId, selectedFeatureCode || undefined);
} else { } else {
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren'); showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren'));
} }
} catch { } 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 { return {
id: inst.id, id: inst.id,
label: inst.label, label: inst.label,
featureLabel: feature ? getFeatureLabel(feature) : inst.featureCode, featureLabel: feature ? getFeatureLabel(feature, t) : inst.featureCode,
userCount: inst.userCount ?? 0, userCount: inst.userCount ?? 0,
}; };
}), }),
@ -307,9 +306,11 @@ export const AccessManagementHub: React.FC = () => {
<div className={styles.adminPage}> <div className={styles.adminPage}>
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <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()}> <button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
<FaSync /> Erneut versuchen <FaSync /> {t('Erneut versuchen')}
</button> </button>
</div> </div>
</div> </div>
@ -320,9 +321,9 @@ export const AccessManagementHub: React.FC = () => {
<div className={styles.adminPage}> <div className={styles.adminPage}>
<div className={styles.pageHeader}> <div className={styles.pageHeader}>
<div> <div>
<h1 className={styles.pageTitle}>Zugriffsverwaltung</h1> <h1 className={styles.pageTitle}>{t('Zugriffsverwaltung')}</h1>
<p className={styles.pageSubtitle}> <p className={styles.pageSubtitle}>
Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten {t('Feature-Instanzen, Benutzer und Rollen an einem Ort verwalten')}
</p> </p>
</div> </div>
</div> </div>
@ -334,7 +335,7 @@ export const AccessManagementHub: React.FC = () => {
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
<FaBuilding style={{ marginRight: 8 }} /> <FaBuilding style={{ marginRight: 8 }} />
Mandant: {t('Mandant')}:
</label> </label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
@ -352,7 +353,7 @@ export const AccessManagementHub: React.FC = () => {
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
Feature: {t('Feature')}:
</label> </label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
@ -376,14 +377,14 @@ export const AccessManagementHub: React.FC = () => {
} }
disabled={loading} disabled={loading}
> >
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren <FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button> </button>
<button <button
className={styles.primaryButton} className={styles.primaryButton}
onClick={() => setShowWizard(true)} onClick={() => setShowWizard(true)}
disabled={features.length === 0} disabled={features.length === 0}
> >
+ Neue Instanz erstellen + {t('Neue Instanz erstellen')}
</button> </button>
</div> </div>
)} )}
@ -398,21 +399,21 @@ export const AccessManagementHub: React.FC = () => {
className={viewMode === 'list' ? hubStyles.viewModeActive : hubStyles.viewModeButton} className={viewMode === 'list' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
> >
<FaList /> Listenansicht <FaList /> {t('Listenansicht')}
</button> </button>
<button <button
type="button" type="button"
className={viewMode === 'hierarchy' ? hubStyles.viewModeActive : hubStyles.viewModeButton} className={viewMode === 'hierarchy' ? hubStyles.viewModeActive : hubStyles.viewModeButton}
onClick={() => setViewMode('hierarchy')} onClick={() => setViewMode('hierarchy')}
> >
<FaSitemap /> Hierarchie <FaSitemap /> {t('Hierarchie')}
</button> </button>
</div> </div>
<Link to="/admin/mandates" className={hubStyles.mandatesLink}> <Link to="/admin/mandates" className={hubStyles.mandatesLink}>
<FaBuilding /> Mandanten verwalten <FaBuilding /> {t('Mandanten verwalten')}
</Link> </Link>
<Link to="/admin/user-mandates" className={hubStyles.mandatesLink}> <Link to="/admin/user-mandates" className={hubStyles.mandatesLink}>
<FaUsers /> Mandant-Benutzer <FaUsers /> {t('Mandant-Benutzer')}
</Link> </Link>
</div> </div>
@ -423,7 +424,7 @@ export const AccessManagementHub: React.FC = () => {
instancesByMandate={instancesByMandate} instancesByMandate={instancesByMandate}
instanceUsersMap={instanceUsersMap} instanceUsersMap={instanceUsersMap}
features={features} features={features}
getFeatureLabel={getFeatureLabel} getFeatureLabel={(f) => getFeatureLabel(f, t)}
loading={hierarchyUsersLoading} loading={hierarchyUsersLoading}
onOpenDetail={handleOpenDetail} onOpenDetail={handleOpenDetail}
/> />
@ -432,7 +433,7 @@ export const AccessManagementHub: React.FC = () => {
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}> <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> </p>
</div> </div>
) : ( ) : (
@ -444,7 +445,7 @@ export const AccessManagementHub: React.FC = () => {
<span className={hubStyles.statsValue}> <span className={hubStyles.statsValue}>
{loading || statsLoading ? '…' : overviewStats.instances} {loading || statsLoading ? '…' : overviewStats.instances}
</span> </span>
<span className={hubStyles.statsLabel}>Instanzen</span> <span className={hubStyles.statsLabel}>{t('Instanzen')}</span>
</div> </div>
</div> </div>
<div className={hubStyles.statsCard}> <div className={hubStyles.statsCard}>
@ -469,7 +470,7 @@ export const AccessManagementHub: React.FC = () => {
<div className={hubStyles.diagramCard}> <div className={hubStyles.diagramCard}>
<FaLink className={hubStyles.statsIcon} /> <FaLink className={hubStyles.statsIcon} />
<div className={hubStyles.diagramContent}> <div className={hubStyles.diagramContent}>
<span className={hubStyles.diagramTitle}>Beziehungen</span> <span className={hubStyles.diagramTitle}>{t('Beziehungen')}</span>
<div className={hubStyles.diagramFlow}> <div className={hubStyles.diagramFlow}>
<div className={hubStyles.diagramNode}>{relationshipData.mandateName}</div> <div className={hubStyles.diagramNode}>{relationshipData.mandateName}</div>
<div className={hubStyles.diagramNodes}> <div className={hubStyles.diagramNodes}>
@ -480,7 +481,7 @@ export const AccessManagementHub: React.FC = () => {
))} ))}
{relationshipData.instances.length > 5 && ( {relationshipData.instances.length > 5 && (
<div className={hubStyles.diagramNodeSmall}> <div className={hubStyles.diagramNodeSmall}>
+{relationshipData.instances.length - 5} weitere +{relationshipData.instances.length - 5} {t('weitere')}
</div> </div>
)} )}
</div> </div>
@ -491,7 +492,7 @@ export const AccessManagementHub: React.FC = () => {
</div> </div>
<section className={hubStyles.section}> <section className={hubStyles.section}>
<h2 className={hubStyles.sectionTitle}>Feature-Instanzen</h2> <h2 className={hubStyles.sectionTitle}>{t('Feature-Instanzen')}</h2>
{loading && filteredInstances.length === 0 ? ( {loading && filteredInstances.length === 0 ? (
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner} /> <div className={styles.spinner} />
@ -502,14 +503,14 @@ export const AccessManagementHub: React.FC = () => {
<FaCube className={styles.emptyIcon} /> <FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanzen')}</h3> <h3 className={styles.emptyTitle}>{t('Keine Feature-Instanzen')}</h3>
<p className={styles.emptyDescription}> <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> </p>
<button <button
className={styles.primaryButton} className={styles.primaryButton}
onClick={() => setShowWizard(true)} onClick={() => setShowWizard(true)}
disabled={features.length === 0} disabled={features.length === 0}
> >
+ Erste Instanz erstellen + {t('Erste Instanz erstellen')}
</button> </button>
</div> </div>
) : ( ) : (
@ -526,8 +527,12 @@ export const AccessManagementHub: React.FC = () => {
</div> </div>
<div className={hubStyles.instanceMeta}> <div className={hubStyles.instanceMeta}>
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span> <span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span>
<span>{inst.userCount ?? '—'} Benutzer</span> <span>
<span>{inst.roleCount ?? '—'} Rollen</span> {inst.userCount ?? '—'} {t('Benutzer')}
</span>
<span>
{inst.roleCount ?? '—'} {t('Rollen')}
</span>
</div> </div>
<div className={hubStyles.instanceActions}> <div className={hubStyles.instanceActions}>
<button <button
@ -535,7 +540,7 @@ export const AccessManagementHub: React.FC = () => {
className={hubStyles.cardAction} className={hubStyles.cardAction}
onClick={() => handleOpenDetail(inst, selectedMandateId)} onClick={() => handleOpenDetail(inst, selectedMandateId)}
> >
<FaUsers /> Benutzer verwalten <FaUsers /> {t('Benutzer verwalten')}
</button> </button>
<button <button
type="button" type="button"
@ -544,7 +549,7 @@ export const AccessManagementHub: React.FC = () => {
disabled={!inst.enabled} disabled={!inst.enabled}
title={t('Rollen synchronisieren')} title={t('Rollen synchronisieren')}
> >
<FaCogs /> Rollen sync <FaCogs /> {t('Rollen sync')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -20,7 +20,6 @@ import { TextField } from '../../components/UiComponents/TextField';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext'; import { useLanguage } from '../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../types/mandate';
export const AdminFeatureAccessPage: React.FC = () => { export const AdminFeatureAccessPage: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
@ -94,7 +93,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
render: (value: string) => { render: (value: string) => {
const feature = features.find(f => f.code === value); const feature = features.find(f => f.code === value);
if (feature) { if (feature) {
return t(labelAsI18nKey(feature.label, value)); return t(feature.label || value);
} }
return value; return value;
} }
@ -122,7 +121,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
try { try {
// Validate label // Validate label
if (!createLabel || createLabel.trim() === '') { if (!createLabel || createLabel.trim() === '') {
showError('Fehler', 'Label ist erforderlich.'); showError(t('Fehler'), t('Label ist erforderlich.'));
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
@ -132,7 +131,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
if (createFeatureCode === 'chatbot') { if (createFeatureCode === 'chatbot') {
// Validate required fields // Validate required fields
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { 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); setIsSubmitting(false);
return; return;
} }
@ -176,9 +175,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotAllowedProviders([]); setChatbotAllowedProviders([]);
fetchInstances(selectedMandateId); fetchInstances(selectedMandateId);
loadFeatures(); // Refresh global navigation cache 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 { } else {
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz'); showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -228,7 +227,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
if (editingInstance.featureCode === 'chatbot') { if (editingInstance.featureCode === 'chatbot') {
// Validate required fields // Validate required fields
if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { 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); setIsSubmitting(false);
return; return;
} }
@ -270,9 +269,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotAllowedProviders([]); setChatbotAllowedProviders([]);
fetchInstances(selectedMandateId); fetchInstances(selectedMandateId);
loadFeatures(); // Refresh global navigation cache 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 { } else {
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Feature-Instanz'); showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -285,10 +284,10 @@ export const AdminFeatureAccessPage: React.FC = () => {
const result = await deleteInstance(selectedMandateId, instanceId); const result = await deleteInstance(selectedMandateId, instanceId);
if (result.success) { if (result.success) {
loadFeatures(); // Refresh global navigation cache 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; return true;
} else { } 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; return false;
} }
}; };
@ -301,11 +300,15 @@ export const AdminFeatureAccessPage: React.FC = () => {
const result = await syncInstanceRoles(selectedMandateId, instance.id, true); const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
if (result.success && result.data) { if (result.success && result.data) {
showSuccess( showSuccess(
'Rollen synchronisiert', t('Rollen synchronisiert'),
`Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}` t('Hinzugefügt: {added}\nEntfernt: {removed}\nUnverändert: {unchanged}', {
added: result.data.added,
removed: result.data.removed,
unchanged: result.data.unchanged,
})
); );
} else { } else {
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen'); showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Rollen'));
} }
} finally { } finally {
setSyncingInstance(null); setSyncingInstance(null);
@ -314,18 +317,14 @@ export const AdminFeatureAccessPage: React.FC = () => {
// Get mandate name // Get mandate name
const getMandateName = (mandate: Mandate) => { const getMandateName = (mandate: Mandate) => {
if (mandate.label) return mandate.label; return mandate.label || mandate.name || mandate.id;
if (typeof mandate.name === 'object') {
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
}
return mandate.name || mandate.id;
}; };
// Get feature label // Get feature label
const getFeatureLabel = (code: string) => { const getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code); const feature = features.find(f => f.code === code);
if (feature) { if (feature) {
return t(labelAsI18nKey(feature.label, code)); return t(feature.label || code);
} }
return code; return code;
}; };
@ -337,7 +336,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<span className={styles.errorIcon}></span> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler: {error}</p> <p className={styles.errorMessage}>Fehler: {error}</p>
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}> <button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
<FaSync /> Erneut versuchen <FaSync /> {t('Erneut versuchen')}
</button> </button>
</div> </div>
</div> </div>
@ -348,7 +347,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}> <div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}> <div className={styles.pageHeader}>
<div> <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> <p className={styles.pageSubtitle}>{t('Verwalten Sie Feature-Instanzen für jeden')}</p>
</div> </div>
</div> </div>
@ -358,7 +357,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
<FaBuilding style={{ marginRight: 8 }} /> <FaBuilding style={{ marginRight: 8 }} />
Mandant auswählen: {t('Mandant auswählen:')}
</label> </label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
@ -381,15 +380,19 @@ export const AdminFeatureAccessPage: React.FC = () => {
onClick={() => fetchInstances(selectedMandateId)} onClick={() => fetchInstances(selectedMandateId)}
disabled={loading} disabled={loading}
> >
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren <FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button> </button>
<button <button
className={styles.primaryButton} className={styles.primaryButton}
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
disabled={features.length === 0} 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> </button>
</div> </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))' }}> <div className={styles.infoBox} style={{ borderColor: 'var(--error-color, #dc3545)', backgroundColor: 'var(--error-bg, rgba(220, 53, 69, 0.1))' }}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
<span> <span>
Keine Features geladen. {t('Keine Features geladen.')}
{error ? ` Fehler: ${error}` : ' Die API hat keine Features zurückgegeben.'} {error ? ` Fehler: ${error}` : ` ${t('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('Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/')}
</span> </span>
<button <button
className={styles.secondaryButton} className={styles.secondaryButton}
onClick={() => fetchFeatures()} onClick={() => fetchFeatures()}
style={{ marginLeft: '1rem' }} style={{ marginLeft: '1rem' }}
> >
<FaSync /> Features erneut laden <FaSync /> {t('Features erneut laden')}
</button> </button>
</div> </div>
) : null} ) : null}
@ -431,7 +435,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<FaBuilding className={styles.emptyIcon} /> <FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}> <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> </p>
</div> </div>
) : ( ) : (
@ -505,12 +509,12 @@ export const AdminFeatureAccessPage: React.FC = () => {
{/* Feature Code Selector - Required for chatbot config */} {/* Feature Code Selector - Required for chatbot config */}
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}> <div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
<label className={styles.configLabel} style={{ fontWeight: 600 }}> <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> </label>
<DropdownSelect <DropdownSelect
items={features.map(f => ({ items={features.map(f => ({
id: f.code, id: f.code,
label: t(labelAsI18nKey(f.label, f.code)), label: t(f.label || f.code),
value: f.code value: f.code
}))} }))}
selectedItemId={createFeatureCode} selectedItemId={createFeatureCode}
@ -528,7 +532,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
/> />
{!createFeatureCode && ( {!createFeatureCode && (
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}> <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> </p>
)} )}
</div> </div>
@ -536,7 +540,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
{/* Chatbot Configuration Title - Show when chatbot is selected */} {/* Chatbot Configuration Title - Show when chatbot is selected */}
{createFeatureCode === 'chatbot' && ( {createFeatureCode === 'chatbot' && (
<h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}> <h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
Chatbot-Konfiguration {t('Chatbot-Konfiguration')}
</h3> </h3>
)} )}
@ -544,7 +548,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
{createFeatureCode && ( {createFeatureCode && (
<div className={styles.configField} style={{ marginBottom: '1.5rem' }}> <div className={styles.configField} style={{ marginBottom: '1.5rem' }}>
<label className={styles.configLabel}> <label className={styles.configLabel}>
Label: <span style={{ color: 'var(--error-color)' }}>*</span> {t('Label')}: <span style={{ color: 'var(--error-color)' }}>*</span>
</label> </label>
<TextField <TextField
type="text" type="text"

View file

@ -17,7 +17,6 @@ import api from '../../api';
import styles from './Admin.module.css'; import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext'; import { useLanguage } from '../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../types/mandate';
export const AdminFeatureInstanceUsersPage: React.FC = () => { export const AdminFeatureInstanceUsersPage: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
@ -93,7 +92,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
allOptions.push({ allOptions.push({
mandateId: mandate.id, mandateId: mandate.id,
instanceId: inst.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, instanceLabel: inst.label || inst.id,
featureCode: inst.featureCode, featureCode: inst.featureCode,
combinedKey: `${mandate.id}:${inst.id}`, combinedKey: `${mandate.id}:${inst.id}`,
@ -314,9 +313,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
setShowAddModal(false); setShowAddModal(false);
refreshUsers(); refreshUsers();
loadFeatures(); // Refresh global navigation cache 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 { } else {
showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers'); showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers'));
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -338,9 +337,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
setEditingUser(null); setEditingUser(null);
refreshUsers(); refreshUsers();
loadFeatures(); // Refresh global navigation cache 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 { } else {
showError('Fehler', result.error || 'Fehler beim Aktualisieren'); showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren'));
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -354,9 +353,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
if (result.success) { if (result.success) {
refreshUsers(); refreshUsers();
loadFeatures(); // Refresh global navigation cache 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 { } 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 getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code); const feature = features.find(f => f.code === code);
if (feature) { if (feature) {
return t(labelAsI18nKey(feature.label, code)); return t(feature.label || code);
} }
return code; return code;
}; };
@ -396,9 +395,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}> <div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.errorContainer}> <div className={styles.errorContainer}>
<span className={styles.errorIcon}></span> <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()}> <button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
<FaSync /> Erneut versuchen <FaSync /> {t('Erneut versuchen')}
</button> </button>
</div> </div>
</div> </div>
@ -419,7 +420,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}> <div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
Mandant / Feature-Instanz: {t('Mandant / Feature-Instanz')}:
</label> </label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
@ -458,14 +459,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
onClick={() => refreshUsers()} onClick={() => refreshUsers()}
disabled={usersLoading} disabled={usersLoading}
> >
<FaSync className={usersLoading ? 'spinning' : ''} /> Aktualisieren <FaSync className={usersLoading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button> </button>
<button <button
className={styles.primaryButton} className={styles.primaryButton}
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
disabled={availableUsers.length === 0 || instanceRoles.length === 0} disabled={availableUsers.length === 0 || instanceRoles.length === 0}
> >
<FaPlus /> Benutzer hinzufügen <FaPlus /> {t('Benutzer hinzufügen')}
</button> </button>
</div> </div>
)} )}
@ -475,10 +476,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{selectedOption && ( {selectedOption && (
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaBuilding style={{ marginRight: 8 }} /> <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> <span style={{ margin: '0 16px', color: 'var(--color-border)' }}>|</span>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
<span>Instanz: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})</span> <span>
{t('Instanz')}: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})
</span>
</div> </div>
)} )}
@ -497,7 +502,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{/* Warning if no roles available */} {/* Warning if no roles available */}
{selectedInstance && instanceRoles.length === 0 && !usersLoading && ( {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> </span>
<span>{t('Diese Instanz hat noch keine')}</span> <span>{t('Diese Instanz hat noch keine')}</span>
</div> </div>
@ -595,7 +600,9 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}> <div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}> <div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Rollen bearbeiten: {editingUser.username}</h2> <h2 className={styles.modalTitle}>
{t('Rollen bearbeiten')}: {editingUser.username}
</h2>
<button <button
className={styles.modalClose} className={styles.modalClose}
onClick={() => setEditingUser(null)} onClick={() => setEditingUser(null)}

View file

@ -23,18 +23,18 @@ import { useLanguage } from '../../providers/language/LanguageContext';
interface Feature { interface Feature {
id?: string; id?: string;
code: string; // Backend uses 'code' not 'featureCode' code: string;
featureCode?: string; // Alias for backward compatibility featureCode?: string;
label: string | { [key: string]: string }; // Backend uses 'label' not 'name' label: string;
name?: string | { [key: string]: string }; // Alias for backward compatibility name?: string;
description?: string | { [key: string]: string }; description?: string;
icon?: string; icon?: string;
} }
interface FeatureRole { interface FeatureRole {
id: string; id: string;
roleLabel: string; roleLabel: string;
description?: { [key: string]: string }; description?: string;
featureCode: string; featureCode: string;
mandateId?: string | null; mandateId?: string | null;
featureInstanceId?: string | null; featureInstanceId?: string | null;
@ -118,17 +118,14 @@ export const AdminFeatureRolesPage: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [selectedFeatureCode]); }, [selectedFeatureCode, t]);
useEffect(() => { useEffect(() => {
fetchRoles(); fetchRoles();
}, [fetchRoles]); }, [fetchRoles]);
// Get text from multilingual object const getTextValue = (value: string | undefined): string => {
const getTextValue = (value: string | { [key: string]: string } | undefined): string => { return value || '-';
if (!value) return '-';
if (typeof value === 'string') return value;
return value.de || value.en || Object.values(value)[0] || '-';
}; };
// Table columns // Table columns
@ -148,7 +145,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
type: 'string' as const, type: 'string' as const,
sortable: false, sortable: false,
width: 300, width: 300,
formatter: (value: string | { [key: string]: string }) => getTextValue(value) formatter: (value: string) => getTextValue(value)
}, },
{ {
key: 'featureCode', key: 'featureCode',
@ -178,9 +175,9 @@ export const AdminFeatureRolesPage: React.FC = () => {
{ {
name: 'description', name: 'description',
label: t('Beschreibung'), label: t('Beschreibung'),
type: 'multilingual', type: 'textarea',
required: false, required: false,
description: t('Mehrsprachige Beschreibung') description: t('Beschreibung der Rolle')
} }
]; ];
return fields; return fields;
@ -200,15 +197,15 @@ export const AdminFeatureRolesPage: React.FC = () => {
{ {
name: 'description', name: 'description',
label: t('Beschreibung'), label: t('Beschreibung'),
type: 'multilingual', type: 'textarea',
required: false, required: false,
description: t('Mehrsprachige Beschreibung') description: t('Beschreibung der Rolle')
} }
]; ];
}, [t]); }, [t]);
// Handle create role // Handle create role
const handleCreateRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => { const handleCreateRole = async (data: { roleLabel: string; description?: string }) => {
if (!selectedFeatureCode) return; if (!selectedFeatureCode) return;
setIsSubmitting(true); setIsSubmitting(true);
try { try {
@ -216,20 +213,20 @@ export const AdminFeatureRolesPage: React.FC = () => {
params.append('roleLabel', data.roleLabel); params.append('roleLabel', data.roleLabel);
params.append('featureCode', selectedFeatureCode); 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); setShowCreateModal(false);
await fetchRoles(); await fetchRoles();
} catch (err: any) { } catch (err: any) {
console.error('Error creating role:', err); 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 { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
// Handle edit role // Handle edit role
const handleEditRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => { const handleEditRole = async (data: { roleLabel: string; description?: string }) => {
if (!editingRole) return; if (!editingRole) return;
setIsSubmitting(true); setIsSubmitting(true);
try { try {
@ -241,7 +238,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
await fetchRoles(); await fetchRoles();
} catch (err: any) { } catch (err: any) {
console.error('Error updating role:', err); 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 { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -254,7 +251,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
await fetchRoles(); await fetchRoles();
} catch (err: any) { } catch (err: any) {
console.error('Error deleting role:', err); 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> <span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{error}</p> <p className={styles.errorMessage}>{error}</p>
<button className={styles.secondaryButton} onClick={() => window.location.reload()}> <button className={styles.secondaryButton} onClick={() => window.location.reload()}>
<FaSync /> Erneut versuchen <FaSync /> {t('Erneut versuchen')}
</button> </button>
</div> </div>
</div> </div>
@ -297,7 +294,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}> <label className={styles.filterLabel}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
Feature: {t('Feature:')}
</label> </label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
@ -323,13 +320,13 @@ export const AdminFeatureRolesPage: React.FC = () => {
onClick={() => fetchRoles()} onClick={() => fetchRoles()}
disabled={loading} disabled={loading}
> >
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren <FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button> </button>
<button <button
className={styles.primaryButton} className={styles.primaryButton}
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
> >
<FaPlus /> Neue Feature-Rolle <FaPlus /> {t('Neue Feature-Rolle')}
</button> </button>
</div> </div>
)} )}
@ -340,8 +337,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.infoBox}> <div className={styles.infoBox}>
<FaUserShield style={{ marginRight: 8 }} /> <FaUserShield style={{ marginRight: 8 }} />
<span> <span>
<strong>Feature-Template-Rollen</strong> werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert. <strong>{t('Feature-Template-Rollen')}</strong>{' '}
Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus. {t('werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert. Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.')}
</span> </span>
</div> </div>
)} )}
@ -352,7 +349,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<FaCube className={styles.emptyIcon} /> <FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3> <h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
<p className={styles.emptyDescription}> <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> </p>
</div> </div>
) : ( ) : (
@ -414,14 +411,16 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.modalContent}> <div className={styles.modalContent}>
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}> <div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
<span>Feature: <strong>{selectedFeatureCode}</strong></span> <span>
{t('Feature')}: <strong>{selectedFeatureCode}</strong>
</span>
</div> </div>
<FormGeneratorForm <FormGeneratorForm
attributes={createFields} attributes={createFields}
mode="create" mode="create"
onSubmit={handleCreateRole} onSubmit={handleCreateRole}
onCancel={() => setShowCreateModal(false)} onCancel={() => setShowCreateModal(false)}
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'} submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')}
cancelButtonText={t('Abbrechen')} cancelButtonText={t('Abbrechen')}
/> />
</div> </div>
@ -445,7 +444,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.modalContent}> <div className={styles.modalContent}>
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}> <div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} /> <FaCube style={{ marginRight: 8 }} />
<span>Feature: <strong>{editingRole.featureCode}</strong></span> <span>{t('Feature:')} <strong>{editingRole.featureCode}</strong></span>
</div> </div>
<FormGeneratorForm <FormGeneratorForm
attributes={editFields} attributes={editFields}
@ -468,7 +467,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}> <h2 className={styles.modalTitle}>
<FaShieldAlt style={{ marginRight: 8 }} /> <FaShieldAlt style={{ marginRight: 8 }} />
Berechtigungen: {permissionsRole.roleLabel} {t('Berechtigungen')}: {permissionsRole.roleLabel}
</h2> </h2>
<button <button
className={styles.modalClose} className={styles.modalClose}
@ -480,7 +479,9 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.modalContent}> <div className={styles.modalContent}>
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}> <div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} /> <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> <span style={{ marginLeft: '1rem' }}>{t('Template-Rolle global')}</span>
</div> </div>
<AccessRulesEditor <AccessRulesEditor

Some files were not shown because too many files have changed in this diff Show more