alle sprachtexte mit dot-strings ersetzt

This commit is contained in:
ValueOn AG 2026-04-11 00:07:30 +02:00
parent 9ac2d5a6c1
commit 7cf0795660
131 changed files with 1410 additions and 1279 deletions

View file

@ -86,7 +86,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
<button
className={`${styles.iconButton} ${styles.danger}`}
onClick={() => onDelete(rule.id)}
title={t('accessRulesEditor.regelLoeschen')}
title={t('delete rule')}
>
<FaTrash />
</button>
@ -140,7 +140,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
/>
</div>
<div className={styles.permissionItem}>
<span className={styles.permissionLabel}>{t('accessRulesEditor.delete')}</span>
<span className={styles.permissionLabel}>{t('delete')}</span>
<AccessLevelSelect
value={rule.delete}
onChange={(value) => onUpdate(rule.id, { delete: value })}
@ -221,13 +221,13 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
<form className={styles.addRuleForm} onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<div className={styles.objectSelectorLabel}>
<label className={styles.formLabel}>{t('accessRulesEditor.objektAuswaehlen')}</label>
<label className={styles.formLabel}>{t('select object')}</label>
<button
type="button"
className={styles.toggleCustomButton}
onClick={() => setUseCustom(!useCustom)}
>
{useCustom ? t('accessRulesEditor.ausKatalogWaehlen') : t('accessRulesEditor.freieEingabe')}
{useCustom ? t('select from catalog') : t('free input')}
</button>
</div>
@ -246,7 +246,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
onChange={(e) => setItem(e.target.value)}
className={styles.formSelect}
>
<option value="">{t('accessRulesEditor.globalAlleObjekte')}</option>
<option value="">{t('global all objects')}</option>
{Object.entries(groupedObjects).map(([feature, objs]) => (
<optgroup key={feature} label={feature.toUpperCase()}>
{objs.map(obj => (
@ -281,9 +281,9 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
{/* Header Row */}
<div className={styles.matrixHeader}>
<div className={styles.matrixLabel}></div>
<div className={styles.matrixGroup}>{t('accessRulesEditor.eigeneM')}</div>
<div className={styles.matrixGroup}>{t('accessRulesEditor.gruppeG')}</div>
<div className={styles.matrixGroup}>{t('accessRulesEditor.alleA')}</div>
<div className={styles.matrixGroup}>{t('own')}</div>
<div className={styles.matrixGroup}>{t('group')}</div>
<div className={styles.matrixGroup}>{t('Alle')}</div>
</div>
{/* CRUD Rows */}
@ -633,9 +633,9 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
// Render tabs
const tabs: { id: TabType; label: string; icon: React.ReactNode; count: number }[] = [
{ id: 'DATA', label: t('accessRulesEditor.daten'), icon: <FaTable />, count: groupedRules.DATA.length },
{ id: 'DATA', label: t('data'), icon: <FaTable />, count: groupedRules.DATA.length },
{ id: 'UI', label: 'UI', icon: <FaDesktop />, count: groupedRules.UI.length },
{ id: 'RESOURCE', label: t('accessRulesEditor.ressourcen'), icon: <FaServer />, count: groupedRules.RESOURCE.length },
{ id: 'RESOURCE', label: t('resources'), icon: <FaServer />, count: groupedRules.RESOURCE.length },
{ id: 'JSON', label: 'JSON', icon: <FaCode />, count: rules.length },
];
@ -644,7 +644,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
<div className={styles.accessRulesEditor}>
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('accessRulesEditor.ladeBerechtigungen')}</span>
<span>{t('loading permissions')}</span>
</div>
</div>
);

View file

@ -166,7 +166,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
<button
className={`${styles.iconButton} ${styles.danger}`}
onClick={() => onDelete(rule.id)}
title={t('accessRulesTable.regelLoeschen')}
title={t('delete rule')}
>
<FaTrash />
</button>
@ -199,13 +199,13 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
<table className={styles.accessRulesTable}>
<thead>
<tr>
<th className={styles.colObject}>{t('accessRulesTable.objektDotnotation')}</th>
<th className={styles.colObject}>{t('object dot notation')}</th>
<th className={styles.colView}>View</th>
{isDataContext && (
<>
<th className={styles.colGroupHeader} colSpan={4}>{t('accessRulesTable.eigeneM')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('accessRulesTable.gruppeG')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('accessRulesTable.alleA')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('own')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('group')}</th>
<th className={styles.colGroupHeader} colSpan={4}>{t('Alle')}</th>
</>
)}
<th className={styles.colActions}></th>
@ -217,15 +217,15 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
<th title="Create">C</th>
<th title="Read">R</th>
<th title="Update">U</th>
<th title={t('accessRulesTable.delete')}>D</th>
<th title={t('delete')}>D</th>
<th title="Create">C</th>
<th title="Read">R</th>
<th title="Update">U</th>
<th title={t('accessRulesTable.delete')}>D</th>
<th title={t('delete')}>D</th>
<th title="Create">C</th>
<th title="Read">R</th>
<th title="Update">U</th>
<th title={t('accessRulesTable.delete')}>D</th>
<th title={t('delete')}>D</th>
<th></th>
</tr>
)}

View file

@ -61,11 +61,11 @@ export function ContentPreview({
if (isOpen && fileId) {
// Check if we have valid data
if (!fileId || fileId === 'undefined' || fileId === 'null') {
setError(t('contentPreview.invalidFileId'));
setError(t('Ungültige Datei-ID'));
return;
}
if (!fileName || fileName === 'Unknown Item') {
setError(t('contentPreview.fileNameNotAvailable'));
setError(t('Dateiname nicht verfügbar'));
return;
}
loadPreview();
@ -98,7 +98,7 @@ export function ContentPreview({
setError(result.error || 'Failed to load preview');
}
} catch (err) {
setError(t('contentPreview.anUnexpectedErrorOccurredWhile'));
setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
}
};
@ -168,7 +168,7 @@ export function ContentPreview({
previewUrl={undefined}
previewContent={previewContent}
fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadPdfPreview'))}
onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
/>
);
}
@ -194,9 +194,9 @@ export function ContentPreview({
return (
<div className={styles.jsonContainer}>
<div className={styles.jsonHeader}>
<span className={styles.jsonTitle}>{t('contentPreview.jsonPreviewFallback')}</span>
<span className={styles.jsonTitle}>{t('JSON-Vorschau als Fallback')}</span>
<div className={styles.jsonHeaderRight}>
<span className={styles.jsonSize}>{t('contentPreview.rawContent')}</span>
<span className={styles.jsonSize}>{t('Rohinhalt')}</span>
</div>
</div>
<pre className={styles.jsonPreview}>
@ -219,7 +219,7 @@ export function ContentPreview({
<ImageRenderer
previewUrl={previewUrl}
fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadImagePreview'))}
onError={() => setError(t('Bildvorschau konnte nicht geladen werden'))}
/>
);
@ -230,7 +230,7 @@ export function ContentPreview({
<HtmlRenderer
previewUrl={previewUrl}
fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadHtmlPreview'))}
onError={() => setError(t('HTML-Vorschau konnte nicht geladen werden'))}
/>
);
}
@ -240,7 +240,7 @@ export function ContentPreview({
previewUrl={previewUrl}
fileName={fileName}
mimeType={mimeType}
onError={() => setError(t('contentPreview.failedToLoadTextPreview'))}
onError={() => setError(t('Textvorschau konnte nicht geladen werden'))}
/>
);
@ -257,7 +257,7 @@ export function ContentPreview({
previewUrl={previewUrl}
previewContent={previewContent || undefined}
fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadPdfPreview'))}
onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
/>
);
}
@ -267,7 +267,7 @@ export function ContentPreview({
<HtmlRenderer
previewUrl={previewUrl}
fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadHtmlPreview'))}
onError={() => setError(t('HTML-Vorschau konnte nicht geladen werden'))}
/>
);
}
@ -279,7 +279,7 @@ export function ContentPreview({
previewUrl={previewUrl}
fileName={fileName}
mimeType={mimeType}
onError={() => setError(t('contentPreview.previewNotSupportedForThis'))}
onError={() => setError(t('Vorschau wird für dieses Format nicht unterstützt'))}
/>
);

View file

@ -79,7 +79,7 @@ export function UrlContentPreview({
}
// If PDF.js also fails, show error
setIsLoading(false);
setError(t('urlContentPreview.failedToLoadPdfThis'));
setError(t('Fehler beim Laden des PDFs'));
setShowPdfAnyway(true);
};
@ -111,7 +111,7 @@ export function UrlContentPreview({
} else if (isLoading && !hasLoaded && usePdfJs) {
// PDF.js also failed, show error
setShowPdfAnyway(true);
setError(t('urlContentPreview.pdfLaedtLangsamBitteVerwenden'));
setError(t('PDF lädt langsam, bitte verwenden'));
setIsLoading(false);
}
}, QUICK_TIMEOUT);
@ -129,7 +129,7 @@ export function UrlContentPreview({
try {
new URL(url);
} catch (e) {
setError(t('urlContentPreview.invalidUrl'));
setError(t('Ungültige URL'));
setIsLoading(false);
}
}
@ -314,7 +314,7 @@ export function UrlContentPreview({
<div className={styles.unsupportedContainer}>
<div className={styles.unsupportedIcon}>📄</div>
<div className={styles.fileName}>{fileName}</div>
<p>{t('urlContentPreview.previewNotSupportedForThis')}</p>
<p>{t('Vorschau wird hierfür nicht unterstützt')}</p>
<button onClick={handleDownload} className={styles.retryButton}>
Download File
</button>

View file

@ -339,7 +339,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
) : (
typeof data.values[index] === 'object' && data.values[index] !== null && 'keys' in data.values[index] ?
renderTable(data.values[index], level + 1, rowPath) :
<span className={styles.jsonValue}>{t('jsonRenderer.errorInvalidNestedData')}</span>
<span className={styles.jsonValue}>{t('Fehler: Ungültige verschachtelte Daten')}</span>
)
)}
</div>

View file

@ -141,7 +141,7 @@ export function PdfJsRenderer({
return (
<div className={styles.loadingContainer}>
<div className={styles.spinner}></div>
<p>{t('pdfJsRenderer.pdfWirdGeladen')}</p>
<p>{t('PDF wird geladen')}</p>
</div>
);
}

View file

@ -20,7 +20,7 @@ export function TextRenderer({
return (
<div className={styles.textContainer}>
<div className={styles.textHeader}>
<span className={styles.textTitle}>{t('textRenderer.textPreview')}</span>
<span className={styles.textTitle}>{t('Textvorschau')}</span>
</div>
<pre className={styles.textPreview}>
<code className={styles.textCode}>

View file

@ -237,7 +237,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
setExecuteResult({ success: true } as ExecuteGraphResponse);
} else {
const label = await promptInput('Workflow-Name:', {
title: t('automation2FlowEditor.saveWorkflow'),
title: t('Workflow speichern'),
defaultValue: 'Neuer Workflow',
placeholder: 'Name des Workflows',
});
@ -595,7 +595,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
</div>
<div className={styles.loading}>
<FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} />
<p>{t('automation2FlowEditor.ladeNodetypen')}</p>
<p>{t('Lade Nodetypen…')}</p>
</div>
</div>
);

View file

@ -38,9 +38,9 @@ interface CanvasHeaderProps {
function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> {
return {
draft: { label: t('canvasHeader.entwurf'), color: 'var(--warning-color, #ffc107)' },
published: { label: t('canvasHeader.veroeffentlicht'), color: 'var(--success-color, #28a745)' },
archived: { label: t('canvasHeader.archiviert'), color: 'var(--text-secondary, #666)' },
draft: { label: t('Entwurf'), color: 'var(--warning-color, #ffc107)' },
published: { label: t('Veröffentlicht'), color: 'var(--success-color, #28a745)' },
archived: { label: t('Archiviert'), color: 'var(--text-secondary, #666)' },
};
}
@ -153,7 +153,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
<button
type="button"
className={styles.canvasGearBtn}
title={t('canvasHeader.workflowkonfigurationEinstiegStarts')}
title={t('Workflowkonfiguration Einstieg/Starts')}
aria-label="Workflow-Konfiguration"
onClick={onWorkflowSettings}
>
@ -172,7 +172,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton}
onClick={() => setNewMenuOpen((p) => !p)}
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, paddingLeft: 4, paddingRight: 6, borderLeft: '1px solid rgba(0,0,0,0.15)' }}
title={t('canvasHeader.neuAusVorlage')}
title={t('Neu aus Vorlage')}
>
<FaCaretDown style={{ fontSize: '0.7rem' }} />
</button>
@ -216,9 +216,9 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton}
onClick={() => setTemplateMenuOpen((p) => !p)}
disabled={templateSaving}
title={t('canvasHeader.alsVorlageSpeichern')}
title={t('Als Vorlage speichern')}
>
{templateSaving ? <FaSpinner className={styles.spinner} /> : <><FaBookmark style={{ marginRight: 4 }} />{t('canvasHeader.alsVorlage')}</>}
{templateSaving ? <FaSpinner className={styles.spinner} /> : <><FaBookmark style={{ marginRight: 4 }} />{t('Als Vorlage')}</>}
</button>
{templateMenuOpen && (
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 100, background: 'var(--bg-primary, #fff)', border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', minWidth: 180, marginTop: 4 }}>
@ -244,7 +244,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
}}
style={{ padding: '0.4rem', minWidth: 180 }}
>
<option value="">{t('canvasHeader.workflowLaden')}</option>
<option value="">{t('Workflow laden')}</option>
{workflows.map((w) => (
<option key={w.id} value={w.id}>
{w.label}
@ -270,7 +270,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
)}
</button>
{onToggleChat && (
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('canvasHeader.workspacepanelChatsDateienQuellen')}>
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('Workspace-Panel: Chats, Dateien, Quellen')}>
<FaDatabase style={{ marginRight: '0.4rem' }} />
Workspace
</button>
@ -287,7 +287,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
style={{ padding: '0.3rem', minWidth: 140, fontSize: '0.85rem' }}
disabled={versionLoading}
>
<option value="">{t('canvasHeader.aktuelle')}</option>
<option value="">{t('Aktuelle')}</option>
{versions.map((v) => (
<option key={v.id} value={v.id}>
v{v.versionNumber} ({statusBadge[v.status]?.label ?? v.status})
@ -312,7 +312,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton}
onClick={() => onPublishVersion(currentVersion.id)}
disabled={versionLoading}
title={t('canvasHeader.versionVeroeffentlichen')}
title={t('Version veröffentlichen')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
>
<FaCloudUploadAlt style={{ marginRight: 4 }} />
@ -325,7 +325,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton}
onClick={() => onUnpublishVersion(currentVersion.id)}
disabled={versionLoading}
title={t('canvasHeader.veroeffentlichungZuruecknehmen')}
title={t('Veröffentlichung zurücknehmen')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
>
<FaCloudDownloadAlt style={{ marginRight: 4 }} />
@ -338,7 +338,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton}
onClick={() => onArchiveVersion(currentVersion.id)}
disabled={versionLoading}
title={t('canvasHeader.versionArchivieren')}
title={t('Version archivieren')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
>
<FaArchive style={{ marginRight: 4 }} />
@ -351,7 +351,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton}
onClick={onCreateDraft}
disabled={versionLoading}
title={t('canvasHeader.neuenEntwurfErstellen')}
title={t('Neuen Entwurf erstellen')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
>
+ Entwurf
@ -381,10 +381,10 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
}}
>
{executeResult.success ? (
<>{t('canvasHeader.ausfuehrungAbgeschlossen')}</>
<>{t('Ausführung abgeschlossen')}</>
) : (executeResult as { paused?: boolean }).paused ? (
<>
Workflow pausiert. Öffne <strong>{t('canvasHeader.workflowsTasks')}</strong> in der Sidebar, um den
Workflow pausiert. Öffne <strong>{t('Workflows/Tasks')}</strong> in der Sidebar, um den
Task zu bearbeiten.
</>
) : (

View file

@ -189,7 +189,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
<ChatMessageList
messages={messages}
isProcessing={loading}
emptyMessage={t('editorChatPanel.describeWhatYouWantTo')}
emptyMessage={t('Beschreiben Sie, was Sie tun möchten')}
/>
{/* Pending files (from UDB drag/click) */}
@ -279,7 +279,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
value={prompt}
onChange={e => setPrompt(e.target.value)}
onKeyDown={_handleKeyDown}
placeholder={workflowId ? t('editorChatPanel.describeAChange') : t('editorChatPanel.saveWorkflowFirst')}
placeholder={workflowId ? t('Beschreiben Sie eine Änderung') : t('Speichern Sie zuerst den Workflow')}
disabled={!workflowId || loading}
style={{
flex: 1, minHeight: 36, maxHeight: 100, resize: 'vertical',
@ -296,7 +296,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
<button
onClick={() => setShowSourcePicker(prev => !prev)}
disabled={loading || !workflowId}
title={t('editorChatPanel.datenquellenAnhaengen')}
title={t('Datenquellen anhängen')}
style={{
width: 36, height: 36, borderRadius: 8,
border: '1px solid var(--border-color, #ddd)',
@ -401,7 +401,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
background: prompt.trim() && workflowId ? 'var(--primary-color, #F25843)' : '#ccc',
color: '#fff', cursor: prompt.trim() && workflowId ? 'pointer' : 'default',
fontWeight: 600, fontSize: 12,
}}>{t('editorChatPanel.send')}</button>
}}>{t('Senden')}</button>
)}
</div>
</div>

View file

@ -642,7 +642,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
style={{ cursor: 'pointer' }}
role="button"
tabIndex={-1}
aria-label={t('flowCanvas.verbindungAuswaehlenEntfZumLoeschen')}
aria-label={t('Verbindung auswählen, Entf zum Löschen')}
>
<path
d={pathD}
@ -661,7 +661,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
pointerEvents="none"
/>
{isWarning && !isSelected && (
<title>{t('flowCanvas.typeMismatchWarningOutputType')}</title>
<title>{t('Typeninkompatibilität: Ausgabetyp')}</title>
)}
</g>
);
@ -760,8 +760,8 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
outputLabel ??
(selectedConnectionId && !isOutput
? used
? t('flowCanvas.aktuellesZielKlickenZumAbwaehlen')
: t('flowCanvas.klickenZumUmleiten')
? t('Aktuelles Ziel klicken, um abzuwählen')
: t('Klicken zum Umleiten')
: undefined)
}
/>
@ -840,7 +840,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
)}
{nodes.length === 0 && (
<div className={styles.canvasPlaceholder}>
<p>{t('flowCanvas.nodesAusDerListeLinks')}</p>
<p>{t('Nodes aus der Liste links')}</p>
</div>
)}
</div>

View file

@ -82,13 +82,13 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
<div className={styles.nodeConfigPanel}>
{showNameField && (
<div className={styles.nodeConfigNameRow}>
<label htmlFor="node-config-name">{t('nodeConfigPanel.bezeichnung')}</label>
<label htmlFor="node-config-name">{t('Bezeichnung')}</label>
<input
id="node-config-name"
type="text"
value={node.title ?? ''}
onChange={(e) => onNodeUpdate(node.id, { title: e.target.value })}
placeholder={t('nodeConfigPanel.zbKundenformularPruefenLand')}
placeholder={t('z.B. Kundenformular prüfen, Land')}
/>
<p className={styles.nodeConfigNameHint}>
Wird im Data Picker angezeigt, um diesen Node zu identifizieren.

View file

@ -88,7 +88,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({ nodeTypes,
<input
type="text"
className={styles.sidebarSearch}
placeholder={t('nodeSidebar.nodesDurchsuchen')}
placeholder={t('Nodes durchsuchen')}
value={filter}
onChange={(e) => onFilterChange(e.target.value)}
/>

View file

@ -183,7 +183,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
</div>
{steps.length === 0 && !loading && (
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('runTracingPanel.noStepsRecordedYet')}</div>
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('Noch keine Schritte aufgezeichnet')}</div>
)}
{steps.map((step: any) => {
const startStr = _formatTimestamp(step.startedAt);
@ -222,7 +222,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
</span>
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{step.retryCount > 0 && (
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('runTracingPanel.retryCount')}>
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}>
{step.retryCount}x retry
</span>
)}

View file

@ -15,10 +15,10 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
/** Vier Einstiege; bei „Immer aktiv“ folgt später die Listener-Konfiguration (E-Mail, Webhook, …). */
function _getKindOptions(t: (key: string) => string): { value: string; label: string }[] {
return [
{ value: 'manual', label: t('workflowConfigurationModal.manuellerTrigger') },
{ value: 'form', label: t('workflowConfigurationModal.formular') },
{ value: 'schedule', label: t('workflowConfigurationModal.zeitplan') },
{ value: 'always_on', label: t('workflowConfigurationModal.immerAktiv') },
{ value: 'manual', label: t('Manueller Trigger') },
{ value: 'form', label: t('Formular') },
{ value: 'schedule', label: t('Zeitplan') },
{ value: 'always_on', label: t('Immer aktiv') },
];
}
@ -89,7 +89,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
className={styles.workflowModalInput}
value={titleDe}
onChange={(e) => setTitleDe(e.target.value)}
placeholder={t('workflowConfigurationModal.zBAngebotAnlegen')}
placeholder={t('z.B. Angebot anlegen')}
/>
<div className={styles.workflowModalRadioGroup} role="radiogroup" aria-label="Einstiegsart">

View file

@ -76,7 +76,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<div className={styles.formFieldRowHeader}>
<span
className={styles.formFieldDragHandle}
title={t('formNodeConfig.zumVerschiebenZiehen')}
title={t('Zum Verschieben ziehen')}
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', String(i));
@ -133,8 +133,8 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<option value="number">Number</option>
<option value="date">Date</option>
<option value="boolean">Checkbox</option>
<option value="clickup_tasks">{t('formNodeConfig.clickupaufgabeReferenz')}</option>
<option value="clickup_status">{t('formNodeConfig.clickupstatusListe')}</option>
<option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
</select>
<label className={styles.formFieldRequiredLabel}>
<input
@ -151,7 +151,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<button
type="button"
onClick={() => removeField(i)}
title={t('formNodeConfig.feldEntfernen')}
title={t('Feld entfernen')}
className={styles.formFieldRemoveButton}
>
<FaTimes />
@ -198,7 +198,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
Listen-ID (verknüpfte Liste / Ziel-Liste)
</label>
<input
placeholder={t('formNodeConfig.zBAusClickupurlList123456789')}
placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
value={f.clickupListId ?? ''}
onChange={(e) => {
const next = [...fields];

View file

@ -170,7 +170,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
onChange={(e) => onChange(e.target.value)}
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc' }}
>
<option value="">{t('index.selectConnection')}</option>
<option value="">{t('Verbindung wählen')}</option>
{connections.map((c) => (
<option key={c.id} value={c.id}>{c.label}</option>
))}
@ -215,16 +215,16 @@ const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
<option value="eq">equals</option>
<option value="neq">{t('index.notEquals')}</option>
<option value="neq">{t('ungleich')}</option>
<option value="contains">contains</option>
<option value="gt">{t('index.greaterThan')}</option>
<option value="lt">{t('index.lessThan')}</option>
<option value="gt">{t('größer als')}</option>
<option value="lt">{t('kleiner als')}</option>
</select>
<input type="text" value={String(c.value ?? '')} onChange={(e) => updateCase(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
<button onClick={() => removeCase(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div>
))}
<button onClick={addCase} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addCase')}</button>
<button onClick={addCase} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('Fall hinzufügen')}</button>
</div>
);
};
@ -260,7 +260,7 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
<button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div>
))}
<button onClick={addField} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addField')}</button>
<button onClick={addField} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('Feld hinzufügen')}</button>
</div>
);
};
@ -285,7 +285,7 @@ const KeyValueRowsEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
<button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div>
))}
<button onClick={addRow} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addRow')}</button>
<button onClick={addRow} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('Zeile hinzufügen')}</button>
</div>
);
};
@ -302,7 +302,7 @@ const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) =
placeholder={t('index.5')}
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }}
/>
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('index.cronMinHourDayMonth')}</p>
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('Cron: Min Stunde Tag Monat')}</p>
</div>
);
};
@ -317,14 +317,14 @@ const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange
<div style={{ display: 'flex', gap: 4 }}>
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
<option value="eq">equals</option>
<option value="neq">{t('index.notEquals')}</option>
<option value="gt">{t('index.greaterThan')}</option>
<option value="lt">{t('index.lessThan')}</option>
<option value="neq">{t('ungleich')}</option>
<option value="gt">{t('größer als')}</option>
<option value="lt">{t('kleiner als')}</option>
<option value="contains">contains</option>
<option value="empty">{t('index.isEmpty')}</option>
<option value="not_empty">{t('index.isNotEmpty')}</option>
<option value="is_true">{t('index.isTrue')}</option>
<option value="is_false">{t('index.isFalse')}</option>
<option value="empty">{t('ist leer')}</option>
<option value="not_empty">{t('ist nicht leer')}</option>
<option value="is_true">{t('ist wahr')}</option>
<option value="is_false">{t('ist falsch')}</option>
</select>
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
</div>
@ -347,13 +347,13 @@ const MappingTableEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
{mappings.map((m: Record<string, unknown>, i: number) => (
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<input type="text" placeholder={t('index.sourceField')} value={String(m.sourceField ?? '')} onChange={(e) => updateMapping(i, 'sourceField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
<input type="text" placeholder={t('Quellfeld')} value={String(m.sourceField ?? '')} onChange={(e) => updateMapping(i, 'sourceField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
<span style={{ alignSelf: 'center' }}></span>
<input type="text" placeholder={t('index.outputField')} value={String(m.outputField ?? '')} onChange={(e) => updateMapping(i, 'outputField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
<input type="text" placeholder={t('Ausgabefeld')} value={String(m.outputField ?? '')} onChange={(e) => updateMapping(i, 'outputField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
<button onClick={() => removeMapping(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div>
))}
<button onClick={addMapping} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addMapping')}</button>
<button onClick={addMapping} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('Zuordnung hinzufügen')}</button>
</div>
);
};
@ -369,13 +369,13 @@ const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, on
<input type="text" placeholder="Field" value={String(cond.field ?? '')} onChange={(e) => update('field', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
<option value="eq">equals</option>
<option value="neq">{t('index.notEquals')}</option>
<option value="neq">{t('ungleich')}</option>
<option value="contains">contains</option>
<option value="startsWith">{t('index.startsWith')}</option>
<option value="isEmpty">{t('index.isEmpty')}</option>
<option value="isNotEmpty">{t('index.isNotEmpty')}</option>
<option value="gt">{t('index.greaterThan')}</option>
<option value="lt">{t('index.lessThan')}</option>
<option value="startsWith">{t('beginnt mit')}</option>
<option value="isEmpty">{t('ist leer')}</option>
<option value="isNotEmpty">{t('ist nicht leer')}</option>
<option value="gt">{t('größer als')}</option>
<option value="lt">{t('kleiner als')}</option>
</select>
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
</div>

View file

@ -100,7 +100,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
<div className={styles.ifElseConditionEditor}>
<div className={styles.ifElseConditionRow}>
<label>Datenquelle</label>
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('ifElseNodeConfig.formularfeldWaehlen')} />
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
</div>
<div className={styles.ifElseConditionRow}>
<label>Vergleich</label>
@ -120,7 +120,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
value={String(value ?? '')}
onChange={(e) => handleValueChange(e.target.value)}
>
<option value="">{t('ifElseNodeConfig.mimetypeWaehlen')}</option>
<option value="">{t('MIME-Typ wählen')}</option>
{mimeTypeOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label} ({o.value})
@ -142,8 +142,8 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
: fieldType === 'date'
? 'TT.MM.JJJJ'
: isMimeTypeRef
? t('ifElseNodeConfig.zbApplicationpdf')
: t('ifElseNodeConfig.zbCh')
? t('z.B. application/pdf')
: t('z.B. ch')
}
/>
)}

View file

@ -145,8 +145,8 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
<div className={styles.dataPickerOverlay} onClick={onClose}>
<div className={styles.dataPickerModal} onClick={(e) => e.stopPropagation()}>
<div className={styles.dataPickerHeader}>
<h4 className={styles.dataPickerTitle}>{t('dataPicker.datenquelleWaehlen')}</h4>
<button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label={t('dataPicker.schliessen')}>
<h4 className={styles.dataPickerTitle}>{t('Datenquelle wählen')}</h4>
<button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label={t('Schließen')}>
×
</button>
</div>
@ -187,7 +187,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
return node?.type !== 'trigger.manual';
});
if (filteredIds.length === 0 && Object.keys(systemVars).length === 0) {
return <p className={styles.dataPickerEmpty}>{t('dataPicker.keineVorherigenNodesVerfuegbar')}</p>;
return <p className={styles.dataPickerEmpty}>{t('Keine vorherigen Nodes verfügbar')}</p>;
}
return filteredIds.map((nodeId) => {
const node = nodes.find((n) => n.id === nodeId);

View file

@ -58,7 +58,7 @@ export const DynamicValueField: React.FC<DynamicValueFieldProps> = ({ paramKey,
return (
<div className={styles.dynamicValueField}>
<label>{label}</label>
<p className={styles.dynamicValueEmptyHint}>{t('dynamicValueField.keineVorherigenNodesVerfuegbar')}</p>
<p className={styles.dynamicValueEmptyHint}>{t('Keine vorherigen Nodes verfügbar')}</p>
</div>
);
}

View file

@ -85,7 +85,7 @@ export const HybridStaticRefField: React.FC<HybridStaticRefFieldProps> = ({ labe
<StatischKontextSelect
value={value}
onChange={onChange}
placeholder={t('hybridStaticRefField.quelleWaehlen')}
placeholder={t('Quelle wählen')}
pathPickMode={pathPickMode}
/>
</div>

View file

@ -184,7 +184,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
return (
<div className={styles.ifElseConditionRow}>
<label>{t('loopItemsSelect.datenquelleFuerIteration')}</label>
<label>{t('Datenquelle für Iteration')}</label>
<select
value={currentValue}
onChange={(e) => {

View file

@ -19,7 +19,7 @@ const FORM_FIELD_TYPES = ['text', 'number', 'email', 'date', 'boolean', 'clickup
function _parseFields(params: Record<string, unknown>, t: (key: string) => string): FormField[] {
const raw = params.formFields;
if (!Array.isArray(raw)) return [{ name: 'field1', label: t('formStartNodeConfig.field1'), type: 'text' }];
if (!Array.isArray(raw)) return [{ name: 'field1', label: t('Feld 1'), type: 'text' }];
return raw.map((f, i) => {
if (f && typeof f === 'object' && !Array.isArray(f)) {
const o = f as Record<string, unknown>;
@ -62,7 +62,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<div key={idx} className={styles.formFieldRow}>
<input
className={styles.startsInput}
placeholder={t('formStartNodeConfig.namePayloadkey')}
placeholder={t('Name (Payload-Key)')}
value={f.name}
onChange={(e) => {
const next = [...fields];
@ -99,7 +99,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<option value="email">E-Mail</option>
<option value="date">Datum</option>
<option value="boolean">Ja/Nein</option>
<option value="clickup_status">{t('formStartNodeConfig.clickupstatusListe')}</option>
<option value="clickup_status">{t('ClickUp-Status Liste')}</option>
</select>
<button
type="button"
@ -114,7 +114,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
type="button"
className={styles.startsAddBtn}
onClick={() =>
setFields([...fields, { name: `field${fields.length + 1}`, label: t('formStartNodeConfig.newField'), type: 'text' }])
setFields([...fields, { name: `field${fields.length + 1}`, label: t('Neues Feld'), type: 'text' }])
}
>
+ Feld

View file

@ -20,11 +20,11 @@ import { useLanguage } from '../../../../providers/language/LanguageContext';
function _getModeOptions(t: (key: string) => string): { value: ScheduleMode; title: string; subtitle: string }[] {
return [
{ value: 'daily', title: t('scheduleStartNodeConfig.taeglich'), subtitle: t('scheduleStartNodeConfig.jedenTagZurGleichenZeit') },
{ value: 'weekdays', title: t('scheduleStartNodeConfig.werktage'), subtitle: t('scheduleStartNodeConfig.montagBisFreitag') },
{ value: 'weekly', title: t('scheduleStartNodeConfig.bestimmteTage'), subtitle: t('scheduleStartNodeConfig.wochentageAuswaehlen') },
{ value: 'calendar', title: t('scheduleStartNodeConfig.einAndererZeitraum'), subtitle: t('scheduleStartNodeConfig.monatlichOderJaehrlich') },
{ value: 'interval', title: t('scheduleStartNodeConfig.intervall'), subtitle: t('scheduleStartNodeConfig.inRegelmaessigenAbstaenden') },
{ value: 'daily', title: t('Täglich'), subtitle: t('Jeden Tag zur gleichen Zeit') },
{ value: 'weekdays', title: t('Werktage'), subtitle: t('Montag bis Freitag') },
{ value: 'weekly', title: t('Bestimmte Tage'), subtitle: t('Wochentage auswählen') },
{ value: 'calendar', title: t('Ein anderer Zeitraum'), subtitle: t('Monatlich oder jährlich') },
{ value: 'interval', title: t('Intervall'), subtitle: t('In regelmäßigen Abständen') },
];
}
@ -45,11 +45,11 @@ const MONTH_NAMES_DE = [
function _getIntervalUnits(t: (key: string) => string): { value: IntervalUnit; label: string; title: string }[] {
return [
{ value: 'seconds', label: t('scheduleStartNodeConfig.sek'), title: t('scheduleStartNodeConfig.sekunden') },
{ value: 'minutes', label: t('scheduleStartNodeConfig.min'), title: t('scheduleStartNodeConfig.minuten') },
{ value: 'hours', label: t('scheduleStartNodeConfig.h'), title: t('scheduleStartNodeConfig.stunden') },
{ value: 'days', label: t('scheduleStartNodeConfig.d'), title: t('scheduleStartNodeConfig.tage') },
{ value: 'years', label: t('scheduleStartNodeConfig.a'), title: t('scheduleStartNodeConfig.jahre') },
{ value: 'seconds', label: t('Sekunde'), title: t('Sekunden') },
{ value: 'minutes', label: t('Minute'), title: t('Minuten') },
{ value: 'hours', label: t('Stunde'), title: t('Stunden') },
{ value: 'days', label: t('Tag'), title: t('Tage') },
{ value: 'years', label: t('Jahr'), title: t('Jahre') },
];
}
@ -403,7 +403,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
{o.value === 'interval' && (
<div className={styles.scheduleIntervalRow}>
<span className={styles.scheduleFieldLabel}>{t('scheduleStartNodeConfig.alle')}</span>
<span className={styles.scheduleFieldLabel}>{t('Alle')}</span>
<input
type="number"
min={1}

View file

@ -114,7 +114,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
onChange={(e) => handleCaseValueChange(index, e.target.value)}
className={styles.startsInput}
>
<option value="">{t('switchNodeConfig.mimetypeWaehlen')}</option>
<option value="">{t('MIME-Typ wählen')}</option>
{mimeTypeOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label} ({o.value})
@ -154,9 +154,9 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
}}
className={styles.startsInput}
>
<option value="">{t('switchNodeConfig.waehlen')}</option>
<option value="true">{t('switchNodeConfig.jaWahr')}</option>
<option value="false">{t('switchNodeConfig.neinFalsch')}</option>
<option value="">{t('hlen')}</option>
<option value="true">{t('Ja (true)')}</option>
<option value="false">{t('Nein (false)')}</option>
</select>
);
}
@ -189,24 +189,24 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
<RefSourceSelect
value={ref}
onChange={handleRefChange}
placeholder={t('switchNodeConfig.feldZumVergleichenWaehlen')}
placeholder={t('Feld zum Vergleich wählen')}
/>
</div>
{!ref && (
<div className={styles.ifElseConditionRow}>
<label>{t('switchNodeConfig.festerWertFallsKeineReferenz')}</label>
<label>{t('Fester Wert (ohne Referenz)')}</label>
<input
type="text"
value={String(staticValue ?? '')}
onChange={(e) => handleStaticValueChange(e.target.value)}
placeholder={t('switchNodeConfig.zbChOder42')}
placeholder={t('z. B. CH oder 42')}
/>
</div>
)}
<div className={styles.ifElseConditionRow}>
<label>{t('switchNodeConfig.faelleReihenfolgeAusgang')}</label>
<label>{t('Fälle / Reihenfolge / Ausgabe')}</label>
<div className={styles.formFieldsList}>
{cases.map((c, i) => {
const opDef = operators.find((o) => o.value === c.operator) ?? operators[0];

View file

@ -270,7 +270,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
</button>
)}
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('folderTree.dateienLoeschen')}`}>
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('Dateien löschen')}`}>
<FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
</button>
@ -278,7 +278,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
</>
) : (
(sel.onDeleteFile || sel.onDeleteFiles) && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('folderTree.loeschen')}>
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('schen')}>
<FaTrash />
</button>
)
@ -311,7 +311,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
e.stopPropagation();
sel.onNeutralizeToggle?.(file.id, !file.neutralize);
}}
title={file.neutralize ? t('folderTree.neutralisierungAktivKlickenZumDeaktivieren') : t('folderTree.neutralisierungAusKlickenZumAktivieren')}
title={file.neutralize ? t('Neutralisierung aktiv, klicken zum Deaktivieren') : t('Neutralisierung aus, klicken zum Aktivieren')}
style={{ fontSize: 14, opacity: file.neutralize ? 1 : 0.4 }}
>
{'\uD83D\uDD12'}
@ -381,7 +381,7 @@ function _TreeNode({
const _handleAdd = useCallback(async (e: React.MouseEvent) => {
e.stopPropagation();
if (!onCreateFolder) return;
const name = await promptFolderName('Neuer Ordnername:', { title: t('folderTree.newFolder'), placeholder: 'Ordnername' });
const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
if (name?.trim()) {
await onCreateFolder(name.trim(), node.id);
if (!expandedIds.has(node.id)) onToggle(node.id);
@ -500,37 +500,37 @@ function _TreeNode({
)}
<span className={styles.actions}>
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title={t('folderTree.ordnerHerunterladenZip')}>
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title={t('Ordner herunterladen (ZIP)')}>
<FaDownload />
</button>
)}
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={_handleAdd} title={t('folderTree.neuerUnterordner')}>
<button className={styles.actionBtn} onClick={_handleAdd} title={t('Neuer Unterordner')}>
<FaPlus />
</button>
)}
{onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('folderTree.umbenennen')}>
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('Umbenennen')}>
<FaPen />
</button>
)}
{isMultiSelected && sel.selectedItemIds.size > 1 ? (
<>
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} ${t('folderTree.ordnerLoeschen')}`}>
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} ${t('Ordner löschen')}`}>
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
</button>
)}
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('folderTree.dateienLoeschen')}`}>
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('Dateien löschen')}`}>
<FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
</button>
)}
</>
) : onDeleteFolder && (
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('folderTree.loeschen')}>
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('schen')}>
<FaTrash />
</button>
)}
@ -756,7 +756,7 @@ export default function FolderTree({
<span className={`${styles.folderName} ${styles.rootLabel}`}>(Global)</span>
<span className={styles.rootActions}>
{onRefresh && (
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('folderTree.aktualisieren')}>
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('Aktualisieren')}>
<FaSyncAlt />
</button>
)}
@ -765,10 +765,10 @@ export default function FolderTree({
className={styles.actionBtn}
onClick={async (e) => {
e.stopPropagation();
const name = await promptFolderName('Neuer Ordnername:', { title: t('folderTree.newFolder'), placeholder: 'Ordnername' });
const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
if (name?.trim()) await onCreateFolder(name.trim(), null);
}}
title={t('folderTree.neuerOrdner')}
title={t('Neuer Ordner')}
>
<FaPlus />
</button>

View file

@ -119,7 +119,7 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
const { t } = useLanguage();
if (!section.data?.length) {
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
return <div className={styles.noData}>{t('Keine Daten')}</div>;
}
const chartData = section.data.map(d => ({
@ -165,7 +165,7 @@ const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode:
const { t } = useLanguage();
if (!section.data?.length) {
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
return <div className={styles.noData}>{t('Keine Daten')}</div>;
}
const maxValue = Math.max(...section.data.map(d => d.value), 0.01);
@ -198,7 +198,7 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
const { t } = useLanguage();
if (!section.data?.length) {
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
return <div className={styles.noData}>{t('Keine Daten')}</div>;
}
const formatter = section.formatValue || ((v: number) => _defaultFormatCurrency(v, currencyCode));
@ -246,7 +246,7 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
const { t } = useLanguage();
if (!section.data?.length) {
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
return <div className={styles.noData}>{t('Keine Daten')}</div>;
}
const formatter = section.formatValue || ((v: number) => _defaultFormatCurrency(v, currencyCode));
@ -294,7 +294,7 @@ const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string):
const { t } = useLanguage();
if (!section.data?.length) {
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
return <div className={styles.noData}>{t('Keine Daten')}</div>;
}
const formatter = section.formatValue || ((v: number) => _defaultFormatCurrency(v, currencyCode));
@ -355,7 +355,7 @@ const _ReportTableSection: React.FC<ReportTableSectionProps> = ({ section, curre
const [showAll, setShowAll] = useState(false);
if (!section.rows?.length) {
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
return <div className={styles.noData}>{t('Keine Daten')}</div>;
}
const maxRows = section.maxRows || 0;
@ -469,7 +469,7 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
case 'table':
return <_ReportTableSection section={section} currencyCode={currencyCode} />;
default:
return <div className={styles.noData}>{t('formGeneratorReport.unbekannterSektionstyp')}</div>;
return <div className={styles.noData}>{t('Unbekannter Sektionstyp')}</div>;
}
};
@ -541,11 +541,11 @@ const _Toolbar: React.FC<ToolbarProps> = ({
const _renderPeriodLabel = (p: ReportPeriod): string => {
const labels: Record<ReportPeriod, string> = {
day: t('formGeneratorReport.tagesansicht'),
week: t('formGeneratorReport.wochenansicht'),
month: t('formGeneratorReport.monatsansicht'),
quarter: t('formGeneratorReport.quartalsansicht'),
year: t('formGeneratorReport.jahresansicht')
day: t('Tagesansicht'),
week: t('Wochenansicht'),
month: t('Monatsansicht'),
quarter: t('Quartalsansicht'),
year: t('Jahresansicht')
};
return labels[p] || p;
};
@ -555,7 +555,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
{/* Period Selector */}
{hasPeriod && (
<div className={styles.toolbarGroup}>
<span className={styles.toolbarLabel}>{t('formGeneratorReport.zeitraum')}</span>
<span className={styles.toolbarLabel}>{t('Zeitraum')}</span>
<select
className={styles.select}
value={filterState.period || periodSelector!.defaultPeriod}
@ -600,7 +600,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
{/* Date Range */}
{hasDateRange && (
<div className={styles.toolbarGroup}>
<span className={styles.toolbarLabel}>{t('formGeneratorReport.von')}</span>
<span className={styles.toolbarLabel}>{t('Von')}</span>
<input
type="date"
className={styles.dateInput}
@ -728,7 +728,7 @@ export const FormGeneratorReport: React.FC<FormGeneratorReportProps> = ({
filterState={filterState}
onFilterStateChange={_handleFilterStateChange}
/>
<div className={styles.loadingContainer}>{t('formGeneratorReport.ladeDaten')}</div>
<div className={styles.loadingContainer}>{t('Daten laden')}</div>
</div>
);
}
@ -750,7 +750,7 @@ export const FormGeneratorReport: React.FC<FormGeneratorReportProps> = ({
filterState={filterState}
onFilterStateChange={_handleFilterStateChange}
/>
<div className={styles.noData}>{noDataMessage || t('formGeneratorReport.keineDatenVerfuegbar')}</div>
<div className={styles.noData}>{noDataMessage || t('Keine Daten verfügbar')}</div>
</div>
);
}

View file

@ -143,7 +143,7 @@ export const UserSection: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowLegalModal(false)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2>{t('userSection.rechtlicheHinweise')}</h2>
<h2>{t('Legal notices')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowLegalModal(false)}
@ -153,25 +153,25 @@ export const UserSection: React.FC = () => {
</div>
<div className={styles.modalContent}>
<div className={styles.legalSection}>
<h3>{t('userSection.datenverarbeitungUndKinutzung')}</h3>
<h3>{t('Datenverarbeitung und KI-Nutzung')}</h3>
<h4>{t('userSection.1EinwilligungZurDatenverarbeitung')}</h4>
<p>{t('userSection.mitDerNutzungDieserAnwendung')}</p>
<p>{t('By using this application')}</p>
<ul>
<li>{t('userSection.sieAutorisierenDieErfassungVerarbeitung')}</li>
<li>{t('userSection.nutzerdatenKoennenAnDrittanbieterVon')}</li>
<li>{t('userSection.dieseEinwilligungErstrecktSichAuf')}</li>
<li>{t('You authorize the collection and processing')}</li>
<li>{t('User data may be shared with third-party providers of')}</li>
<li>{t('This consent extends to')}</li>
</ul>
<h4>{t('userSection.2AnerkennungDerKiverarbeitungsrisiken')}</h4>
<ul>
<li>{t('userSection.kisystemeKoennenUnerwarteteOderUngenaue')}</li>
<li>{t('userSection.kidiensteKoennenDatenGemaessIhren')}</li>
<li>{t('userSection.trotzSicherheitsmassnahmenKoennenDatenAnfaellig')}</li>
<li>{t('AI systems may produce unexpected or inaccurate')}</li>
<li>{t('AI services can process data according to their')}</li>
<li>{t('Despite security measures, data may be vulnerable')}</li>
</ul>
<h4>{t('userSection.3Haftungsausschluss')}</h4>
<p>{t('userSection.imGroesstmoeglichenUmfangVerzichtenSie')}</p>
<p>{t('To the fullest extent possible, you waive')}</p>
</div>
<div className={styles.legalLinks}>

View file

@ -159,7 +159,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
<div className={styles.dropdown}>
{/* Header */}
<div className={styles.header}>
<h3>{t('notificationBell.benachrichtigungen')}</h3>
<h3>{t('Benachrichtigungen')}</h3>
{visibleNotifications.some(n => n.status === 'unread') && (
<button
className={styles.markAllRead}
@ -173,7 +173,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
{/* Content */}
<div className={styles.content}>
{loading && visibleNotifications.length === 0 && (
<div className={styles.loading}>{t('notificationBell.lade')}</div>
<div className={styles.loading}>{t('Lade')}</div>
)}
{error && (
@ -183,7 +183,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
{!loading && !error && visibleNotifications.length === 0 && (
<div className={styles.empty}>
<FaBell className={styles.emptyIcon} />
<p>{t('notificationBell.keineBenachrichtigungen')}</p>
<p>{t('Keine Benachrichtigungen')}</p>
</div>
)}
@ -253,7 +253,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
<button
className={styles.dismissButton}
onClick={(e) => handleDismiss(notification, e)}
aria-label={t('notificationBell.schliessen')}
aria-label={t('Schließen')}
>
<FaTimes />
</button>

View file

@ -97,22 +97,22 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
onboardingSteps.push({
id: 'mandate',
label: t('onboardingAssistant.setupMandate'),
label: t('Mandat einrichten'),
description: hasAdminMandate
? 'Dein Mandant ist eingerichtet.'
: hasFeature
? t('onboardingAssistant.duBistMitgliedEinesMandanten')
: t('onboardingAssistant.erstelleDeinenArbeitsbereich'),
? t('Du bist Mitglied eines Mandanten')
: t('Erstelle deinen Arbeitsbereich'),
completed: mandateStepDone,
action: mandateStepDone ? undefined : () => setShowWizard(true),
});
onboardingSteps.push({
id: 'feature',
label: t('onboardingAssistant.activateFirstFeature'),
label: t('Aktiviere dein erstes Feature'),
description: hasFeature
? t('onboardingAssistant.duHastAktiveFeatures')
: t('onboardingAssistant.aktiviereDeinErstesFeatureIm'),
? t('Du hast aktive Features')
: t('Aktiviere dein erstes Feature im'),
completed: hasFeature,
action: hasFeature ? undefined : () => navigate('/store'),
});
@ -126,10 +126,10 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
onboardingSteps.push({
id: 'connection',
label: t('onboardingAssistant.connectFirstDataSource'),
label: t('Verbinde deine erste Datenquelle'),
description: hasConnection
? t('onboardingAssistant.duHastVerbindungenEingerichtet')
: t('onboardingAssistant.verbindeDeineErsteDatenquelle'),
? t('Du hast Verbindungen eingerichtet')
: t('Verbinde deine erste Datenquelle'),
completed: hasConnection,
action: hasConnection ? undefined : () => navigate('/basedata/connections'),
});
@ -147,10 +147,10 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
const chatAction = workspaceInstancePath ? () => navigate(workspaceInstancePath!) : undefined;
onboardingSteps.push({
id: 'chat',
label: t('onboardingAssistant.startFirstAiChat'),
label: t('Starte deinen ersten AI-Chat'),
description: hasChat
? t('onboardingAssistant.duHastBereitsChatsGestartet')
: t('onboardingAssistant.starteDeinenErstenChatMit'),
? t('Du hast bereits Chats gestartet')
: t('Starte deinen ersten Chat mit'),
completed: hasChat,
action: hasChat ? undefined : chatAction,
});
@ -213,7 +213,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
<div>
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('onboardingAssistant.willkommenBeiPoweron')}</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)' }}>
{completedCount} von {steps.length} Schritten abgeschlossen
</p>

View file

@ -24,7 +24,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
companyName: mandateName.trim() || undefined,
});
if (res.data?.alreadyProvisioned) {
setError(t('onboardingWizard.duHastBereitsEinenMandanten'));
setError(t('Du hast bereits einen Mandanten'));
return;
}
window.dispatchEvent(new CustomEvent('features-changed'));
@ -46,7 +46,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
background: 'var(--bg-primary, #fff)', borderRadius: '12px', padding: '32px',
maxWidth: '480px', width: '90%', boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
}}>
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>{t('onboardingWizard.mandantErstellen')}</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' }}>
Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.
</p>
@ -60,7 +60,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
<input type="radio" name="plan" checked={planKey === 'TRIAL_7D'}
onChange={() => setPlanKey('TRIAL_7D')} />
<div>
<strong>{t('onboardingWizard.kostenlosTesten')}</strong>
<strong>{t('Kostenlos testen')}</strong>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
7 Tage gratis, danach flexibel upgraden
</div>
@ -75,7 +75,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
<input type="radio" name="plan" checked={planKey === 'STANDARD_MONTHLY'}
onChange={() => setPlanKey('STANDARD_MONTHLY')} />
<div>
<strong>{t('onboardingWizard.standardMonatlich')}</strong>
<strong>{t('Standard monatlich')}</strong>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
Team-Workspace mit vollem Funktionsumfang
</div>
@ -90,7 +90,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
<input
type="text" value={mandateName}
onChange={(e) => setMandateName(e.target.value)}
placeholder={t('onboardingWizard.zBFirmennameOderProjektname')}
placeholder={t('z.B. Firmenname oder Projektname')}
style={{
width: '100%', padding: '10px 12px', borderRadius: '6px',
border: '1px solid var(--border, #d1d5db)', fontSize: '1rem',
@ -114,7 +114,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
background: 'var(--accent, #4f46e5)', color: '#fff', cursor: 'pointer',
opacity: loading ? 0.6 : 1,
}}>
{loading ? t('onboardingWizard.wirdEingerichtet') : t('onboardingWizard.mandantErstellen')}
{loading ? t('Wird eingerichtet') : t('Mandant erstellen')}
</button>
</div>
</div>

View file

@ -143,7 +143,7 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
disabled={disabled || loading}
className={styles.select}
>
<option value="">{t('providerSelector.auto')}</option>
<option value="">{t('Auto')}</option>
{providerOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
@ -282,7 +282,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
className={styles.triggerButton}
onClick={() => setIsExpanded(!isExpanded)}
disabled={disabled}
title={t('providerSelector.providerAuswaehlen')}
title={t('Provider auswählen')}
>
<span className={styles.buttonIcon}>{summaryIcon}</span>
</button>
@ -303,7 +303,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
</div>
{loading ? (
<div className={styles.loading}>{t('providerSelector.lade')}</div>
<div className={styles.loading}>{t('Laden')}</div>
) : (
<div className={styles.checkboxList}>
{allowedProviders.map((provider) => (
@ -348,7 +348,7 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
}) => {
const { t } = useLanguage();
if (providers.length === 0) {
return <span className={styles.allProviders}>{t('providerSelector.alleProvider')}</span>;
return <span className={styles.allProviders}>{t('Alle Provider')}</span>;
}
return (

View file

@ -80,7 +80,7 @@ export const QuickActionBoard: React.FC<QuickActionBoardProps> = ({
if (loading) {
return (
<div className={styles.board}>
<h3 className={styles.boardTitle}>{t('quickActions.title')}</h3>
<h3 className={styles.boardTitle}>{t('Schnellaktionen')}</h3>
<div className={styles.grid}>
{[1, 2, 3, 4].map((i) => (
<div key={i} className={`${styles.card} ${styles.cardSkeleton}`}>
@ -106,7 +106,7 @@ export const QuickActionBoard: React.FC<QuickActionBoardProps> = ({
if (!grouped || !categories || categories.length === 0) {
return (
<div className={styles.board}>
<h3 className={styles.boardTitle}>{t('quickActions.title')}</h3>
<h3 className={styles.boardTitle}>{t('Schnellaktionen')}</h3>
<div className={styles.grid}>
{actions.map((action) => (
<button
@ -135,7 +135,7 @@ export const QuickActionBoard: React.FC<QuickActionBoardProps> = ({
return (
<div className={styles.board}>
<h3 className={styles.boardTitle}>{t('quickActions.title')}</h3>
<h3 className={styles.boardTitle}>{t('Schnellaktionen')}</h3>
{sortedCategories.map((cat) => {
const catActions = actionsByCategory.get(cat.id);
if (!catActions || catActions.length === 0) return null;

View file

@ -47,20 +47,20 @@ function _getImportModes(t: (key: string) => string): { value: ImportMode; label
return [
{
value: 'merge',
label: t('rbacExportImport.zusammenfuehren'),
description: t('rbacExportImport.bestehendeRegelnAktualisieren'),
label: t('Zusammenführen'),
description: t('Bestehende Regeln aktualisieren'),
icon: <FaCheckCircle style={{ color: '#38a169' }} />,
},
{
value: 'add_only',
label: t('rbacExportImport.nurHinzufuegen'),
description: t('rbacExportImport.nurNeueRegelnHinzufuegen'),
label: t('Nur hinzufügen'),
description: t('Nur neue Regeln hinzufügen'),
icon: <FaInfoCircle style={{ color: '#3182ce' }} />,
},
{
value: 'replace',
label: t('rbacExportImport.ersetzen'),
description: t('rbacExportImport.alleBestehendenRegelnLoeschen'),
label: t('Ersetzen'),
description: t('Alle bestehenden Regeln löschen'),
icon: <FaExclamationTriangle style={{ color: '#d69e2e' }} />,
},
];
@ -88,7 +88,7 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
<h5>Scope</h5>
<ul className={styles.previewList}>
<li><strong>Typ:</strong> {data.scope.type}</li>
{data.scope.mandateName && <li><strong>{t('rbacExportImport.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>}
</ul>
</div>
@ -148,21 +148,21 @@ const ImportResult: React.FC<ImportResultProps> = ({ result, onClose }) => {
<FaExclamationTriangle className={styles.resultIcon} />
)}
<h4 className={styles.resultTitle}>
{isSuccess ? t('rbacExportImport.importErfolgreich') : t('rbacExportImport.importFehlgeschlagen')}
{isSuccess ? t('Import erfolgreich') : t('Import fehlgeschlagen')}
</h4>
<button className={styles.closeButton} onClick={onClose}></button>
</div>
<div className={styles.resultContent}>
<ul className={styles.resultStats}>
<li><strong>Modus:</strong> {importModes.find(m => m.value === result.mode)?.label}</li>
<li><strong>{t('rbacExportImport.rollenErstellt')}</strong> {result.rolesCreated}</li>
<li><strong>{t('rbacExportImport.rollenAktualisiert')}</strong> {result.rolesUpdated}</li>
<li><strong>{t('rbacExportImport.regelnErstellt')}</strong> {result.rulesCreated}</li>
<li><strong>{t('rbacExportImport.regelnAktualisiert')}</strong> {result.rulesUpdated}</li>
<li><strong>{t('Rollen erstellt')}</strong> {result.rolesCreated}</li>
<li><strong>{t('Rollen aktualisiert')}</strong> {result.rolesUpdated}</li>
<li><strong>{t('Regeln erstellt')}</strong> {result.rulesCreated}</li>
<li><strong>{t('Regeln aktualisiert')}</strong> {result.rulesUpdated}</li>
</ul>
{result.errors && result.errors.length > 0 && (
<div className={styles.resultErrors}>
<h5>{t('rbacExportImport.fehler')}</h5>
<h5>{t('Fehler')}</h5>
<ul>
{result.errors.map((err, i) => (
<li key={i}>{err}</li>
@ -341,7 +341,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
) : (
<>
<FaUpload className={styles.fileIcon} />
<span>{t('rbacExportImport.jsondateiAuswaehlenOderHierAblegen')}</span>
<span>{t('JSON-Datei auswählen oder hier ablegen')}</span>
</>
)}
</label>
@ -349,7 +349,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
<button
className={styles.clearButton}
onClick={handleClearImport}
title={t('rbacExportImport.dateiEntfernen')}
title={t('Datei entfernen')}
>
<FaTrash />
</button>
@ -367,7 +367,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
{importData && (
<div className={styles.importInfo}>
<div className={styles.importStats}>
<span><strong>{t('rbacExportImport.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>Quelle:</strong> {importData.scope.type}</span>
</div>

View file

@ -276,7 +276,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
<ul ref={suggestionsRef} className={styles.suggestionsList}>
{isLoading && (
<li className={styles.suggestionItem}>
<span className={styles.loadingText}>{t('addressAutocomplete.sucheAdressen')}</span>
<span className={styles.loadingText}>{t('search addresses')}</span>
</li>
)}
{!isLoading && autocompleteError && (
@ -286,7 +286,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
)}
{!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
<li className={styles.suggestionItem}>
<span className={styles.noResultsText}>{t('addressAutocomplete.keineAdressenGefunden')}</span>
<span className={styles.noResultsText}>{t('no addresses found')}</span>
</li>
)}
{!isLoading && suggestions.map((suggestion, index) => (

View file

@ -150,7 +150,7 @@ const AutoScroll: React.FC<AutoScrollProps> = ({ children,
<button
className={styles.scrollToBottomButton}
onClick={handleNewMessageClick}
aria-label={t('autoScroll.scrollToBottom')}
aria-label={t('Zum Ende scrollen')}
>
<span className={styles.scrollArrow}></span>
</button>

View file

@ -42,7 +42,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
<div className={styles.bauvorschriftenGrid}>
{bauvorschriften.ausnuetzungsziffer !== undefined && bauvorschriften.ausnuetzungsziffer !== null && (
<div className={styles.bauvorschriftItem}>
<span className={styles.label}>{t('bauvorschriftenSection.ausnuetzungsziffer')}</span>
<span className={styles.label}>{t('Ausnützungsziffer')}</span>
<span className={styles.value}>{bauvorschriften.ausnuetzungsziffer}%</span>
</div>
)}
@ -54,7 +54,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
)}
{bauvorschriften.gebaeudelaengeMax !== undefined && bauvorschriften.gebaeudelaengeMax !== null && (
<div className={styles.bauvorschriftItem}>
<span className={styles.label}>{t('bauvorschriftenSection.gebaeudelaengeMax')}</span>
<span className={styles.label}>{t('Gebäudelänge max.')}</span>
<span className={styles.value}>{bauvorschriften.gebaeudelaengeMax} m</span>
</div>
)}
@ -66,19 +66,19 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
)}
{bauvorschriften.mehrlaengenzuschlag && (
<div className={styles.bauvorschriftItem}>
<span className={styles.label}>{t('bauvorschriftenSection.mehrlaengenzuschlag')}</span>
<span className={styles.label}>{t('Mehrlängenzuschlag')}</span>
<span className={styles.value}>{bauvorschriften.mehrlaengenzuschlag}</span>
</div>
)}
{bauvorschriften.hoechstmassMax !== undefined && bauvorschriften.hoechstmassMax !== null && (
<div className={styles.bauvorschriftItem}>
<span className={styles.label}>{t('bauvorschriftenSection.hoechstmassMax')}</span>
<span className={styles.label}>{t('Höchstmaß max.')}</span>
<span className={styles.value}>{bauvorschriften.hoechstmassMax} m</span>
</div>
)}
{bauvorschriften.fassadenhoehe && (
<div className={styles.bauvorschriftItem}>
<span className={styles.label}>{t('bauvorschriftenSection.fassadenhoehe')}</span>
<span className={styles.label}>{t('Fassadenhöhe')}</span>
<span className={styles.value}>{bauvorschriften.fassadenhoehe}</span>
</div>
)}

View file

@ -184,7 +184,7 @@ export function ConnectedFilesList({
return (
<div className={styles.container}>
<div className={styles.header}>
<h3 className={styles.title}>{t('connectedFilesList.connectedFiles')}</h3>
<h3 className={styles.title}>{t('Verbundene Dateien')}</h3>
<span className={styles.count}>({allFiles.length})</span>
</div>
<div className={styles.fileList}>
@ -231,14 +231,14 @@ export function ConnectedFilesList({
cursor: onAttach ? 'pointer' : 'default',
userSelect: 'none' // Prevent text selection on click
}}
title={onAttach ? (isPendingFile ? t('connectedFilesList.clickToDetachFromNext') : t('connectedFilesList.clickToAttachForNext')) : undefined}
title={onAttach ? (isPendingFile ? t('Klicken, um vom nächsten zu trennen') : t('Klicken, um für den nächsten anzuhängen')) : undefined}
>
<div className={styles.fileInfo}>
<div className={styles.fileName} title={file.fileName}>
{file.fileName}
{onAttach && (
<span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: '#666' }}>
{isPendingFile ? t('connectedFilesList.clickToDetach') : t('connectedFilesList.clickToAttach')}
{isPendingFile ? t('Klicken, um zu trennen') : t('Klicken, um anzuhängen')}
</span>
)}
</div>
@ -248,7 +248,7 @@ export function ConnectedFilesList({
</span>
{file.source && (
<span className={styles.fileSource}>
{file.source === 'user_uploaded' ? t('connectedFilesList.uploaded') : t('connectedFilesList.aiCreated')}
{file.source === 'user_uploaded' ? t('Hochgeladen') : t('KI erstellt')}
</span>
)}
{isPendingFile && (

View file

@ -194,7 +194,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
if (ring && ring.length >= 3) {
const latLngs = ring.map(([x, y]) => toWgs84(x, y));
const polygon = L.polygon(latLngs, SELECTED_STYLE);
polygon.bindPopup(`<div><strong>${t('mapViewLeaflet.ausgewaehlteFlaeche')}</strong><br/><em>${t('mapViewLeaflet.zumEntfernenParzelleImPanel')}</em></div>`);
polygon.bindPopup(`<div><strong>${t('Ausgewählte Fläche')}</strong><br/><em>${t('Zum Entfernen Parzelle im Panel auswählen')}</em></div>`);
if (onParcelClick) {
polygon.on('click', () => onParcelClick('combined'));
}
@ -207,7 +207,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
if (ring && ring.length >= 3) {
const latLngs = ring.map(([x, y]) => toWgs84(x, y));
const polygon = L.polygon(latLngs, SELECTED_STYLE);
polygon.bindPopup(`<div><strong>${t('mapViewLeaflet.ausgewaehlteFlaeche')}</strong><br/><em>${t('mapViewLeaflet.zumEntfernenParzelleImPanel')}</em></div>`);
polygon.bindPopup(`<div><strong>${t('Ausgewählte Fläche')}</strong><br/><em>${t('Zum Entfernen Parzelle im Panel auswählen')}</em></div>`);
if (onParcelClick) {
polygon.on('click', () => onParcelClick('combined'));
}
@ -225,7 +225,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
<div>
<strong>Parzelle ${parcel.number || parcel.id}</strong><br/>
${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''}
<em>{t('mapViewLeaflet.ausgewaehlt')}</em>
<em>{t('Ausgewählt')}</em>
</div>
`);
if (onParcelClick) {

View file

@ -81,7 +81,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
{isError && (
<div className={styles.errorIndicator}>
<FaExclamationTriangle className={styles.errorIcon} />
<span>{t('chatMessage.aktionFehlgeschlagen')}</span>
<span>{t('Aktion fehlgeschlagen')}</span>
</div>
)}
@ -201,7 +201,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
<button
onClick={handleConfirmDelete}
className={styles.messageDeleteConfirmBtn}
title={t('chatMessage.loeschenBestaetigen')}
title={t('Löschen bestätigen')}
disabled={isDeleting}
>
<IoIosCheckmark />
@ -209,7 +209,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
<button
onClick={handleCancelDelete}
className={styles.messageDeleteCancelBtn}
title={t('chatMessage.abbrechen')}
title={t('Abbrechen')}
disabled={isDeleting}
>
<IoIosClose />
@ -219,7 +219,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
<button
onClick={handleDeleteClick}
className={styles.messageDeleteBtn}
title={t('chatMessage.nachrichtLoeschen')}
title={t('Nachricht löschen')}
disabled={isDeleting}
>
<IoIosTrash />

View file

@ -227,7 +227,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<div className={styles.content}>
{selectionSummary?.total_area_m2 != null && (
<div className={styles.aggregatedSection}>
<h3 className={styles.aggregatedTitle}>{t('parcelInfoPanel.gesamtflaeche')}</h3>
<h3 className={styles.aggregatedTitle}>{t('Gesamtfläche')}</h3>
<p className={styles.aggregatedValue}>
{selectionSummary.total_area_m2.toFixed(2)} m²
{selectionSummary.total_area_m2 >= 10000 && (
@ -266,7 +266,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<button
className={styles.removeButton}
onClick={() => onRemoveParcel(parcelData.parcel.id)}
title={t('parcelInfoPanel.parzelleEntfernen')}
title={t('Parzelle entfernen')}
>
<FaTrash />
</button>
@ -317,7 +317,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{parcelData.parcel.area_m2 !== undefined && (
<div className={styles.infoItem}>
<span className={styles.label}>{t('parcelInfoPanel.flaeche')}</span>
<span className={styles.label}>{t('Fläche')}</span>
<span className={styles.value}>
{parcelData.parcel.area_m2.toFixed(2)} m²
{parcelData.parcel.area_m2 >= 10000 && (
@ -330,7 +330,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{parcelData.parcel.realestate_type && (
<div className={styles.infoItem}>
<span className={styles.label}>{t('parcelInfoPanel.grundstueckstyp')}</span>
<span className={styles.label}>{t('Grundstückstyp')}</span>
<span className={styles.value}>{parcelData.parcel.realestate_type}</span>
</div>
)}
@ -344,7 +344,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<div className={styles.bzoSection}>
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
{docsLoading[parcelData.parcel.id] && (
<p className={styles.bzoHint}>{t('parcelInfoPanel.dokumenteWerdenGeladen')}</p>
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
)}
{docsError[parcelData.parcel.id] && (
<p className={styles.bzoError}>{docsError[parcelData.parcel.id]}</p>
@ -362,7 +362,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
fileName: doc.fileName,
mimeType: doc.mimeType
})}
title={t('parcelInfoPanel.dokumentOeffnen')}
title={t('Dokument öffnen')}
>
<FaEye /> Öffnen
</button>
@ -380,7 +380,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
areaForMach
)}
disabled={extractLoading[parcelData.parcel.id]}
title={t('parcelInfoPanel.inhaltMitLanggraphExtrahierenInkl')}
title={t('Inhalt mit Langgraph extrahieren inkl.')}
>
{extractLoading[parcelData.parcel.id] ? (
<FaSync className={styles.spin} />
@ -404,7 +404,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<>
{fakten.length > 0 && (
<div className={styles.bzoFakten}>
<span className={styles.label}>{t('parcelInfoPanel.faktenAusBzo')}</span>
<span className={styles.label}>{t('Fakten aus BZO')}</span>
<ul className={styles.rulesList}>
{fakten.map((row: { item: string; value: string; source?: string }, i: number) => (
<li key={i}>
@ -421,7 +421,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{vorschlaege.length > 0 && (
<div className={styles.bzoSuggestions}>
<span className={styles.label}>{t('parcelInfoPanel.vorschlaege')}</span>
<span className={styles.label}>{t('Vorschläge')}</span>
<ul className={styles.rulesList}>
{vorschlaege.map((row: { item: string; value: string; is_section?: boolean } | string, i: number) => {
if (typeof row === 'string') return <li key={i}>{row}</li>;
@ -440,7 +440,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{zusatzinfo.length > 0 && (
<div className={styles.bzoZusatzinfo}>
<span className={styles.label}>{t('parcelInfoPanel.weiterfuehrendeBestimmungen')}</span>
<span className={styles.label}>{t('Weiterführende Bestimmungen')}</span>
<div className={styles.zusatzinfoList}>
{zusatzinfo.map((art: { article_label: string; article_title: string; text: string; source?: string }, i: number) => (
<details key={i} className={styles.zusatzinfoItem}>
@ -491,7 +491,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
})()}
{import.meta.env.DEV && (
<details className={styles.zoneDetails}>
<summary className={styles.zoneSummary}>{t('parcelInfoPanel.detailsAnzeigen')}</summary>
<summary className={styles.zoneSummary}>{t('Details anzeigen')}</summary>
<pre className={styles.zoneData}>
{JSON.stringify(parcelData.parcel.zone, null, 2)}
</pre>
@ -532,7 +532,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<button
className={styles.removeButton}
onClick={() => onRemoveParcel(parcelData.parcel.id)}
title={t('parcelInfoPanel.parzelleEntfernen')}
title={t('Parzelle entfernen')}
>
<FaTrash />
</button>
@ -583,7 +583,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{parcelData.parcel.area_m2 !== undefined && (
<div className={styles.infoItem}>
<span className={styles.label}>{t('parcelInfoPanel.flaeche')}</span>
<span className={styles.label}>{t('Fläche')}</span>
<span className={styles.value}>
{parcelData.parcel.area_m2.toFixed(2)} m²
{parcelData.parcel.area_m2 >= 10000 && (
@ -596,7 +596,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{parcelData.parcel.realestate_type && (
<div className={styles.infoItem}>
<span className={styles.label}>{t('parcelInfoPanel.grundstueckstyp')}</span>
<span className={styles.label}>{t('Grundstückstyp')}</span>
<span className={styles.value}>{parcelData.parcel.realestate_type}</span>
</div>
)}
@ -610,7 +610,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<div className={styles.bzoSection}>
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
{docsLoading[parcelData.parcel.id] && (
<p className={styles.bzoHint}>{t('parcelInfoPanel.dokumenteWerdenGeladen')}</p>
<p className={styles.bzoHint}>{t('Dokumente werden geladen')}</p>
)}
{docsError[parcelData.parcel.id] && (
<p className={styles.bzoError}>{docsError[parcelData.parcel.id]}</p>
@ -628,7 +628,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
fileName: doc.fileName,
mimeType: doc.mimeType
})}
title={t('parcelInfoPanel.dokumentOeffnen')}
title={t('Dokument öffnen')}
>
<FaEye /> Öffnen
</button>
@ -646,7 +646,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
areaForMachFlat
)}
disabled={extractLoading[parcelData.parcel.id]}
title={t('parcelInfoPanel.inhaltMitLanggraphExtrahierenInkl')}
title={t('Inhalt mit Langgraph extrahieren inkl.')}
>
{extractLoading[parcelData.parcel.id] ? (
<FaSync className={styles.spin} />
@ -670,7 +670,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
<>
{fakten.length > 0 && (
<div className={styles.bzoFakten}>
<span className={styles.label}>{t('parcelInfoPanel.faktenAusBzo')}</span>
<span className={styles.label}>{t('Fakten aus BZO')}</span>
<ul className={styles.rulesList}>
{fakten.map((row: { item: string; value: string; source?: string }, i: number) => (
<li key={i}>
@ -687,7 +687,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{vorschlaege.length > 0 && (
<div className={styles.bzoSuggestions}>
<span className={styles.label}>{t('parcelInfoPanel.vorschlaege')}</span>
<span className={styles.label}>{t('Vorschläge')}</span>
<ul className={styles.rulesList}>
{vorschlaege.map((row: { item: string; value: string; is_section?: boolean } | string, i: number) => {
if (typeof row === 'string') return <li key={i}>{row}</li>;
@ -706,7 +706,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
)}
{zusatzinfo.length > 0 && (
<div className={styles.bzoZusatzinfo}>
<span className={styles.label}>{t('parcelInfoPanel.weiterfuehrendeBestimmungen')}</span>
<span className={styles.label}>{t('Weiterführende Bestimmungen')}</span>
<div className={styles.zusatzinfoList}>
{zusatzinfo.map((art: { article_label: string; article_title: string; text: string; source?: string }, i: number) => (
<details key={i} className={styles.zusatzinfoItem}>
@ -755,7 +755,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
})()}
{import.meta.env.DEV && (
<details className={styles.zoneDetails}>
<summary className={styles.zoneSummary}>{t('parcelInfoPanel.detailsAnzeigen')}</summary>
<summary className={styles.zoneSummary}>{t('Details anzeigen')}</summary>
<pre className={styles.zoneData}>
{JSON.stringify(parcelData.parcel.zone, null, 2)}
</pre>
@ -843,7 +843,7 @@ export const BZOInformationDisplay: React.FC<BZOInformationDisplayProps> = ({ da
{/* Summary Section */}
{data.ai_summary && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>{t('parcelInfoPanel.zusammenfassung')}</h5>
<h5 className={styles.bzoSubTitle}>{t('Zusammenfassung')}</h5>
<div className={styles.bzoSummary}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
@ -1082,7 +1082,7 @@ export const BZOInformationDisplay: React.FC<BZOInformationDisplayProps> = ({ da
{/* Statistics */}
{data.extracted_content && (
<div className={styles.bzoSubSection}>
<h5 className={styles.bzoSubTitle}>{t('parcelInfoPanel.statistiken')}</h5>
<h5 className={styles.bzoSubTitle}>{t('Statistiken')}</h5>
<div className={styles.bzoStats}>
<div className={styles.bzoStatItem}>
<span className={styles.bzoStatLabel}>Zonen:</span>

View file

@ -108,7 +108,7 @@ export function Popup({
<button
className={styles.closeButton}
onClick={onClose}
aria-label={t('popup.close')}
aria-label={t('Schließen')}
>
×
</button>

View file

@ -53,7 +53,7 @@ export const Toast: React.FC<ToastProps> = ({ toast, onClose }) => {
<button
className={styles.closeButton}
onClick={() => onClose(toast.id)}
aria-label={t('toast.schliessen')}
aria-label={t('Schließen')}
>
<FaTimes />
</button>

View file

@ -50,7 +50,7 @@ export function ViewForm<T extends Record<string, any>>({
if (field.type) {
// Boolean/Checkbox types
if (isCheckboxType(field.type)) {
return value ? t('viewForm.yes') : t('viewForm.no');
return value ? t('Ja') : t('Nein');
}
// Select/Enum types

View file

@ -69,7 +69,7 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
{/* Status and Round Badges */}
<div className={styles.workflowStatus}>
{showSpinner && (
<div className={styles.spinner} aria-label={t('workflowStatus.workflowRunning')} />
<div className={styles.spinner} aria-label={t('Workflow läuft')} />
)}
{workflowStatus.status && (
<span className={styles.statusBadge} data-status={workflowStatus.status}>

View file

@ -309,7 +309,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
<button
className={`${styles.actionBtn} ${styles.actionBtnDanger}`}
onClick={async (e) => { e.stopPropagation(); await onDeleteChat(chat.id); _loadChats(); }}
title={t('chatsTab.loeschen')}
title={t('schen')}
>
🗑
</button>
@ -331,7 +331,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
return labels[code] || code;
};
if (loading) return <div className={styles.loading}>{t('chatsTab.ladeChats')}</div>;
if (loading) return <div className={styles.loading}>{t('Chats werden geladen…')}</div>;
return (
<div className={styles.chatsTab}>
@ -339,12 +339,12 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
<input
className={styles.search}
type="text"
placeholder={t('chatsTab.suchen')}
placeholder={t('Suchen')}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{onCreateNew && (
<button className={styles.createBtn} onClick={() => { onCreateNew(); setTimeout(_loadChats, 500); }} title={t('chatsTab.neuerChat')}>
<button className={styles.createBtn} onClick={() => { onCreateNew(); setTimeout(_loadChats, 500); }} title={t('Neuer Chat')}>
+
</button>
)}
@ -436,7 +436,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({ context,
{_allChats.length === 0 && (
<div className={styles.emptyState}>
{filter === 'archived' ? t('chatsTab.keineArchiviertenChats') : t('chatsTab.keineAktivenChats')}
{filter === 'archived' ? t('Keine archivierten Chats') : t('Keine aktiven Chats')}
</div>
)}
</div>

View file

@ -233,7 +233,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
}
}, [_loadFiles]);
if (loading) return <div className={styles.loading}>{t('filesTab.ladeDateien')}</div>;
if (loading) return <div className={styles.loading}>{t('Dateien laden')}</div>;
return (
<div
@ -261,7 +261,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#F25843' }}
title={t('filesTab.uploadFiles')}
title={t('Dateien hochladen')}
>
{uploading ? '...' : '+'}
</button>
@ -284,7 +284,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
<input
type="text"
placeholder={t('filesTab.dateienSuchen')}
placeholder={t('Dateien suchen')}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
style={{
@ -325,7 +325,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
{_fileNodes.length === 0 && (
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
{searchQuery ? t('filesTab.keineDateienGefunden') : t('filesTab.keineDateienDragDropZum')}
{searchQuery ? t('Keine Dateien gefunden') : t('Keine Dateien. Drag & Drop zum Hochladen.')}
</div>
)}
</div>

View file

@ -853,14 +853,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
fontSize: 13, padding: '0 2px', lineHeight: 1,
opacity: ds.neutralize ? 1 : 0.35,
}}
title={ds.neutralize ? t('sourcesTab.neutralizeOnClickToDeactivate') : t('sourcesTab.neutralizeOffClickToActivate')}
title={ds.neutralize ? t('Neutralisierung: Klick zum Deaktivieren') : t('Neutralisierung aus: Klick zum Aktivieren')}
>
{'\uD83D\uDD12'}
</button>
<button
onClick={() => _removeDatasource(ds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.entfernen')}
title={t('Entfernen')}
>
{'\u2715'}
</button>
@ -956,7 +956,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button
onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.removeAllTablesForThis')}
title={t('Alle Tabellen für diese Quelle entfernen')}
>
{'\u2715'}
</button>
@ -986,14 +986,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button
onClick={() => _toggleFeatureNeutralize(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
title={fds.neutralize ? t('sourcesTab.neutralizeOn') : t('sourcesTab.neutralizeOff')}
title={fds.neutralize ? t('Neutralisierung an') : t('Neutralisierung aus')}
>
{'\uD83D\uDD12'}
</button>
<button
onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.remove')}
title={t('Entfernen')}
>
{'\u2715'}
</button>
@ -1029,14 +1029,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button
onClick={() => _toggleFeatureNeutralize(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
title={fds.neutralize ? t('sourcesTab.neutralizeOn') : t('sourcesTab.neutralizeOff')}
title={fds.neutralize ? t('Neutralisierung an') : t('Neutralisierung aus')}
>
{'\uD83D\uDD12'}
</button>
<button
onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.entfernen')}
title={t('Entfernen')}
>
{'\u2715'}
</button>
@ -1167,13 +1167,13 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
opacity: isAdding ? 0.5 : 1,
flexShrink: 0,
}}
title={t('sourcesTab.addAsDataSource')}
title={t('Als Datenquelle hinzufügen')}
>
{isAdding ? '...' : '+ Add'}
</button>
)}
{canAdd && alreadyAdded && (
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('sourcesTab.alreadyAdded')}>
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('Bereits hinzugefügt')}>
{'\u2713'}
</span>
)}
@ -1431,13 +1431,13 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
opacity: isAdding ? 0.5 : 1, flexShrink: 0,
}}
title={t('sourcesTab.addAsFeatureDataSource')}
title={t('Als Feature-Datenquelle hinzufügen')}
>
{isAdding ? '...' : '+ Add'}
</button>
)}
{isAdded && (
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('sourcesTab.alreadyAdded')}>
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('Bereits hinzugefügt')}>
{'\u2713'}
</span>
)}
@ -1578,13 +1578,13 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
opacity: isAdding ? 0.5 : 1, flexShrink: 0,
}}
title={t('sourcesTab.addAllTablesForThis')}
title={t('Alle Tabellen für diese Quelle hinzufügen')}
>
{isAdding ? '...' : '+ Add'}
</button>
)}
{isAdded && (
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('sourcesTab.alreadyAdded')}>
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('Bereits hinzugefügt')}>
{'\u2713'}
</span>
)}

View file

@ -21,10 +21,10 @@ export type AccessLevel = 'n' | 'm' | 'g' | 'a' | null;
export function _getAccessLevelOptions(t: (key: string) => string): { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] {
return [
{ value: 'n', label: t('useAccessRules.keine'), color: '#e53e3e' },
{ value: 'm', label: t('useAccessRules.eigene'), color: '#d69e2e' },
{ value: 'g', label: t('useAccessRules.gruppe'), color: '#3182ce' },
{ value: 'a', label: t('useAccessRules.alle'), color: '#38a169' },
{ value: 'n', label: t('Keine'), color: '#e53e3e' },
{ value: 'm', label: t('Eigene'), color: '#d69e2e' },
{ value: 'g', label: t('Gruppe'), color: '#3182ce' },
{ value: 'a', label: t('Alle'), color: '#38a169' },
];
}

View file

@ -29,9 +29,9 @@ export function useConfirm() {
const resolveRef = useRef<((v: boolean) => void) | null>(null);
const _defaults: Required<ConfirmOptions> = useMemo(() => ({
title: t('confirm.title'),
confirmLabel: t('confirm.confirmLabel'),
cancelLabel: t('confirm.cancelLabel'),
title: t('Bestätigung'),
confirmLabel: t('Bestätigen'),
cancelLabel: t('Abbrechen'),
variant: 'primary' as const,
}), [t]);

View file

@ -33,9 +33,9 @@ export function usePrompt() {
const inputRef = useRef<HTMLInputElement>(null);
const _defaults: Required<PromptOptions> = useMemo(() => ({
title: t('prompt.title'),
confirmLabel: t('prompt.confirmLabel'),
cancelLabel: t('prompt.cancelLabel'),
title: t('Titel'),
confirmLabel: t('Bestätigen'),
cancelLabel: t('Abbrechen'),
placeholder: '',
defaultValue: '',
variant: 'primary' as const,

View file

@ -22,7 +22,7 @@ const LoadingScreen: React.FC = () => {
return (
<div className={styles.loadingContainer}>
<div className={styles.loadingSpinner} />
<p>{t('ui.ladeFeaturedaten')}</p>
<p>{t('Lade Featuredaten')}</p>
</div>
);
};
@ -41,7 +41,7 @@ const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' })
return (
<div className={styles.errorContainer}>
<div className={styles.errorIcon}></div>
<h2>{t('ui.zugriffNichtMoeglich')}</h2>
<h2>{t('Zugriff nicht möglich')}</h2>
<p>{message}</p>
<a href={returnPath} className={styles.errorLink}>
Zurück zur Übersicht

View file

@ -67,7 +67,7 @@ const MainLayoutInner: React.FC = () => {
<button
className={styles.mobileBackdrop}
onClick={() => setIsMobileSidebarOpen(false)}
aria-label={t('mainLayout.navigationSchliessen')}
aria-label={t('Navigation schließen')}
/>
)}
@ -109,7 +109,7 @@ const MainLayoutInner: React.FC = () => {
<button
className={styles.mobileMenuButton}
onClick={() => setIsMobileSidebarOpen(true)}
aria-label={t('mainLayout.navigationOeffnen')}
aria-label={t('Navigation öffnen')}
>
</button>

View file

@ -234,7 +234,7 @@ export const AutomationsDashboardPage: React.FC = () => {
{metrics?.runsByStatus && Object.keys(metrics.runsByStatus).length > 0 && (
<div style={{ marginBottom: 24 }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, marginBottom: 8 }}>{t('automationsDashboard.runsNachStatus')}</h3>
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, marginBottom: 8 }}>{t('Läufe nach Status')}</h3>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{Object.entries(metrics.runsByStatus).map(([status, count]) => (
<span

View file

@ -72,8 +72,8 @@ export const DashboardPage: React.FC = () => {
return (
<div className={styles.dashboard}>
<header className={styles.header}>
<h1>{t('dashboard.uebersicht')}</h1>
<p className={styles.subtitle}>{t('dashboard.lade')}</p>
<h1>{t('Übersicht')}</h1>
<p className={styles.subtitle}>{t('Lade')}</p>
</header>
</div>
);
@ -82,7 +82,7 @@ export const DashboardPage: React.FC = () => {
return (
<div className={styles.dashboard}>
<header className={styles.header}>
<h1>{t('dashboard.uebersicht')}</h1>
<h1>{t('Übersicht')}</h1>
{totalInstances > 0 && (
<p className={styles.subtitle}>
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}.

View file

@ -67,18 +67,18 @@ const PlaceholderView: React.FC<{ title: string; description: string }> = ({ tit
const ChatworkflowDashboard: React.FC = () => {
const { t } = useLanguage();
return (
<PlaceholderView title={t('feature.workflowDashboard')} description={t('feature.uebersichtDerWorkflows')} />
<PlaceholderView title={t('Workflow-Dashboard')} description={t('Übersicht der Workflows')} />
);
};
const ChatworkflowRuns: React.FC = () => {
const { t } = useLanguage();
return <PlaceholderView title="Runs" description={t('feature.workflowausfuehrungen')} />;
return <PlaceholderView title="Runs" description={t('Workflow-Ausführungen')} />;
};
const ChatworkflowFiles: React.FC = () => {
const { t } = useLanguage();
return <PlaceholderView title={t('feature.dateien')} description="Workflow-Dateien" />;
return <PlaceholderView title={t('Dateien')} description="Workflow-Dateien" />;
};
// Chatbot Views
@ -87,7 +87,7 @@ const ChatworkflowFiles: React.FC = () => {
const ChatbotSettings: React.FC = () => {
const { t } = useLanguage();
return (
<PlaceholderView title={t('feature.chatbotEinstellungen')} description={t('feature.konfigurationDesChatbots')} />
<PlaceholderView title={t('Chatbot-Einstellungen')} description={t('Konfiguration des Chatbots')} />
);
};
@ -96,8 +96,8 @@ const NotFound: React.FC = () => {
const { t } = useLanguage();
return (
<div className={styles.notFound}>
<h2>{t('feature.seiteNichtGefunden')}</h2>
<p>{t('feature.dieseViewExistiertNichtOder')}</p>
<h2>{t('Seite nicht gefunden')}</h2>
<p>{t('Diese Ansicht existiert nicht oder')}</p>
</div>
);
};
@ -106,8 +106,8 @@ const AccessDenied: React.FC = () => {
const { t } = useLanguage();
return (
<div className={styles.accessDenied}>
<h2>{t('feature.zugriffVerweigert')}</h2>
<p>{t('feature.duHastKeineBerechtigungFuer')}</p>
<h2>{t('Zugriff verweigert')}</h2>
<p>{t('Du hast keine Berechtigung für')}</p>
</div>
);
};

View file

@ -161,11 +161,11 @@ export const GDPRPage: React.FC = () => {
<main className={styles.content}>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('gDPR.yourDataRights')}</h2>
<h2 className={styles.sectionTitle}>{t('Ihre Datenrechte')}</h2>
<div className={styles.actions}>
<div className={styles.actionCard}>
<h3>{t('gDPR.accessArticle15')}</h3>
<p>{t('gDPR.downloadAFullExportOf')}</p>
<h3>{t('Zugriff (Artikel 15)')}</h3>
<p>{t('Einen vollständigen Export herunterladen von')}</p>
<button
className={styles.primaryButton}
onClick={handleDataExport}
@ -186,8 +186,8 @@ export const GDPRPage: React.FC = () => {
</div>
<div className={styles.actionCard}>
<h3>{t('gDPR.portabilityArticle20')}</h3>
<p>{t('gDPR.downloadAMachinereadableJsonldExport')}</p>
<h3>{t('Datenübertragbarkeit (Artikel 20)')}</h3>
<p>{t('Einen maschinenlesbaren JSON-LD-Export herunterladen')}</p>
<button
className={styles.secondaryButton}
onClick={handlePortabilityExport}
@ -208,8 +208,8 @@ export const GDPRPage: React.FC = () => {
</div>
<div className={styles.actionCard}>
<h3>{t('gDPR.erasureArticle17')}</h3>
<p>{t('gDPR.permanentlyDeleteYourAccountAnd')}</p>
<h3>{t('Löschung (Artikel 17)')}</h3>
<p>{t('Ihr Konto dauerhaft löschen und')}</p>
{!showDeleteConfirm && (
<button
className={styles.dangerButton}
@ -278,13 +278,13 @@ export const GDPRPage: React.FC = () => {
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('gDPR.processingInformation')}</h2>
{isLoadingConsent && <p className={styles.mutedText}>{t('gDPR.loadingConsentInfo')}</p>}
<h2 className={styles.sectionTitle}>{t('Verarbeitungsinformationen')}</h2>
{isLoadingConsent && <p className={styles.mutedText}>{t('Lade Einwilligungsinformationen')}</p>}
{consentError && <p className={styles.errorText}>{consentError}</p>}
{!isLoadingConsent && !consentError && consentInfo && (
<div className={styles.infoGrid}>
<div className={styles.infoBlock}>
<h3>{t('gDPR.dataCollected')}</h3>
<h3>{t('Gesammelte Daten')}</h3>
<ul>
{Object.entries(consentInfo.dataCollected || {}).map(([key, value]) => (
<li key={key}>
@ -294,7 +294,7 @@ export const GDPRPage: React.FC = () => {
</ul>
</div>
<div className={styles.infoBlock}>
<h3>{t('gDPR.processing')}</h3>
<h3>{t('Verarbeitung')}</h3>
<ul>
{Object.entries(consentInfo.dataProcessing || {}).map(([key, value]) => (
<li key={key}>
@ -304,7 +304,7 @@ export const GDPRPage: React.FC = () => {
</ul>
</div>
<div className={styles.infoBlock}>
<h3>{t('gDPR.yourRights')}</h3>
<h3>{t('Ihre Rechte')}</h3>
<ul>
{Object.entries(consentInfo.userRights || {}).map(([key, value]) => (
<li key={key}>

View file

@ -57,7 +57,7 @@ export const InvitePage: React.FC = () => {
useEffect(() => {
const validate = async () => {
if (!token) {
setError(t('invite.keinEinladungstokenAngegeben'));
setError(t('Kein Einladungstoken angegeben'));
setValidating(false);
return;
}
@ -163,7 +163,7 @@ export const InvitePage: React.FC = () => {
<div className={styles.card}>
<div className={styles.loading}>
<FaSpinner className={styles.spinner} />
<p>{t('invite.einladungWirdUeberprueft')}</p>
<p>{t('Einladung wird überprüft')}</p>
</div>
</div>
</div>
@ -177,7 +177,7 @@ export const InvitePage: React.FC = () => {
<div className={styles.card}>
<div className={styles.errorState}>
<FaTimesCircle className={styles.errorIcon} />
<h1>{t('invite.ungueltigeEinladung')}</h1>
<h1>{t('Ungültige Einladung')}</h1>
<p>{validation?.reason || 'Diese Einladung ist nicht gültig.'}</p>
<Link to="/login" className={styles.primaryButton}>
Zur Anmeldung
@ -195,9 +195,9 @@ export const InvitePage: React.FC = () => {
<div className={styles.card}>
<div className={styles.successState}>
<FaCheckCircle className={styles.successIcon} />
<h1>{t('invite.erfolgreich')}</h1>
<p>{t('invite.sieWurdenErfolgreichZumMandanten')}</p>
<p className={styles.redirectMessage}>{t('invite.sieWerdenWeitergeleitet')}</p>
<h1>{t('erfolgreich')}</h1>
<p>{t('Sie wurden erfolgreich zum Mandanten hinzugefügt')}</p>
<p className={styles.redirectMessage}>{t('Sie werden weitergeleitet')}</p>
</div>
</div>
</div>
@ -212,12 +212,12 @@ export const InvitePage: React.FC = () => {
<div className={styles.card}>
<div className={styles.errorState}>
<FaTimesCircle className={styles.errorIcon} />
<h1>{t('invite.falscheAnmeldung')}</h1>
<h1>{t('Falsche Anmeldung')}</h1>
<p>
Diese Einladung ist für <strong>{validation.targetUsername}</strong> bestimmt.
Sie sind als <strong>{cachedUser?.username || 'anderer Benutzer'}</strong> angemeldet.
</p>
<p>{t('invite.bitteMeldenSieSichAb')}</p>
<p>{t('Bitte melden Sie sich ab')}</p>
<Link to="/" className={styles.primaryButton}>
Zum Dashboard
</Link>
@ -230,9 +230,9 @@ export const InvitePage: React.FC = () => {
// Already authenticated - show accept button
const isFeatureInvite = !!validation.featureInstanceId;
const introText = isFeatureInvite
? t('invite.sieWurdenEingeladenEinemMandanten')
: t('invite.sieWurdenEingeladenEinemMandanten');
const rolesLabel = isFeatureInvite ? t('invite.featuresMitZugewiesenenRollen') : t('invite.zugewieseneRollen');
? t('Sie wurden eingeladen, einem Mandanten beizutreten')
: t('Sie wurden eingeladen, einem Mandanten beizutreten');
const rolesLabel = isFeatureInvite ? t('Features mit zugewiesenen Rollen') : t('Zugewiesene Rollen');
const rolesValue = validation.featureInstanceName && validation.roleLabels?.length
? `${validation.featureInstanceName} (${validation.roleLabels.join(', ')})`
: validation.roleLabels?.join(', ') || '';
@ -242,7 +242,7 @@ export const InvitePage: React.FC = () => {
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<h1>{t('invite.einladungAnnehmen')}</h1>
<h1>{t('Einladung annehmen')}</h1>
<p>{introText}</p>
</div>
@ -255,12 +255,12 @@ export const InvitePage: React.FC = () => {
)}
{validation.mandateName && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>{t('invite.mandant')}</span>
<span className={styles.infoLabel}>{t('Mandant')}</span>
<span className={styles.infoValue}>{validation.mandateName}</span>
</div>
)}
<div className={styles.infoRow}>
<span className={styles.infoLabel}>{t('invite.status')}</span>
<span className={styles.infoLabel}>{t('Status')}</span>
<span className={styles.infoValue}>Angemeldet</span>
</div>
{rolesValue && (
@ -305,7 +305,7 @@ export const InvitePage: React.FC = () => {
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<h1>{t('invite.einladungAnnehmen')}</h1>
<h1>{t('Einladung annehmen')}</h1>
<p>{introText}</p>
</div>
@ -318,7 +318,7 @@ export const InvitePage: React.FC = () => {
)}
{validation.mandateName && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>{t('invite.mandant')}</span>
<span className={styles.infoLabel}>{t('Mandant')}</span>
<span className={styles.infoValue}>{validation.mandateName}</span>
</div>
)}

View file

@ -145,7 +145,7 @@ function Login() {
{hasPendingInvitation && (
<div className={styles.invitationNotice}>
<FaEnvelopeOpenText className={styles.invitationIcon} />
<span>{t('login.sieHabenEineAusstehendeEinladung')}</span>
<span>{t('Sie haben eine ausstehende Einladung')}</span>
</div>
)}
@ -186,7 +186,7 @@ function Login() {
}}
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
/>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('login.passwort')}</label>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('Passwort')}</label>
</div>
<div className={styles.disclaimer}>
<p>
@ -237,7 +237,7 @@ function Login() {
</button>
<div className={styles.registerLink}>
<span>{t('login.duHastNochKeinKonto')}</span>
<span>{t('Du hast noch kein Konto?')}</span>
</div>
<div className={styles.ctaSection}>
<button

View file

@ -66,7 +66,7 @@ function PasswordResetRequest() {
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>{t('passwordResetRequest.passwortZuruecksetzen')}</h2>
<h2 className={styles.title}>{t('Passwort zurücksetzen')}</h2>
<div className={styles.loginForm}>
{validationError && (
<div className={styles.error}>{validationError}</div>
@ -101,7 +101,7 @@ function PasswordResetRequest() {
</div>
<div className={styles.infoMessage}>
<p>{t('passwordResetRequest.gebenSieIhrenBenutzernamenEin')}</p>
<p>{t('Geben Sie Ihren Benutzernamen ein')}</p>
</div>
<button
@ -115,7 +115,7 @@ function PasswordResetRequest() {
)}
<div className={styles.registerLink}>
<span>{t('passwordResetRequest.zurueckZum')}</span>
<span>{t('Zurück zum')}</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}

View file

@ -138,7 +138,7 @@ function Register() {
{hasPendingInvitation && !successMessage && (
<div className={styles.invitationNotice}>
<FaEnvelopeOpenText className={styles.invitationIcon} />
<span>{t('register.sieHabenEineAusstehendeEinladung')}</span>
<span>{t('Sie haben eine ausstehende Einladung')}</span>
</div>
)}
@ -191,11 +191,11 @@ function Register() {
onBlur={() => setFullNameFocused(false)}
className={`${styles.input} ${fullNameFocused || formData.fullName ? styles.focused : ''}`}
/>
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>{t('register.vollstaendigerName')}</label>
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>{t('Vollständiger Name')}</label>
</div>
<div className={styles.infoMessage}>
<p>{t('register.nachDerRegistrierungErhaltenSie')}</p>
<p>{t('Nach der Registrierung erhalten Sie')}</p>
</div>
<div className={styles.disclaimer}>
@ -215,7 +215,7 @@ function Register() {
)}
<div className={styles.registerLink}>
<span>{t('register.bereitsRegistriert')}</span>
<span>{t('Bereits registriert')}</span>
<button
className={styles.textButton}
onClick={() => navigate("/login", { state: location.state })}

View file

@ -107,7 +107,7 @@ function Reset() {
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>{t('reset.neuesPasswortSetzen')}</h2>
<h2 className={styles.title}>{t('Neues Passwort setzen')}</h2>
<div className={styles.loginForm}>
<div className={styles.error}>{tokenError}</div>
<div className={styles.registerLink}>
@ -119,7 +119,7 @@ function Reset() {
</button>
</div>
<div className={styles.registerLink}>
<span>{t('reset.oderZurueckZum')}</span>
<span>{t('Oder zurück zum')}</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}
@ -147,7 +147,7 @@ function Reset() {
</div>
<div className={styles.loginSection}>
<div className={styles.loginBox}>
<h2 className={styles.title}>{t('reset.neuesPasswortSetzen')}</h2>
<h2 className={styles.title}>{t('Neues Passwort setzen')}</h2>
<div className={styles.loginForm}>
{(validationError || error) && (
<div className={styles.error}>{validationError || error}</div>
@ -159,7 +159,7 @@ function Reset() {
{!successMessage && (
<form onSubmit={handleSubmit}>
<div className={styles.passwordHint}>{t('reset.mindestens8Zeichen')}</div>
<div className={styles.passwordHint}>{t('Mindestens 8 Zeichen')}</div>
<div className={styles.floatingLabelInput}>
<input
type="password"
@ -174,7 +174,7 @@ function Reset() {
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
autoComplete="new-password"
/>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('reset.neuesPasswort')}</label>
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('Neues Passwort')}</label>
</div>
@ -192,7 +192,7 @@ function Reset() {
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
autoComplete="new-password"
/>
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>{t('reset.passwortBestaetigen')}</label>
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>{t('Passwort bestätigen')}</label>
</div>
<button
@ -206,7 +206,7 @@ function Reset() {
)}
<div className={styles.registerLink}>
<span>{t('reset.zurueckZum')}</span>
<span>{t('Zurück zum')}</span>
<button
className={styles.textButton}
onClick={() => navigate("/login")}

View file

@ -21,11 +21,11 @@ type SettingsTab = 'profile' | 'appearance' | 'voice' | 'neutralization' | 'priv
function _getTabs(t: (key: string) => string): { key: SettingsTab; label: string }[] {
return [
{ key: 'profile', label: t('settings.tabProfil') },
{ key: 'appearance', label: t('settings.tabDarstellung') },
{ key: 'voice', label: t('settings.tabStimmeSprache') },
{ key: 'neutralization', label: t('settings.tabNeutralisierung') },
{ key: 'privacy', label: t('settings.tabDatenschutz') },
{ key: 'profile', label: t('Tab Profil') },
{ key: 'appearance', label: t('Tab Darstellung') },
{ key: 'voice', label: t('Tab Stimme & Sprache') },
{ key: 'neutralization', label: t('Tab Neutralisierung') },
{ key: 'privacy', label: t('Tab Datenschutz') },
];
}
@ -48,9 +48,9 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
const languageOptions = availableLanguages.map((l) => ({ value: l.code, label: l.label || l.code }));
const profileAttributes: AttributeDefinition[] = [
{ name: 'fullName', type: 'string', label: t('settings.vollstaendigerName'), description: t('settings.ihrVollstaendigerName'), required: false, placeholder: t('settings.placeholderName') },
{ name: 'email', type: 'email', label: t('settings.emailAdresse'), description: t('settings.emailBeschreibung'), required: true, placeholder: t('settings.placeholderEmail') },
{ name: 'language', type: 'select', label: t('settings.sprache'), description: t('settings.anzeigespracheDerAnwendung'), required: true, options: languageOptions },
{ name: 'fullName', type: 'string', label: t('Vollständiger Name'), description: t('Ihr vollständiger Name'), required: false, placeholder: t('Name-Platzhalter') },
{ name: 'email', type: 'email', label: t('E-Mail-Adresse'), description: t('E-Mail-Beschreibung'), required: true, placeholder: t('E-Mail-Platzhalter') },
{ name: 'language', type: 'select', label: t('Sprache'), description: t('Anzeigesprache der Anwendung'), required: true, options: languageOptions },
];
const handleSubmit = async (formData: any) => {
@ -72,12 +72,12 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
<div className={styles.modalOverlay} onClick={onClose}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2>{t('settings.profilBearbeiten')}</h2>
<h2>{t('Profil bearbeiten')}</h2>
<button className={styles.closeButton} onClick={onClose}>&times;</button>
</div>
<div className={styles.modalBody}>
{error && <div className={styles.errorMessage}>{error}</div>}
<FormGeneratorForm attributes={profileAttributes} data={userData} mode="edit" onSubmit={handleSubmit} onCancel={onClose} submitButtonText={isSaving ? t('settings.speichern') : t('settings.speichern')} cancelButtonText={t('settings.abbrechen')} />
<FormGeneratorForm attributes={profileAttributes} data={userData} mode="edit" onSubmit={handleSubmit} onCancel={onClose} submitButtonText={isSaving ? t('Speichern') : t('Speichern')} cancelButtonText={t('Abbrechen')} />
</div>
</div>
</div>
@ -177,7 +177,7 @@ const VoiceSettingsTab: React.FC = () => {
method: 'put',
data: { sttLanguage, ttsLanguage: sttLanguage, ttsVoiceMap: mapObj },
});
setSuccess(t('settings.einstellungenGespeichert'));
setSuccess(t('Einstellungen gespeichert'));
setTimeout(() => setSuccess(null), 3000);
await _loadSettings();
} catch (err: any) {
@ -199,7 +199,7 @@ const VoiceSettingsTab: React.FC = () => {
const audio = new Audio(`data:audio/mp3;base64,${result.audio}`);
audio.play();
}
} catch { setError(t('settings.stimmtestFehlgeschlagen')); }
} catch { setError(t('Stimmtest fehlgeschlagen')); }
finally { setTesting(null); }
}, [request]);
@ -215,7 +215,7 @@ const VoiceSettingsTab: React.FC = () => {
];
const _displayLanguages = languages.length > 0 ? languages : _defaultLangs;
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('settings.einstellungenWerdenGeladen')}</div>;
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('Einstellungen werden geladen')}</div>;
return (
<>
@ -223,11 +223,11 @@ const VoiceSettingsTab: React.FC = () => {
{success && <div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', color: '#16a34a', padding: '0.75rem 1rem', borderRadius: 6, marginBottom: '1rem', fontSize: '0.875rem' }}>{success}</div>}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.sttspracheSpracheingabe')}</h2>
<h2 className={styles.sectionTitle}>{t('STT-Sprache Spracheingabe')}</h2>
<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<label className={styles.settingLabel}>{t('settings.spracheFuerSpracherkennung')}</label>
<p className={styles.settingDescription}>{t('settings.wirdFuerDieSprachezutexterkennungVerwendet')}</p>
<label className={styles.settingLabel}>{t('Sprache für Spracherkennung')}</label>
<p className={styles.settingDescription}>{t('Wird für Sprach- und Texterkennung verwendet')}</p>
</div>
<div className={styles.settingControl}>
<select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}>
@ -240,7 +240,7 @@ const VoiceSettingsTab: React.FC = () => {
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.ttsstimmenSprachausgabe')}</h2>
<h2 className={styles.sectionTitle}>{t('TTS-Stimmen Sprachausgabe')}</h2>
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
Die Sprache wird automatisch erkannt. Hier kann pro Sprache eine bevorzugte Stimme festgelegt werden.
</p>
@ -251,7 +251,7 @@ const VoiceSettingsTab: React.FC = () => {
</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
<thead><tr style={{ borderBottom: '1px solid var(--border-color, #e0e0e0)' }}><th style={{ textAlign: 'left', padding: '0.5rem' }}>{t('settings.sprache')}</th><th style={{ textAlign: 'left', padding: '0.5rem' }}>{t('settings.stimme')}</th><th /><th /></tr></thead>
<thead><tr style={{ borderBottom: '1px solid var(--border-color, #e0e0e0)' }}><th style={{ textAlign: 'left', padding: '0.5rem' }}>{t('Sprache')}</th><th style={{ textAlign: 'left', padding: '0.5rem' }}>{t('Stimme')}</th><th /><th /></tr></thead>
<tbody>
{voiceMap.map(entry => (
<tr key={entry.language} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
@ -263,7 +263,7 @@ const VoiceSettingsTab: React.FC = () => {
</button>
</td>
<td style={{ padding: '0.5rem' }}>
<button className={styles.button} style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem', color: '#dc2626' }} onClick={() => _handleRemoveEntry(entry.language)}>{t('settings.entfernen')}</button>
<button className={styles.button} style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem', color: '#dc2626' }} onClick={() => _handleRemoveEntry(entry.language)}>{t('Entfernen')}</button>
</td>
</tr>
))}
@ -273,7 +273,7 @@ const VoiceSettingsTab: React.FC = () => {
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div>
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('settings.sprache')}</label>
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('Sprache')}</label>
<select className={styles.select} value={addLanguage} onChange={e => setAddLanguage(e.target.value)}>
{_displayLanguages.map((lang: any) => (
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option>
@ -281,15 +281,15 @@ const VoiceSettingsTab: React.FC = () => {
</select>
</div>
<div>
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('settings.stimme')}</label>
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('Stimme')}</label>
<select className={styles.select} value={addVoiceName} onChange={e => setAddVoiceName(e.target.value)} disabled={loadingVoices}>
<option value="">{t('settings.standard')}</option>
<option value="">{t('Standard')}</option>
{addVoices.map((v: any) => (
<option key={v.name || v} value={v.name || v}>{v.displayName || v.name || v}</option>
))}
</select>
</div>
<button className={styles.button} onClick={_handleAddEntry} style={{ padding: '0.5rem 1rem' }}>{t('settings.zuweisen')}</button>
<button className={styles.button} onClick={_handleAddEntry} style={{ padding: '0.5rem 1rem' }}>{t('Zuweisen')}</button>
<button className={styles.button} onClick={() => _handleTestVoice(addLanguage, addVoiceName)} disabled={testing !== null} style={{ padding: '0.5rem 1rem' }}>
{testing === addLanguage ? '...' : 'Testen'}
</button>
@ -297,7 +297,7 @@ const VoiceSettingsTab: React.FC = () => {
</section>
<button className={styles.button} onClick={_handleSave} disabled={saving} style={{ background: 'var(--primary-color, #2563eb)', color: '#fff', border: 'none', padding: '0.625rem 1.5rem', fontWeight: 600, borderRadius: 6 }}>
{saving ? t('settings.speichern') : t('settings.einstellungenSpeichern')}
{saving ? t('Speichern') : t('Einstellungen speichern')}
</button>
</>
);
@ -358,14 +358,14 @@ const NeutralizationMappingsTab: React.FC = () => {
return text.slice(0, 2) + '*'.repeat(Math.min(text.length - 4, 20)) + text.slice(-2);
};
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('settings.mappingsWerdenGeladen')}</div>;
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('Mappings werden geladen')}</div>;
return (
<>
{error && <div className={styles.errorMessage}>{error}</div>}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.platzhaltermappingsLokal')}</h2>
<h2 className={styles.sectionTitle}>{t('Platzhaltermappings lokal')}</h2>
<div
style={{
marginBottom: '1rem',
@ -379,7 +379,7 @@ const NeutralizationMappingsTab: React.FC = () => {
}}
>
<strong>AI-Workspace:</strong> Neutralisierter Chat-Text, Dokumente und Platzhalter-Mappings finden Sie unter{' '}
<strong>{t('settings.mandantAiworkspaceinstanzEinstellungenTabNeutralisierung')}</strong> (nicht auf dieser
<strong>{t('Mandant AI-Workspace-Instanz Einstellungen Tab Neutralisierung')}</strong> (nicht auf dieser
Seite). Dieser Tab zeigt nur die <strong>lokale</strong> Liste über <code>/api/local/neutralization-mappings</code>.
</div>
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
@ -488,8 +488,8 @@ export const SettingsPage: React.FC = () => {
return (
<div className={styles.settings}>
<header className={styles.header}>
<h1>{t('settings.einstellungen')}</h1>
<p className={styles.subtitle}>{t('settings.persoenlicheEinstellungenUndPraeferenzen')}</p>
<h1>{t('Einstellungen')}</h1>
<p className={styles.subtitle}>{t('Persönliche Einstellungen und Präferenzen')}</p>
</header>
<nav style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--border-color, #e0e0e0)', marginBottom: '1.5rem' }}>
@ -508,14 +508,14 @@ export const SettingsPage: React.FC = () => {
{activeTab === 'profile' && (
<>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.konto')}</h2>
<h2 className={styles.sectionTitle}>{t('Konto')}</h2>
<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<label className={styles.settingLabel}>{t('settings.profilBearbeiten')}</label>
<p className={styles.settingDescription}>{t('settings.aendernSieIhrenNamenUnd')}</p>
<label className={styles.settingLabel}>{t('Profil bearbeiten')}</label>
<p className={styles.settingDescription}>{t('Ändern Sie Ihren Namen und')}</p>
</div>
<div className={styles.settingControl}>
<button className={styles.button} onClick={async () => { await refetchUser(); setIsProfileModalOpen(true); }}>{t('settings.profilOeffnen')}</button>
<button className={styles.button} onClick={async () => { await refetchUser(); setIsProfileModalOpen(true); }}>{t('Profil öffnen')}</button>
</div>
</div>
{currentUser && (
@ -538,18 +538,18 @@ export const SettingsPage: React.FC = () => {
{activeTab === 'appearance' && (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.darstellung')}</h2>
<h2 className={styles.sectionTitle}>{t('Darstellung')}</h2>
<div className={styles.settingRow}>
<div className={styles.settingInfo}><label className={styles.settingLabel}>Theme</label><p className={styles.settingDescription}>{t('settings.waehlenSieZwischenHellemUnd')}</p></div>
<div className={styles.settingInfo}><label className={styles.settingLabel}>Theme</label><p className={styles.settingDescription}>{t('Wählen zwischen Hell- und Dunkelmodus')}</p></div>
<div className={styles.settingControl}>
<div className={styles.themeToggle}>
<button className={`${styles.themeButton} ${theme === 'light' ? styles.active : ''}`} onClick={() => handleThemeChange('light')}>{t('settings.themeHell')}</button>
<button className={`${styles.themeButton} ${theme === 'dark' ? styles.active : ''}`} onClick={() => handleThemeChange('dark')}>{t('settings.themeDunkel')}</button>
<button className={`${styles.themeButton} ${theme === 'light' ? styles.active : ''}`} onClick={() => handleThemeChange('light')}>{t('Thema Hell')}</button>
<button className={`${styles.themeButton} ${theme === 'dark' ? styles.active : ''}`} onClick={() => handleThemeChange('dark')}>{t('Thema Dunkel')}</button>
</div>
</div>
</div>
<div className={styles.settingRow}>
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('settings.anzeigesprache')}</label><p className={styles.settingDescription}>{t('settings.spracheBeschreibung')}{languageError && <span className={styles.errorText}> {languageError}</span>}</p></div>
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('Anzeigesprache')}</label><p className={styles.settingDescription}>{t('Sprachbeschreibung')}{languageError && <span className={styles.errorText}> {languageError}</span>}</p></div>
<div className={styles.settingControl}>
<select className={styles.select} value={currentLanguage} onChange={(e) => handleLanguageChange(e.target.value)} disabled={isSavingLanguage}>
{availableLanguages.map((l) => (
@ -558,7 +558,7 @@ export const SettingsPage: React.FC = () => {
</option>
))}
</select>
{isSavingLanguage && <span className={styles.savingIndicator}>{t('settings.speichern')}</span>}
{isSavingLanguage && <span className={styles.savingIndicator}>{t('Speichern')}</span>}
</div>
</div>
</section>
@ -570,13 +570,13 @@ export const SettingsPage: React.FC = () => {
{activeTab === 'privacy' && (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.datenschutz')}</h2>
<h2 className={styles.sectionTitle}>{t('Datenschutz')}</h2>
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
{t('settings.datenschutzBeschreibung')}
{t('Datenschutzbeschreibung')}
</p>
<div className={styles.settingRow}>
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('settings.gdprPrivacy')}</label><p className={styles.settingDescription}>{t('settings.datenexportPortabilitaetUndKontoloeschung')}</p></div>
<div className={styles.settingControl}><Link className={`${styles.button} ${styles.linkButton}`} to="/gdpr">{t('settings.gdprOeffnen')}</Link></div>
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('GDPR Datenschutz')}</label><p className={styles.settingDescription}>{t('Datenexport, Portabilität und Kontolöschung')}</p></div>
<div className={styles.settingControl}><Link className={`${styles.button} ${styles.linkButton}`} to="/gdpr">{t('GDPR öffnen')}</Link></div>
</div>
</section>
)}

View file

@ -112,7 +112,7 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
disabled={isProcessing}
>
{isProcessing
? t('store.wirdAktiviert', t('store.activating'))
? t('Wird aktiviert…')
: t('Aktivieren für {name}', { name: String(m.label || m.name) })}
</button>
))}
@ -130,10 +130,7 @@ const StorePage: React.FC = () => {
<div className={styles.header}>
<h1>{t('Feature Store')}</h1>
<p className={styles.subtitle}>
{t(
'Aktiviere Features für dein Konto. Deine Daten sind isoliert und nur für dich sichtbar.',
t('store.activateFeaturesForYourAccount')
)}
{t('Aktiviere Features für dein Konto. Deine Daten sind isoliert und nur für dich sichtbar.')}
</p>
</div>
@ -163,7 +160,7 @@ const StorePage: React.FC = () => {
)}
{subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && (
<span className={styles.bannerSeparator}>
{t('store.trialEndet', t('store.trialEnds'))}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
{t('Testphase endet am')}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
</span>
)}
</div>
@ -173,11 +170,11 @@ const StorePage: React.FC = () => {
{loading ? (
<div className={styles.loading}>
{t('store.ladeFeatures', t('store.loadingFeatures'))}
{t('Lade Features…')}
</div>
) : features.length === 0 ? (
<div className={styles.empty}>
{t('store.keineFeaturesImStoreVerfuegbar', t('store.noFeaturesAvailableInThe'))}
{t('Keine Features im Store verfügbar.')}
</div>
) : (
<div className={styles.grid}>

View file

@ -341,7 +341,7 @@ export const AccessManagementHub: React.FC = () => {
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">{t('accessManagementHub.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map((m) => (
<option key={m.id} value={m.id}>
{getMandateName(m)}
@ -359,7 +359,7 @@ export const AccessManagementHub: React.FC = () => {
value={selectedFeatureCode}
onChange={(e) => setSelectedFeatureCode(e.target.value)}
>
<option value="">{t('accessManagementHub.alle')}</option>
<option value="">{t('Alle')}</option>
{features.map((f) => (
<option key={f.code} value={f.code}>
{getFeatureLabel(f, t)}
@ -430,7 +430,7 @@ export const AccessManagementHub: React.FC = () => {
) : !selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('accessManagementHub.keinMandantAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.
</p>
@ -453,7 +453,7 @@ export const AccessManagementHub: React.FC = () => {
<span className={hubStyles.statsValue}>
{loading || statsLoading ? '…' : overviewStats.users}
</span>
<span className={hubStyles.statsLabel}>{t('accessManagementHub.benutzer')}</span>
<span className={hubStyles.statsLabel}>{t('Benutzer')}</span>
</div>
</div>
<div className={hubStyles.statsCard}>
@ -462,7 +462,7 @@ export const AccessManagementHub: React.FC = () => {
<span className={hubStyles.statsValue}>
{loading || statsLoading ? '…' : overviewStats.roles}
</span>
<span className={hubStyles.statsLabel}>{t('accessManagementHub.rollenMax')}</span>
<span className={hubStyles.statsLabel}>{t('Rollen (max)')}</span>
</div>
</div>
{relationshipData && relationshipData.instances.length > 0 && (
@ -495,12 +495,12 @@ export const AccessManagementHub: React.FC = () => {
{loading && filteredInstances.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('accessManagementHub.ladeInstanzen')}</span>
<span>{t('Lade Instanzen')}</span>
</div>
) : filteredInstances.length === 0 ? (
<div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('accessManagementHub.keineFeatureinstanzen')}</h3>
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanzen')}</h3>
<p className={styles.emptyDescription}>
Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.
</p>
@ -521,7 +521,7 @@ export const AccessManagementHub: React.FC = () => {
<span
className={`${hubStyles.instanceBadge} ${inst.enabled ? hubStyles.badgeActive : hubStyles.badgeInactive}`}
>
{inst.enabled ? t('accessManagementHub.aktiv') : t('accessManagementHub.inaktiv')}
{inst.enabled ? t('Aktiv') : t('Inaktiv')}
</span>
</div>
<div className={hubStyles.instanceMeta}>
@ -542,7 +542,7 @@ export const AccessManagementHub: React.FC = () => {
className={hubStyles.cardAction}
onClick={() => handleSyncRoles(inst)}
disabled={!inst.enabled}
title={t('accessManagementHub.rollenSynchronisieren')}
title={t('Rollen synchronisieren')}
>
<FaCogs /> Rollen sync
</button>

View file

@ -89,8 +89,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
// Table columns
const columns = useMemo(() => [
{ key: 'label', label: t('adminFeatureAccess.name'), type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 },
{ key: 'featureCode', label: t('adminFeatureAccess.feature'), type: 'string' as const, sortable: true, filterable: true, width: 150,
{ key: 'label', label: t('Name'), type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 },
{ key: 'featureCode', label: t('Feature'), type: 'string' as const, sortable: true, filterable: true, width: 150,
render: (value: string) => {
const feature = features.find(f => f.code === value);
if (feature) {
@ -99,7 +99,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
return value;
}
},
{ key: 'enabled', label: t('adminFeatureAccess.aktiv'), type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
{ key: 'enabled', label: t('Aktiv'), type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
], [features, t]);
// Form attributes from backend - merge with dynamic feature options
@ -349,7 +349,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Feature-Instanzen</h1>
<p className={styles.pageSubtitle}>{t('adminFeatureAccess.verwaltenSieFeatureinstanzenFuerJeden')}</p>
<p className={styles.pageSubtitle}>{t('Verwalten Sie Feature-Instanzen für jeden')}</p>
</div>
</div>
@ -365,7 +365,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">{t('adminFeatureAccess.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>
{getMandateName(m)}
@ -399,7 +399,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
{features.length > 0 ? (
<div className={styles.infoBox}>
<FaCube style={{ marginRight: 8 }} />
<span>{t('adminFeatureAccess.verfuegbareFeatures')} </span>
<span>{t('Verfügbare Features')} </span>
{features.map((f, i) => (
<span key={f.code}>
{i > 0 && ', '}
@ -429,7 +429,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
{!selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminFeatureAccess.keinMandantAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.
</p>
@ -450,7 +450,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
actionButtons={[
{
type: 'delete' as const,
title: t('adminFeatureAccessPage.deleteInstance'),
title: t('Instanz löschen'),
}
]}
customActions={[
@ -458,13 +458,13 @@ export const AdminFeatureAccessPage: React.FC = () => {
id: 'edit',
icon: <FaEdit />,
onClick: handleEditClick,
title: t('adminFeatureAccessPage.editInstance'),
title: t('Instanz bearbeiten'),
},
{
id: 'syncRoles',
icon: <FaCogs />,
onClick: handleSyncRoles,
title: t('adminFeatureAccessPage.syncRoles'),
title: t('Rollen synchronisieren'),
loading: (row: FeatureInstance) => syncingInstance === row.id,
disabled: (row: FeatureInstance) => !row.enabled,
}
@ -474,7 +474,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
pagination: instancesPagination,
handleDelete: handleDeleteInstance,
}}
emptyMessage={t('adminFeatureAccess.keineFeatureinstanzenGefunden')}
emptyMessage={t('Keine Feature-Instanzen gefunden')}
/>
</div>
)}
@ -484,7 +484,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminFeatureAccess.neueFeatureinstanzErstellen')}</h2>
<h2 className={styles.modalTitle}>{t('Neue Feature-Instanz erstellen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -494,11 +494,11 @@ export const AdminFeatureAccessPage: React.FC = () => {
</div>
<div className={styles.modalContent}>
{features.length === 0 ? (
<p>{t('adminFeatureAccess.keineFeaturesVerfuegbarBitteWenden')}</p>
<p>{t('Keine Features verfügbar, bitte wenden')}</p>
) : createFields.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminFeatureAccess.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<div>
@ -523,7 +523,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotEnableWebResearch(true);
setChatbotAllowedProviders([]);
}}
placeholder={t('adminFeatureAccess.featureAuswaehlenErforderlich')}
placeholder={t('Feature-Auswahl erforderlich')}
className={styles.configSelect}
/>
{!createFeatureCode && (
@ -550,7 +550,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
type="text"
value={createLabel}
onChange={(value) => setCreateLabel(value)}
placeholder={t('adminFeatureAccess.instanzbezeichnungEingeben')}
placeholder={t('Instanzbezeichnung eingeben')}
className={styles.configSelect}
size="md"
required={true}
@ -589,8 +589,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotEnableWebResearch(true);
setChatbotAllowedProviders([]);
}}
submitButtonText={t('adminFeatureAccess.erstellen')}
cancelButtonText={t('adminFeatureAccess.abbrechen')}
submitButtonText={t('Erstellen')}
cancelButtonText={t('Abbrechen')}
/>
</div>
)}
@ -606,7 +606,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => { setShowEditModal(false); setEditingInstance(null); }}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminFeatureAccess.featureinstanzBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Feature-Instanz bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => { setShowEditModal(false); setEditingInstance(null); }}
@ -620,14 +620,14 @@ export const AdminFeatureAccessPage: React.FC = () => {
{
name: 'label',
type: 'string' as const,
label: t('adminFeatureAccessPage.bezeichnung'),
label: t('Bezeichnung'),
required: true,
editable: true,
},
{
name: 'enabled',
type: 'boolean' as const,
label: t('adminFeatureAccessPage.aktiviert'),
label: t('Aktiviert'),
required: false,
editable: true,
}
@ -643,8 +643,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotEnableWebResearch(true);
setChatbotAllowedProviders([]);
}}
submitButtonText={t('adminFeatureAccess.speichern')}
cancelButtonText={t('adminFeatureAccess.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
{/* Chatbot Configuration Section */}

View file

@ -203,7 +203,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
const columns = useMemo(() => [
{
key: 'username',
label: t('adminFeatureInstanceUsers.benutzername'),
label: t('Benutzername'),
type: 'text' as const,
sortable: true,
filterable: true,
@ -212,7 +212,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
},
{
key: 'email',
label: t('adminFeatureInstanceUsers.email'),
label: t('E-Mail'),
type: 'text' as const,
sortable: true,
filterable: true,
@ -221,7 +221,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
},
{
key: 'fullName',
label: t('adminFeatureInstanceUsers.vollstaendigerName'),
label: t('Vollständiger Name'),
type: 'text' as const,
sortable: true,
filterable: true,
@ -230,7 +230,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
},
{
key: 'roleLabels',
label: t('adminFeatureInstanceUsers.rollen'),
label: t('Rollen'),
type: 'text' as const,
sortable: false,
filterable: false,
@ -243,7 +243,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
},
{
key: 'enabled',
label: t('adminFeatureInstanceUsers.aktiv'),
label: t('Aktiv'),
type: 'boolean' as const,
sortable: true,
filterable: true,
@ -270,14 +270,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
return [
{
name: 'userId',
label: t('adminFeatureInstanceUsersPage.benutzer'),
label: t('Benutzer'),
type: 'enum' as const,
required: true,
options: userOptions,
},
{
name: 'roleIds',
label: t('adminFeatureInstanceUsersPage.rollen'),
label: t('Rollen'),
type: 'multiselect' as const,
required: true,
options: roleOptions,
@ -290,14 +290,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
return [
{
name: 'roleIds',
label: t('adminFeatureInstanceUsersPage.rollen'),
label: t('Rollen'),
type: 'multiselect' as const,
required: true,
options: roleOptions,
},
{
name: 'enabled',
label: t('adminFeatureInstanceUsersPage.aktiv'),
label: t('Aktiv'),
type: 'checkbox' as const,
required: false,
},
@ -409,8 +409,8 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminFeatureInstanceUsers.featureInstanzBenutzer')}</h1>
<p className={styles.pageSubtitle}>{t('adminFeatureInstanceUsers.verwaltenSieBenutzerzugriffeAufFeatureinstanzen')}</p>
<h1 className={styles.pageTitle}>{t('Feature-Instanz-Benutzer')}</h1>
<p className={styles.pageSubtitle}>{t('Verwalten Sie Benutzerzugriffe auf Feature-Instanzen')}</p>
</div>
</div>
@ -427,7 +427,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
onChange={(e) => setSelectedCombinedKey(e.target.value)}
disabled={loading || combinedOptions.length === 0}
>
<option value="">{t('adminFeatureInstanceUsers.mandantFeatureinstanzWaehlen')}</option>
<option value="">{t('Mandant / Feature-Instanz wählen')}</option>
{/* Group options by mandate */}
{(() => {
const groupedByMandate: Record<string, CombinedInstanceOption[]> = {};
@ -485,7 +485,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{/* Roles info box */}
{selectedInstance && instanceRoles.length > 0 && (
<div className={styles.infoBox}>
<span>{t('adminFeatureInstanceUsers.verfuegbareRollen')} </span>
<span>{t('Verfügbare Rollen')} </span>
{instanceRoles.map((r, i) => (
<span key={r.id}>
{i > 0 && ', '}
@ -499,7 +499,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{selectedInstance && instanceRoles.length === 0 && !usersLoading && (
<div className={styles.warningBox || styles.infoBox}>
<span> </span>
<span>{t('adminFeatureInstanceUsers.dieseInstanzHatNochKeine')}</span>
<span>{t('Diese Instanz hat noch keine')}</span>
</div>
)}
@ -507,11 +507,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{!selectedCombinedKey ? (
<div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminFeatureInstanceUsers.keineFeatureinstanzAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Keine Feature-Instanz ausgewählt')}</h3>
<p className={styles.emptyDescription}>
{combinedOptions.length === 0
? t('adminFeatureInstanceUsers.esGibtNochKeineFeatureinstanzen')
: t('adminFeatureInstanceUsers.waehlenSieEineFeatureinstanzAus')}
? t('Es gibt noch keine Feature-Instanzen')
: t('Wählen Sie eine Feature-Instanz aus')}
</p>
</div>
) : (
@ -531,11 +531,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
{
type: 'edit' as const,
onAction: handleEditClick,
title: t('adminFeatureInstanceUsersPage.editRoles'),
title: t('Rollen bearbeiten'),
},
{
type: 'delete' as const,
title: t('adminFeatureInstanceUsersPage.removeFromInstance'),
title: t('Aus Instanz entfernen'),
}
]}
onDelete={handleRemoveUser}
@ -552,7 +552,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
return false;
},
}}
emptyMessage={t('adminFeatureInstanceUsers.keineBenutzerGefunden')}
emptyMessage={t('Keine Benutzer gefunden')}
/>
</div>
)}
@ -562,7 +562,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminFeatureInstanceUsers.benutzerZurFeatureinstanzHinzufuegen')}</h2>
<h2 className={styles.modalTitle}>{t('Benutzer zur Feature-Instanz hinzufügen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowAddModal(false)}
@ -572,17 +572,17 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
</div>
<div className={styles.modalContent}>
{availableUsers.length === 0 ? (
<p>{t('adminFeatureInstanceUsers.alleBenutzerHabenBereitsZugriff')}</p>
<p>{t('Alle Benutzer haben bereits Zugriff')}</p>
) : instanceRoles.length === 0 ? (
<p>{t('adminFeatureInstanceUsers.dieseFeatureinstanzHatKeineRollen')}</p>
<p>{t('Diese Feature-Instanz hat keine Rollen')}</p>
) : (
<FormGeneratorForm
attributes={addUserFields}
mode="create"
onSubmit={handleAddUser}
onCancel={() => setShowAddModal(false)}
submitButtonText={t('adminFeatureInstanceUsers.hinzufuegen')}
cancelButtonText={t('adminFeatureInstanceUsers.abbrechen')}
submitButtonText={t('Hinzufügen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -610,8 +610,8 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
mode="edit"
onSubmit={handleEditRoles}
onCancel={() => setEditingUser(null)}
submitButtonText={t('adminFeatureInstanceUsers.speichern')}
cancelButtonText={t('adminFeatureInstanceUsers.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>

View file

@ -70,7 +70,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
}
} catch (err: any) {
console.error('Error loading features:', err);
setError(t('adminFeatureRoles.fehlerBeimLadenDerFeatures'));
setError(t('Fehler beim Laden der Features'));
}
};
loadFeatures();
@ -112,7 +112,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
}
} catch (err: any) {
console.error('Error loading feature roles:', err);
setError(t('adminFeatureRoles.fehlerBeimLadenDerFeaturerollen'));
setError(t('Fehler beim Laden der Feature-Rollen'));
setRoles([]);
setPagination(null);
} finally {
@ -135,7 +135,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
const columns = useMemo(() => [
{
key: 'roleLabel',
label: t('adminFeatureRoles.rollenLabel'),
label: t('Rollen-Label'),
type: 'string' as const,
sortable: true,
filterable: true,
@ -144,7 +144,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
},
{
key: 'description',
label: t('adminFeatureRoles.beschreibung'),
label: t('Beschreibung'),
type: 'string' as const,
sortable: false,
width: 300,
@ -152,7 +152,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
},
{
key: 'featureCode',
label: t('adminFeatureRoles.feature'),
label: t('Feature'),
type: 'string' as const,
sortable: true,
filterable: true,
@ -170,17 +170,17 @@ export const AdminFeatureRolesPage: React.FC = () => {
const fields: AttributeDefinition[] = [
{
name: 'roleLabel',
label: t('adminFeatureRolesPage.rollenLabel'),
label: t('Rollen-Label'),
type: 'string',
required: true,
description: t('adminFeatureRolesPage.rollenLabelBeschreibung')
description: t('Rollen-Label Beschreibung')
},
{
name: 'description',
label: t('adminFeatureRolesPage.beschreibung'),
label: t('Beschreibung'),
type: 'multilingual',
required: false,
description: t('adminFeatureRolesPage.mehrsprachigeBeschreibung')
description: t('Mehrsprachige Beschreibung')
}
];
return fields;
@ -191,18 +191,18 @@ export const AdminFeatureRolesPage: React.FC = () => {
return [
{
name: 'roleLabel',
label: t('adminFeatureRolesPage.rollenLabel'),
label: t('Rollen-Label'),
type: 'string',
required: true,
readonly: true,
description: t('adminFeatureRolesPage.rollenLabelReadonly')
description: t('Rollen-Label (nur lesen)')
},
{
name: 'description',
label: t('adminFeatureRolesPage.beschreibung'),
label: t('Beschreibung'),
type: 'multilingual',
required: false,
description: t('adminFeatureRolesPage.mehrsprachigeBeschreibung')
description: t('Mehrsprachige Beschreibung')
}
];
}, [t]);
@ -287,8 +287,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminFeatureRoles.featureRollenRechte')}</h1>
<p className={styles.pageSubtitle}>{t('adminFeatureRoles.templaterollenUndDerenBerechtigungenFuer')}</p>
<h1 className={styles.pageTitle}>{t('Feature-Rollen-Rechte')}</h1>
<p className={styles.pageSubtitle}>{t('Template-Rollen und deren Berechtigungen für')}</p>
</div>
</div>
@ -304,7 +304,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
value={selectedFeatureCode}
onChange={(e) => setSelectedFeatureCode(e.target.value)}
>
<option value="">{t('adminFeatureRoles.featureWaehlen')}</option>
<option value="">{t('Feature wählen')}</option>
{features.map(f => {
const featureCode = f.code || f.featureCode || '';
return (
@ -350,7 +350,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
{!selectedFeatureCode ? (
<div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminFeatureRoles.keinFeatureAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.
</p>
@ -372,11 +372,11 @@ export const AdminFeatureRolesPage: React.FC = () => {
{
type: 'edit' as const,
onAction: handleEditClick,
title: t('adminFeatureRolesPage.editRole'),
title: t('Rolle bearbeiten'),
},
{
type: 'delete' as const,
title: t('adminFeatureRolesPage.deleteRole'),
title: t('Rolle löschen'),
}
]}
customActions={[
@ -384,7 +384,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
id: 'permissions',
icon: <FaShieldAlt />,
onClick: (role: FeatureRole) => setPermissionsRole(role),
title: t('adminFeatureRolesPage.managePermissions'),
title: t('Berechtigungen verwalten'),
}
]}
onDelete={handleDeleteRole}
@ -393,7 +393,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
pagination,
handleDelete: handleDeleteRole,
}}
emptyMessage={t('adminFeatureRoles.keineFeaturerollenGefunden')}
emptyMessage={t('Keine Feature-Rollen gefunden')}
/>
</div>
)}
@ -403,7 +403,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminFeatureRoles.neueFeaturerolleErstellen')}</h2>
<h2 className={styles.modalTitle}>{t('Neue Feature-Rolle erstellen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -422,7 +422,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
onSubmit={handleCreateRole}
onCancel={() => setShowCreateModal(false)}
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'}
cancelButtonText={t('adminFeatureRoles.abbrechen')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>
@ -434,7 +434,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingRole(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminFeatureRoles.featurerolleBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Feature-Rolle bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingRole(null)}
@ -453,8 +453,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
mode="edit"
onSubmit={handleEditRole}
onCancel={() => setEditingRole(null)}
submitButtonText={isSubmitting ? t('adminFeatureRoles.speichern') : t('adminFeatureRoles.speichern')}
cancelButtonText={t('adminFeatureRoles.abbrechen')}
submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>
@ -481,7 +481,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} />
<span>Feature: <strong>{permissionsRole.featureCode}</strong></span>
<span style={{ marginLeft: '1rem' }}>{t('adminFeatureRoles.templaterolleGlobal')}</span>
<span style={{ marginLeft: '1rem' }}>{t('Template-Rolle global')}</span>
</div>
<AccessRulesEditor
roleId={permissionsRole.id}

View file

@ -87,7 +87,7 @@ export const AdminInvitationsPage: React.FC = () => {
const columns = useMemo(() => [
{
key: 'targetUsername',
label: t('adminInvitations.benutzername'),
label: t('Benutzername'),
type: 'string' as const,
sortable: true,
filterable: true,
@ -96,7 +96,7 @@ export const AdminInvitationsPage: React.FC = () => {
},
{
key: 'email',
label: t('adminInvitations.email'),
label: t('E-Mail'),
type: 'string' as const,
sortable: true,
filterable: true,
@ -105,7 +105,7 @@ export const AdminInvitationsPage: React.FC = () => {
const emailText = value || '-';
const emailSent = (row as any).emailSent;
return (
<span title={emailSent ? t('adminInvitations.emailWurdeGesendet') : t('adminInvitations.emailNichtGesendet')}>
<span title={emailSent ? t('E-Mail wurde gesendet') : t('E-Mail nicht gesendet')}>
{emailText} {emailSent && '✓'}
</span>
);
@ -113,7 +113,7 @@ export const AdminInvitationsPage: React.FC = () => {
},
{
key: 'roleIds',
label: t('adminInvitations.rollen'),
label: t('Rollen'),
type: 'string', // Array rendered as string
sortable: false,
filterable: false,
@ -128,7 +128,7 @@ export const AdminInvitationsPage: React.FC = () => {
} as any,
{
key: 'expiresAt',
label: t('adminInvitations.gueltigBis'),
label: t('Gültig bis'),
type: 'number' as const,
sortable: true,
width: 150,
@ -144,7 +144,7 @@ export const AdminInvitationsPage: React.FC = () => {
},
{
key: 'currentUses',
label: t('adminInvitations.verwendet'),
label: t('Verwendet'),
type: 'string' as const,
sortable: true,
width: 100,
@ -152,7 +152,7 @@ export const AdminInvitationsPage: React.FC = () => {
},
{
key: 'createdAt',
label: t('adminInvitations.erstellt'),
label: t('Erstellt'),
type: 'number' as const,
sortable: true,
width: 150,
@ -178,7 +178,7 @@ export const AdminInvitationsPage: React.FC = () => {
// Add helper field expiresInHours if not in model but fields exist
if (fields.length > 0 && !fields.find(f => f.name === 'expiresInHours')) {
fields.push({ name: 'expiresInHours', label: t('adminInvitationsPage.gueltigkeitsdauerStunden'), type: 'number',
fields.push({ name: 'expiresInHours', label: t('Gültigkeitsdauer (Stunden)'), type: 'number',
required: true, default: 72 } as any);
}
// Override required for targetUsername and email (both required for invitations)
@ -262,8 +262,8 @@ export const AdminInvitationsPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminInvitations.einladungen')}</h1>
<p className={styles.pageSubtitle}>{t('adminInvitations.erstellenUndVerwaltenSieEinladungen')}</p>
<h1 className={styles.pageTitle}>{t('Einladungen')}</h1>
<p className={styles.pageSubtitle}>{t('Erstellen und verwalten Sie Einladungen')}</p>
</div>
</div>
@ -279,7 +279,7 @@ export const AdminInvitationsPage: React.FC = () => {
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">{t('adminInvitations.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>
{getMandateName(m)}
@ -330,7 +330,7 @@ export const AdminInvitationsPage: React.FC = () => {
{!selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminInvitations.keinMandantAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Mandanten aus, um dessen Einladungen zu verwalten.
</p>
@ -351,7 +351,7 @@ export const AdminInvitationsPage: React.FC = () => {
actionButtons={[
{
type: 'delete' as const,
title: t('adminInvitationsPage.revokeInvitation'),
title: t('Einladung widerrufen'),
}
]}
customActions={[
@ -359,7 +359,7 @@ export const AdminInvitationsPage: React.FC = () => {
id: 'showUrl',
icon: <FaLink />,
onClick: handleShowUrl,
title: t('adminInvitationsPage.showInvitationLink'),
title: t('Einladungslink anzeigen'),
}
]}
hookData={{
@ -367,7 +367,7 @@ export const AdminInvitationsPage: React.FC = () => {
refetch: (params?: any) => fetchInvitations(params || selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
pagination,
}}
emptyMessage={t('adminInvitations.keineEinladungenGefunden')}
emptyMessage={t('Keine Einladungen gefunden')}
/>
</div>
)}
@ -377,7 +377,7 @@ export const AdminInvitationsPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminInvitations.neueEinladungErstellen')}</h2>
<h2 className={styles.modalTitle}>{t('Neue Einladung erstellen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -389,12 +389,12 @@ export const AdminInvitationsPage: React.FC = () => {
{roles.filter(r => !r.featureInstanceId).length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminInvitations.ladeRollen')}</span>
<span>{t('Rollen laden')}</span>
</div>
) : createFields.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminInvitations.ladeFormular')}</span>
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
@ -402,8 +402,8 @@ export const AdminInvitationsPage: React.FC = () => {
mode="create"
onSubmit={handleCreateInvitation}
onCancel={() => setShowCreateModal(false)}
submitButtonText={t('adminInvitations.einladungErstellen')}
cancelButtonText={t('adminInvitations.abbrechen')}
submitButtonText={t('Einladung erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -439,7 +439,7 @@ export const AdminInvitationsPage: React.FC = () => {
<button
className={styles.copyButton}
onClick={() => handleCopyUrl(showUrlModal.inviteUrl)}
title={t('adminInvitations.inZwischenablageKopieren')}
title={t('In Zwischenablage kopieren')}
>
<FaCopy />
{copySuccess ? ' Kopiert!' : ' Kopieren'}

View file

@ -5,6 +5,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FaDownload, FaFileExport, FaFileImport, FaRedo, FaSync, FaTrash } from 'react-icons/fa';
import api from '../../api';
import axios from 'axios';
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable/FormGeneratorTable';
import { useConfirm } from '../../hooks/useConfirm';
import { useLanguage } from '../../providers/language/LanguageContext';
@ -39,19 +40,30 @@ type ProgressInfo = {
function _getColumns(t: (key: string) => string): ColumnConfig[] {
return [
{ key: 'id', label: t('adminLanguages.code'), type: 'text', sortable: true, filterable: true, width: 90 },
{ key: 'label', label: t('adminLanguages.bezeichnung'), type: 'text', sortable: true, filterable: true, width: 200 },
{ key: 'status', label: t('adminLanguages.status'), type: 'text', sortable: true, filterable: true, width: 120 },
{ key: 'uiCount', label: t('adminLanguages.ui'), type: 'number', sortable: true, width: 80 },
{ key: 'gatewayCount', label: t('adminLanguages.api'), type: 'number', sortable: true, width: 80 },
{ key: 'entriesCount', label: t('adminLanguages.total'), type: 'number', sortable: true, width: 80 },
{ key: 'id', label: t('Code'), type: 'text', sortable: true, filterable: true, width: 90 },
{ key: 'label', label: t('Bezeichnung'), type: 'text', sortable: true, filterable: true, width: 200 },
{ key: 'status', label: t('Status'), type: 'text', sortable: true, filterable: true, width: 120 },
{ key: 'uiCount', label: t('UI'), type: 'number', sortable: true, width: 80 },
{ key: 'gatewayCount', label: t('API'), type: 'number', sortable: true, width: 80 },
{ key: 'entriesCount', label: t('Gesamt'), type: 'number', sortable: true, width: 80 },
];
}
const _PRIORITY_CODES = ['de', 'en', 'fr', 'it'];
const _PRIORITY_CODES = ['de', 'gsw', 'en', 'fr', 'it'];
function _isAbortError(e: unknown): boolean {
if (axios.isCancel(e)) return true;
if (e && typeof e === 'object') {
const err = e as { code?: string; name?: string };
if (err.code === 'ERR_CANCELED' || err.name === 'AbortError' || err.name === 'CanceledError') return true;
}
return false;
}
const _isoChoices: { value: string; label: string }[] = [
{ value: 'de', label: 'de — Deutsch' }, { value: 'en', label: 'en — English' },
{ value: 'de', label: 'de — Deutsch' },
{ value: 'gsw', label: 'gsw — Schweizerdeutsch' },
{ value: 'en', label: 'en — English' },
{ value: 'fr', label: 'fr — Français' }, { value: 'it', label: 'it — Italiano' },
{ value: 'es', label: 'es — Español' }, { value: 'pt', label: 'pt — Português' },
{ value: 'nl', label: 'nl — Nederlands' }, { value: 'pl', label: 'pl — Polski' },
@ -101,7 +113,12 @@ const _isoChoices: { value: string; label: string }[] = [
// Progress overlay component
// ---------------------------------------------------------------------------
const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) => {
const _ProgressOverlay: React.FC<{
progress: ProgressInfo;
onAbort?: () => void;
}> = ({ progress, onAbort }) => {
const { t } = useLanguage();
const canAbort = Boolean(onAbort && !progress.done && !progress.error);
const master = progress.keysMasterTotal;
const pending = progress.keysPending ?? 0;
const cur = progress.keysCurrent ?? 0;
@ -246,6 +263,24 @@ const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) =>
{progress.error}
</p>
)}
{canAbort && (
<button
type="button"
onClick={onAbort}
style={{
marginTop: '1.25rem',
padding: '0.5rem 1.25rem',
borderRadius: 6,
border: '1px solid var(--border-color, #cbd5e1)',
background: 'var(--surface-color, #f8fafc)',
color: 'var(--text-primary, #1e293b)',
fontWeight: 600,
cursor: 'pointer',
}}
>
{t('Abbrechen')}
</button>
)}
</div>
</div>
);
@ -265,6 +300,19 @@ export const AdminLanguagesPage: React.FC = () => {
const [progress, setProgress] = useState<ProgressInfo | null>(null);
const [search, setSearch] = useState('');
const busyRef = useRef(false);
const abortRef = useRef<AbortController | null>(null);
const _endProgressSoon = useCallback((ms: number) => {
window.setTimeout(() => {
setProgress(null);
busyRef.current = false;
abortRef.current = null;
}, ms);
}, []);
const _abortRunning = useCallback(() => {
abortRef.current?.abort();
}, []);
const _load = useCallback(async () => {
try {
@ -289,6 +337,29 @@ export const AdminLanguagesPage: React.FC = () => {
}
}, []);
/** Einheitliche Abbruch-Meldung + Overlay-Ausblendung; optional Listen neu laden (teilweise fertige „Alle aktualisieren“). */
const _finishProgressAborted = useCallback(
async (
partial: Pick<ProgressInfo, 'current' | 'total'> & Partial<Pick<ProgressInfo, 'progressHeading'>>,
ms: number,
refreshLists: boolean,
) => {
setProgress({
message: t('Vorgang abgebrochen.'),
error: t('Die Operation wurde abgebrochen.'),
done: true,
...partial,
});
if (refreshLists) {
await _load();
await refreshAvailableLanguages();
await reloadLanguage();
}
_endProgressSoon(ms);
},
[t, _endProgressSoon, _load, refreshAvailableLanguages, reloadLanguage],
);
useEffect(() => {
_load();
}, [_load]);
@ -326,10 +397,10 @@ export const AdminLanguagesPage: React.FC = () => {
}
}, [addChoices, addCode]);
const _fetchI18nEntriesFromBundle = useCallback(async (): Promise<any[]> => {
const _fetchI18nEntriesFromBundle = useCallback(async (signal?: AbortSignal): Promise<any[]> => {
const base = import.meta.env.BASE_URL || '/';
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
const res = await fetch(`${normalizedBase}i18n-keys.json`);
const res = await fetch(`${normalizedBase}i18n-keys.json`, { signal });
if (!res.ok) {
throw new Error(
t('i18n-keys.json nicht gefunden. Bitte Frontend neu bauen oder Dev-Server starten.'),
@ -347,11 +418,14 @@ export const AdminLanguagesPage: React.FC = () => {
const _syncXx = async () => {
if (busyRef.current) return;
busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setError(null);
setProgress({ message: t('Basisset wird eingelesen…'), current: 0, total: 1 });
try {
const entries = await _fetchI18nEntriesFromBundle();
const res = await api.put('/api/i18n/sets/sync-xx', { entries });
const entries = await _fetchI18nEntriesFromBundle(signal);
const res = await api.put('/api/i18n/sets/sync-xx', { entries }, { signal });
const d = res.data || {};
const addedCount = d.added?.length ?? 0;
const removedCount = d.removed?.length ?? 0;
@ -369,29 +443,40 @@ export const AdminLanguagesPage: React.FC = () => {
await _load();
await refreshAvailableLanguages();
await reloadLanguage();
_endProgressSoon(2500);
} catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler beim Einlesen'), current: 0, total: 1, error: msg, done: true });
setError(msg);
} finally {
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2500);
_endProgressSoon(2500);
}
};
const _updateOne = async (code: string) => {
if (busyRef.current) return;
busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setError(null);
const label = rows.find((r) => r.id === code)?.label || code;
let keysCurrent: number | undefined;
let keysPending: number | undefined;
let keysMasterTotal: number | undefined;
try {
const dr = await api.get(`/api/i18n/sets/${encodeURIComponent(code)}/sync-diff`);
const dr = await api.get(`/api/i18n/sets/${encodeURIComponent(code)}/sync-diff`, { signal });
keysCurrent = dr.data?.currentEntryCount;
keysPending = dr.data?.addedCount;
keysMasterTotal = dr.data?.masterEntryCount;
} catch {
} catch (e) {
if (_isAbortError(e)) {
await _finishProgressAborted({ progressHeading: label, current: 0, total: 1 }, 2200, false);
return;
}
/* sync-diff optional */
}
setProgress({
@ -404,7 +489,7 @@ export const AdminLanguagesPage: React.FC = () => {
keysMasterTotal,
});
try {
const putRes = await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`);
const putRes = await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`, {}, { signal });
const d = putRes.data || {};
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
setProgress({
@ -421,7 +506,12 @@ export const AdminLanguagesPage: React.FC = () => {
await _load();
await refreshAvailableLanguages();
await reloadLanguage();
_endProgressSoon(2000);
} catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ progressHeading: label, current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message;
setProgress({
message: t('Fehler bei {lang}', { lang: label }),
@ -432,8 +522,7 @@ export const AdminLanguagesPage: React.FC = () => {
done: true,
});
setError(msg);
} finally {
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2000);
_endProgressSoon(2000);
}
};
@ -446,6 +535,9 @@ export const AdminLanguagesPage: React.FC = () => {
if (!ok) return;
busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setError(null);
const langCodes = rows.filter((r) => r.id !== 'xx').map((r) => r.id);
@ -455,30 +547,52 @@ export const AdminLanguagesPage: React.FC = () => {
setProgress({ message: t('Basisset wird eingelesen…'), current: step, total: totalSteps });
try {
const entries = await _fetchI18nEntriesFromBundle();
await api.put('/api/i18n/sets/sync-xx', { entries });
const entries = await _fetchI18nEntriesFromBundle(signal);
await api.put('/api/i18n/sets/sync-xx', { entries }, { signal });
step++;
setProgress({ message: t('Basisset synchronisiert.'), current: step, total: totalSteps });
} catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: step, total: totalSteps }, 2800, false);
return;
}
const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler beim Basisset'), current: step, total: totalSteps, error: msg, done: true });
setError(msg);
setTimeout(() => { setProgress(null); busyRef.current = false; }, 3000);
_endProgressSoon(3000);
return;
}
const errors: string[] = [];
for (const code of langCodes) {
if (signal.aborted) {
setProgress({
message: t('Vorgang abgebrochen.'),
current: step,
total: totalSteps,
error: t('Die Operation wurde abgebrochen.'),
done: true,
});
await _load();
await refreshAvailableLanguages();
await reloadLanguage();
_endProgressSoon(3500);
return;
}
const label = rows.find((r) => r.id === code)?.label || code;
let keysCurrent: number | undefined;
let keysPending: number | undefined;
let keysMasterTotal: number | undefined;
try {
const dr = await api.get(`/api/i18n/sets/${encodeURIComponent(code)}/sync-diff`);
const dr = await api.get(`/api/i18n/sets/${encodeURIComponent(code)}/sync-diff`, { signal });
keysCurrent = dr.data?.currentEntryCount;
keysPending = dr.data?.addedCount;
keysMasterTotal = dr.data?.masterEntryCount;
} catch {
} catch (e) {
if (_isAbortError(e)) {
await _finishProgressAborted({ progressHeading: label, current: step + 1, total: totalSteps }, 3500, true);
return;
}
/* sync-diff optional */
}
setProgress({
@ -490,7 +604,7 @@ export const AdminLanguagesPage: React.FC = () => {
keysMasterTotal,
});
try {
const putRes = await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`);
const putRes = await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`, {}, { signal });
const d = putRes.data || {};
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
setProgress({
@ -504,6 +618,10 @@ export const AdminLanguagesPage: React.FC = () => {
keysTranslated: typeof d.translated === 'number' ? d.translated : undefined,
});
} catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ progressHeading: label, current: step + 1, total: totalSteps }, 3500, true);
return;
}
errors.push(`${code}: ${e.response?.data?.detail || e.message}`);
}
step++;
@ -531,7 +649,7 @@ export const AdminLanguagesPage: React.FC = () => {
await _load();
await refreshAvailableLanguages();
await reloadLanguage();
setTimeout(() => { setProgress(null); busyRef.current = false; }, errors.length > 0 ? 5000 : 2500);
_endProgressSoon(errors.length > 0 ? 5000 : 2500);
};
// --- Other actions (unchanged logic, but with busy guard) -----------------
@ -584,18 +702,25 @@ export const AdminLanguagesPage: React.FC = () => {
);
if (!go) return;
busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setProgress({ message: t('Sprache wird erstellt…'), current: 0, total: 1 });
try {
await api.post('/api/i18n/sets', { code });
await api.post('/api/i18n/sets', { code }, { signal });
setProgress({ message: t('Sprache erstellt. KI-Übersetzung läuft im Hintergrund.'), current: 1, total: 1, done: true });
await _load();
await refreshAvailableLanguages();
_endProgressSoon(2500);
} catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler'), current: 0, total: 1, error: msg, done: true });
setError(msg);
} finally {
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2500);
_endProgressSoon(2500);
}
};
@ -630,12 +755,16 @@ export const AdminLanguagesPage: React.FC = () => {
);
if (!ok) return;
busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setProgress({ message: t('Importiere…'), current: 0, total: 1 });
try {
const formData = new FormData();
formData.append('file', file);
const res = await api.post('/api/i18n/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
signal,
});
const d = res.data || {};
setError(null);
@ -651,12 +780,16 @@ export const AdminLanguagesPage: React.FC = () => {
await _load();
await refreshAvailableLanguages();
await reloadLanguage();
_endProgressSoon(2500);
} catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Import fehlgeschlagen'), current: 0, total: 1, error: msg, done: true });
setError(msg);
} finally {
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2500);
_endProgressSoon(2500);
}
};
input.click();
@ -755,7 +888,7 @@ export const AdminLanguagesPage: React.FC = () => {
emptyMessage={t('Keine Einträge')}
/>
{progress && <_ProgressOverlay progress={progress} />}
{progress && <_ProgressOverlay progress={progress} onAbort={_abortRunning} />}
</div>
<ConfirmDialog />

View file

@ -108,7 +108,7 @@ export const AdminLogsPage: React.FC = () => {
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminLogs.gatewayLogs')}</h1>
<h1 className={styles.pageTitle}>{t('Gateway-Logs')}</h1>
<p className={styles.pageSubtitle}>
{lines.length > 0
? `${lines.length} Einträge`
@ -121,7 +121,7 @@ export const AdminLogsPage: React.FC = () => {
className={styles.secondaryButton}
onClick={_handleDownload}
disabled={lines.length === 0}
title={t('adminLogs.logHerunterladen')}
title={t('Log herunterladen')}
>
<FaDownload /> Download
</button>
@ -140,7 +140,7 @@ export const AdminLogsPage: React.FC = () => {
min={1}
max={50000}
/>
<label className={logStyles.controlLabel}>{t('adminLogs.eintraege')}</label>
<label className={logStyles.controlLabel}>{t('Einträge')}</label>
<button
className={styles.primaryButton}
onClick={_handleLoad}
@ -183,7 +183,7 @@ export const AdminLogsPage: React.FC = () => {
{lines.length === 0 && !loading && (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>📋</div>
<p className={styles.emptyTitle}>{t('adminLogs.keineLogsGeladen')}</p>
<p className={styles.emptyTitle}>{t('Keine Logs geladen')}</p>
<p className={styles.emptyDescription}>
Gib die gewünschte Anzahl Einträge ein und klicke auf "Laden".
</p>
@ -192,7 +192,7 @@ export const AdminLogsPage: React.FC = () => {
{loading && lines.length === 0 && (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminLogs.logsWerdenGeladen')}</span>
<span>{t('Logs werden geladen')}</span>
</div>
)}
{lines.map((line, idx) => {

View file

@ -216,9 +216,9 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
// Filter options for scope
const scopeOptions = useMemo(() => [
{ value: 'mandate', label: t('adminMandateRolePermissionsPage.mandantenRollen') },
{ value: 'all', label: t('adminMandateRolePermissionsPage.alleInklTemplates') },
{ value: 'global', label: t('adminMandateRolePermissionsPage.nurTemplates') },
{ value: 'mandate', label: t('Mandanten-Rollen') },
{ value: 'all', label: t('Alle inkl. Templates') },
{ value: 'global', label: t('Nur Templates') },
], [t]);
if (error) {
@ -253,7 +253,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
className={styles.secondaryButton}
onClick={_openCleanupModal}
disabled={loading}
title={t('adminMandateRolePermissions.doppelteRegelnFindenUndBereinigen')}
title={t('Doppelte Regeln finden und bereinigen')}
>
<FaBroom /> Duplikate bereinigen
</button>
@ -270,7 +270,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{/* Filters */}
<div className={styles.filterBar}>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('adminMandateRolePermissions.mandant')}</label>
<label className={styles.filterLabel}>{t('Mandant')}</label>
<select
className={styles.filterSelect}
value={selectedMandateId}
@ -314,7 +314,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{loading && (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminMandateRolePermissions.ladeRollen')}</span>
<span>{t('Lade Rollen')}</span>
</div>
)}
@ -322,13 +322,13 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{!loading && roles.length === 0 && (
<div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} />
<p>{t('adminMandateRolePermissions.keineRollenGefunden')}</p>
<p>{t('Keine Rollen gefunden')}</p>
<p className={styles.emptyHint}>
{scopeFilter === 'mandate'
? 'Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.'
: scopeFilter === 'global'
? t('adminMandateRolePermissions.esGibtNochKeineRollentemplates')
: t('adminMandateRolePermissions.esGibtNochKeineRollen')}
? t('Es gibt noch keine Rollentemplates')
: t('Es gibt noch keine Rollen')}
</p>
</div>
)}
@ -403,7 +403,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{cleanupLoading && (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{cleanupPhase === 'idle' ? t('adminMandateRolePermissions.analysiereDuplikate') : t('adminMandateRolePermissions.bereinigeDuplikate')}</span>
<span>{cleanupPhase === 'idle' ? t('Analysiere Duplikate') : t('Bereinige Duplikate')}</span>
</div>
)}
@ -422,11 +422,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1.25rem' }}>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.totalRules}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.regelnTotal')}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Regeln total')}</div>
</div>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.uniqueSignatures}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.eindeutigeRegeln')}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Eindeutige Regeln')}</div>
</div>
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateGroups > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateGroups > 0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}</div>
@ -446,14 +446,14 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{cleanupPhase === 'done' && (
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
<FaCheckCircle />
<span><strong>{cleanupResult.deletedCount}</strong> {t('adminMandateRolePermissions.doppelteRegelnWurdenErfolgreichEntfernt')}</span>
<span><strong>{cleanupResult.deletedCount}</strong> {t('Doppelte Regeln wurden erfolgreich entfernt')}</span>
</div>
)}
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
<FaCheckCircle />
<span>{t('adminMandateRolePermissions.keineDuplikateGefundenAllesSauber')}</span>
<span>{t('Keine Duplikate gefunden, alles sauber')}</span>
</div>
)}
@ -505,11 +505,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1rem' }}>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{templateFixResult.totalUserMandateRoles}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.rollenzuweisungenTotal')}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Rollenzuweisungen total')}</div>
</div>
<div style={{ padding: '0.75rem', background: templateFixResult.invalidAssignments > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${templateFixResult.invalidAssignments > 0 ? '#fc8181' : '#9ae6b4'}` }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.invalidAssignments > 0 ? '#c53030' : '#2f855a' }}>{templateFixResult.invalidAssignments}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.templateStattInstanz')}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Template statt Instanz')}</div>
</div>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.fixedCount > 0 ? '#2f855a' : 'var(--text-primary)' }}>
@ -526,8 +526,8 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
<thead>
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('adminMandateRolePermissions.rolle')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('adminMandateRolePermissions.mandant')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Rolle')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Mandant')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Aktion</th>
</tr>
</thead>
@ -567,7 +567,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{templateFixResult && templateFixResult.invalidAssignments === 0 && (
<div style={{ marginTop: '1rem', padding: '0.5rem 0.75rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', fontSize: '0.875rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
<FaCheckCircle />
<span>{t('adminMandateRolePermissions.keineFehlerhaftenTemplaterollenzuweisungen')}</span>
<span>{t('Keine fehlerhaften Templaterollenzuweisungen')}</span>
</div>
)}
</>
@ -576,7 +576,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
<div className={styles.modalFooter}>
<button className={styles.secondaryButton} onClick={_closeCleanupModal}>
{cleanupPhase === 'done' ? t('adminMandateRolePermissions.schliessen') : t('adminMandateRolePermissions.abbrechen')}
{cleanupPhase === 'done' ? t('Schließen') : t('Abbrechen')}
</button>
{cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && (
<button

View file

@ -103,7 +103,7 @@ export const AdminMandateRolesPage: React.FC = () => {
const columns = useMemo(() => [
{
key: 'roleLabel',
label: t('adminMandateRoles.bezeichnung'),
label: t('Bezeichnung'),
type: 'string' as const,
sortable: true,
filterable: true,
@ -112,7 +112,7 @@ export const AdminMandateRolesPage: React.FC = () => {
},
{
key: 'description',
label: t('adminMandateRoles.beschreibung'),
label: t('Beschreibung'),
type: 'string' as const,
sortable: false,
filterable: false,
@ -121,7 +121,7 @@ export const AdminMandateRolesPage: React.FC = () => {
},
{
key: 'scopeType',
label: t('adminMandateRoles.geltungsbereich'),
label: t('Geltungsbereich'),
type: 'string' as const,
sortable: true,
filterable: true,
@ -162,13 +162,13 @@ export const AdminMandateRolesPage: React.FC = () => {
if (fields.length > 0) {
fields.push({
name: 'scope',
label: t('adminMandateRolesPage.geltungsbereich'),
label: t('Geltungsbereich'),
type: 'enum' as any,
required: true,
default: 'mandate',
options: [
{ value: 'mandate', label: t('adminMandateRolesPage.nurDieserMandant') },
{ value: 'global', label: t('adminMandateRolesPage.templateBeiNeuenMandanten') },
{ value: 'mandate', label: t('Nur dieser Mandant') },
{ value: 'global', label: t('Template bei neuen Mandanten') },
]
});
}
@ -314,8 +314,8 @@ export const AdminMandateRolesPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminMandateRoles.rollen')}</h1>
<p className={styles.pageSubtitle}>{t('adminMandateRoles.verwaltenSieSystemGlobaleUnd')}</p>
<h1 className={styles.pageTitle}>{t('Rollen')}</h1>
<p className={styles.pageSubtitle}>{t('Verwalten Sie systemweite und globale')}</p>
</div>
<div className={styles.headerActions}>
<button
@ -347,7 +347,7 @@ export const AdminMandateRolesPage: React.FC = () => {
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">{t('adminMandateRoles.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>
{getMandateName(m)}
@ -357,7 +357,7 @@ export const AdminMandateRolesPage: React.FC = () => {
</div>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('adminMandateRoles.filter')}</label>
<label className={styles.filterLabel}>{t('Filter')}</label>
<select
className={styles.filterSelect}
value={scopeFilter}
@ -365,8 +365,8 @@ export const AdminMandateRolesPage: React.FC = () => {
style={{ minWidth: 150 }}
>
<option value="mandate">Mandanten-Rollen</option>
<option value="all">{t('adminMandateRoles.alleInklTemplates')}</option>
<option value="global">{t('adminMandateRoles.nurTemplates')}</option>
<option value="all">{t('Alle inkl. Templates')}</option>
<option value="global">{t('Nur Templates')}</option>
</select>
</div>
@ -405,7 +405,7 @@ export const AdminMandateRolesPage: React.FC = () => {
{!selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminMandateRoles.keinMandantAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Mandanten aus, um dessen Rollen zu verwalten.
</p>
@ -427,12 +427,12 @@ export const AdminMandateRolesPage: React.FC = () => {
{
type: 'edit' as const,
onAction: handleEditClick,
title: t('adminMandateRolesPage.editRole'),
title: t('Rolle bearbeiten'),
disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht bearbeitet werden' } : false
},
{
type: 'delete' as const,
title: t('adminMandateRolesPage.deleteRole'),
title: t('Rolle löschen'),
disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht gelöscht werden' } : false
}
]}
@ -442,7 +442,7 @@ export const AdminMandateRolesPage: React.FC = () => {
pagination: pagination,
handleDelete: handleDeleteRole,
}}
emptyMessage={t('adminMandateRoles.keineRollenGefunden')}
emptyMessage={t('Keine Rollen gefunden')}
/>
</div>
)}
@ -452,7 +452,7 @@ export const AdminMandateRolesPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminMandateRoles.neueRolleErstellen')}</h2>
<h2 className={styles.modalTitle}>{t('Neue Rolle erstellen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -464,7 +464,7 @@ export const AdminMandateRolesPage: React.FC = () => {
{createFields.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminMandateRoles.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
@ -473,7 +473,7 @@ export const AdminMandateRolesPage: React.FC = () => {
onSubmit={handleCreateRole}
onCancel={() => setShowCreateModal(false)}
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'}
cancelButtonText={t('adminMandateRoles.abbrechen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -498,7 +498,7 @@ export const AdminMandateRolesPage: React.FC = () => {
{editFields.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminMandateRoles.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<>
@ -515,8 +515,8 @@ export const AdminMandateRolesPage: React.FC = () => {
mode="edit"
onSubmit={handleEditRole}
onCancel={() => setEditingRole(null)}
submitButtonText={isSubmitting ? t('adminMandateRoles.speichern') : t('adminMandateRoles.speichern')}
cancelButtonText={t('adminMandateRoles.abbrechen')}
submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</>
)}

View file

@ -124,7 +124,7 @@ export const AdminMandatesPage: React.FC = () => {
}
const entered = await prompt(
`Um den Mandanten "${mandate.name}" zu deaktivieren (Soft-Delete), geben Sie den Namen ein:`,
{ title: t('adminMandatesPage.deactivateMandate'), confirmLabel: t('adminMandatesPage.deactivate'), variant: 'danger', placeholder: mandate.name },
{ title: t('Mandat deaktivieren'), confirmLabel: t('Deaktivieren'), variant: 'danger', placeholder: mandate.name },
);
if (entered === null) return;
if (entered !== mandate.name) {
@ -141,7 +141,7 @@ export const AdminMandatesPage: React.FC = () => {
}
const entered = await prompt(
`ACHTUNG: Dies löscht den Mandanten "${mandate.name}" unwiderruflich inkl. aller Subscriptions, Features, Benutzer-Zuweisungen und Daten. Geben Sie den exakten Namen ein:`,
{ title: t('adminMandatesPage.hardDeleteIrreversible'), confirmLabel: t('adminMandatesPage.deletePermanently'), variant: 'danger', placeholder: mandate.name },
{ title: t('Unwiderrufliches Löschen'), confirmLabel: t('Dauerhaft löschen'), variant: 'danger', placeholder: mandate.name },
);
if (entered === null) return;
if (entered !== mandate.name) {
@ -173,7 +173,7 @@ export const AdminMandatesPage: React.FC = () => {
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Mandanten</h1>
<p className={styles.pageSubtitle}>{t('adminMandates.verwaltenSieAlleMandantenIm')}</p>
<p className={styles.pageSubtitle}>{t('Verwalten Sie alle Mandanten im')}</p>
</div>
<div className={styles.headerActions}>
<button
@ -217,11 +217,11 @@ export const AdminMandatesPage: React.FC = () => {
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('adminMandatesPage.edit'),
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('adminMandatesPage.deactivateSoftDelete'),
title: t('Soft-Löschen deaktivieren'),
disabled: (row: Mandate) => row.isSystem
? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' }
: false
@ -231,7 +231,7 @@ export const AdminMandatesPage: React.FC = () => {
id: 'hard-delete',
icon: <FaSkullCrossbones />,
onClick: handleHardDeleteMandate,
title: t('adminMandatesPage.hardDeleteIrreversible'),
title: t('Unwiderrufliches Löschen'),
disabled: (row: Mandate) => row.isSystem
? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' }
: false,
@ -245,7 +245,7 @@ export const AdminMandatesPage: React.FC = () => {
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('adminMandates.keineMandantenGefunden')}
emptyMessage={t('Keine Mandanten gefunden')}
/>
</div>
@ -254,7 +254,7 @@ export const AdminMandatesPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminMandates.neuerMandant')}</h2>
<h2 className={styles.modalTitle}>{t('Neuer Mandant')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -270,7 +270,7 @@ export const AdminMandatesPage: React.FC = () => {
{mandateAttrsLoading || createFormAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminMandates.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
@ -278,8 +278,8 @@ export const AdminMandatesPage: React.FC = () => {
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText={t('adminMandates.erstellen')}
cancelButtonText={t('adminMandates.abbrechen')}
submitButtonText={t('Erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -300,7 +300,7 @@ export const AdminMandatesPage: React.FC = () => {
>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminMandates.mandantBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Mandant bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => {
@ -331,7 +331,7 @@ export const AdminMandatesPage: React.FC = () => {
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminMandates.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
@ -343,8 +343,8 @@ export const AdminMandatesPage: React.FC = () => {
setEditingFormData(null);
setEditingBillingWarning(null);
}}
submitButtonText={t('adminMandates.speichern')}
cancelButtonText={t('adminMandates.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>

View file

@ -205,10 +205,10 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
</div>
)}
<h3 style={{ marginBottom: '1rem', color: 'var(--text-primary)' }}>{t('adminUserAccessOverview.zugriffNachMandant')}</h3>
<h3 style={{ marginBottom: '1rem', color: 'var(--text-primary)' }}>{t('Zugriff nach Mandant')}</h3>
{overview.mandates.length === 0 ? (
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineMandatezuordnungenVorhanden')}</p>
<p className={styles.emptyHint}>{t('Keine Mandatszuordnungen vorhanden')}</p>
) : (
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
{overview.mandates.map((mandate) => {
@ -232,7 +232,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{expandedMandates.has(mandate.id) && (
<div className={styles.roleContent}>
{mandateRoles.length === 0 ? (
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineRollenDirektAmMandanten')}</p>
<p className={styles.emptyHint}>{t('Keine Rollen direkt am Mandanten')}</p>
) : (
<ul className={styles.accessOverviewRoleBullets}>
{mandateRoles.map((r) => (
@ -257,7 +257,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<div className={styles.accessOverviewSubheading}>Feature-Instanzen</div>
{mandate.featureInstances.length === 0 ? (
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineFeatureinstanzenZugewiesen')}</p>
<p className={styles.emptyHint}>{t('Keine Feature-Instanzen zugewiesen')}</p>
) : (
<div className={styles.accessOverviewInstanceStack}>
{mandate.featureInstances.map((instance) => {
@ -338,7 +338,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<div className={styles.roleContent}>
<div style={{ fontSize: '0.875rem' }}>
<p>
<strong>{t('adminUserAccessOverview.beschreibung')}</strong> {_roleDescriptionLine(role) || '—'}
<strong>{t('Beschreibung')}</strong> {_roleDescriptionLine(role) || '—'}
</p>
</div>
</div>
@ -359,13 +359,13 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<div className={styles.scrollableContent}>
<div className={styles.infoBox}>
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
<span>{t('adminUserAccessOverview.uizugriffsrechteBestimmenWelcheSeitenUnd')}</span>
<span>{t('UI-Zugriffsrechte bestimmen, welche Seiten und')}</span>
</div>
{overview.uiAccess.length === 0 ? (
<div className={styles.emptyState}>
<FaEye className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineUiberechtigungen')}</h3>
<h3 className={styles.emptyTitle}>{t('Keine UI-Berechtigungen')}</h3>
<p className={styles.emptyDescription}>
Diesem Benutzer wurden keine expliziten UI-Berechtigungen zugewiesen.
</p>
@ -376,7 +376,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>UI-Element</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Sichtbar</th>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('Gewährt durch')}</th>
</tr>
</thead>
<tbody>
@ -426,7 +426,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{overview.dataAccess.length === 0 ? (
<div className={styles.emptyState}>
<FaDatabase className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineDatenberechtigungen')}</h3>
<h3 className={styles.emptyTitle}>{t('Keine Datenberechtigungen')}</h3>
<p className={styles.emptyDescription}>
Diesem Benutzer wurden keine expliziten Daten-Berechtigungen zugewiesen.
</p>
@ -437,10 +437,10 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Tabelle/Feld</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Lesen</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('adminUserAccessOverview.erstellen')}</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('Erstellen')}</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Update</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('adminUserAccessOverview.loeschen')}</th>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('schen')}</th>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('Gewährt durch')}</th>
</tr>
</thead>
<tbody>
@ -522,13 +522,13 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<div className={styles.scrollableContent}>
<div className={styles.infoBox}>
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
<span>{t('adminUserAccessOverview.ressourcenzugriffsrechteBestimmenWelcheSystemressourcenZb')}</span>
<span>{t('Ressourcenzugriffsrechte bestimmen, welche Systemressourcen z.B.')}</span>
</div>
{overview.resourceAccess.length === 0 ? (
<div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineRessourcenberechtigungen')}</h3>
<h3 className={styles.emptyTitle}>{t('Keine Ressourcenberechtigungen')}</h3>
<p className={styles.emptyDescription}>
Diesem Benutzer wurden keine expliziten Ressourcen-Berechtigungen zugewiesen.
</p>
@ -539,7 +539,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Ressource</th>
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Zugriff</th>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('Gewährt durch')}</th>
</tr>
</thead>
<tbody>
@ -594,8 +594,8 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminUserAccessOverview.benutzerzugriffsuebersicht')}</h1>
<p className={styles.pageSubtitle}>{t('adminUserAccessOverview.zeigtAlleBerechtigungenEinesBenutzers')}</p>
<h1 className={styles.pageTitle}>{t('Benutzerzugriffsübersicht')}</h1>
<p className={styles.pageSubtitle}>{t('Zeigt alle Berechtigungen eines Benutzers')}</p>
</div>
</div>
@ -613,7 +613,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
disabled={loadingUsers}
style={{ minWidth: '300px' }}
>
<option value="">{t('adminUserAccessOverview.benutzerWaehlen')}</option>
<option value="">{t('Benutzer wählen')}</option>
{users.map(user => (
<option key={user.id} value={user.id}>
{user.fullName || user.username} ({user.email})
@ -638,7 +638,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
{!selectedUserId ? (
<div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.benutzerAuswaehlen')}</h3>
<h3 className={styles.emptyTitle}>{t('Benutzer auswählen')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Benutzer aus, um dessen Zugriffsberechtigungen anzuzeigen.
</p>
@ -646,7 +646,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
) : loading ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminUserAccessOverview.ladeZugriffsuebersicht')}</span>
<span>{t('Lade Zugriffsübersicht')}</span>
</div>
) : overview ? (
<>

View file

@ -101,7 +101,7 @@ export const AdminUserMandatesPage: React.FC = () => {
return [
{
key: 'username',
label: t('adminUserMandates.benutzername'),
label: t('Benutzername'),
type: 'text' as any,
sortable: true,
filterable: true,
@ -110,7 +110,7 @@ export const AdminUserMandatesPage: React.FC = () => {
},
{
key: 'email',
label: t('adminUserMandates.email'),
label: t('E-Mail'),
type: 'text' as any,
sortable: true,
filterable: true,
@ -119,7 +119,7 @@ export const AdminUserMandatesPage: React.FC = () => {
},
{
key: 'fullName',
label: t('adminUserMandates.vollstaendigerName'),
label: t('Vollständiger Name'),
type: 'text' as any,
sortable: true,
filterable: true,
@ -128,7 +128,7 @@ export const AdminUserMandatesPage: React.FC = () => {
},
{
key: 'roleLabels',
label: t('adminUserMandates.rollen'),
label: t('Rollen'),
type: 'text' as any,
sortable: false,
filterable: false,
@ -141,7 +141,7 @@ export const AdminUserMandatesPage: React.FC = () => {
},
{
key: 'enabled',
label: t('adminUserMandates.aktiv'),
label: t('Aktiv'),
type: 'boolean' as any,
sortable: true,
filterable: true,
@ -278,7 +278,7 @@ export const AdminUserMandatesPage: React.FC = () => {
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Mandanten-Mitglieder</h1>
<p className={styles.pageSubtitle}>{t('adminUserMandates.verwaltenSieWelcheBenutzerZugriff')}</p>
<p className={styles.pageSubtitle}>{t('Verwalten Sie, welche Benutzer Zugriff')}</p>
</div>
</div>
@ -294,7 +294,7 @@ export const AdminUserMandatesPage: React.FC = () => {
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">{t('adminUserMandates.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>
{getMandateName(m)}
@ -327,7 +327,7 @@ export const AdminUserMandatesPage: React.FC = () => {
{!selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('adminUserMandates.keinMandantAusgewaehlt')}</h3>
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Mandanten aus, um dessen Mitglieder zu verwalten.
</p>
@ -349,11 +349,11 @@ export const AdminUserMandatesPage: React.FC = () => {
{
type: 'edit' as const,
onAction: handleEditClick,
title: t('adminUserMandatesPage.editRoles'),
title: t('Rollen bearbeiten'),
},
{
type: 'delete' as const,
title: t('adminUserMandatesPage.removeFromMandate'),
title: t('Vom Mandat entfernen'),
}
]}
onDelete={handleRemoveUser}
@ -370,7 +370,7 @@ export const AdminUserMandatesPage: React.FC = () => {
return false;
},
}}
emptyMessage={t('adminUserMandates.keineMitgliederGefunden')}
emptyMessage={t('Keine Mitglieder gefunden')}
/>
</div>
)}
@ -380,7 +380,7 @@ export const AdminUserMandatesPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminUserMandates.benutzerZumMandantenHinzufuegen')}</h2>
<h2 className={styles.modalTitle}>{t('Benutzer zum Mandanten hinzufügen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowAddModal(false)}
@ -390,11 +390,11 @@ export const AdminUserMandatesPage: React.FC = () => {
</div>
<div className={styles.modalContent}>
{availableUsers.length === 0 ? (
<p>{t('adminUserMandates.alleBenutzerSindBereitsDiesem')}</p>
<p>{t('Alle Benutzer sind bereits diesem')}</p>
) : roleOptions.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminUserMandates.ladeRollen')}</span>
<span>{t('Lade Rollen')}</span>
</div>
) : (
<FormGeneratorForm
@ -402,8 +402,8 @@ export const AdminUserMandatesPage: React.FC = () => {
mode="create"
onSubmit={handleAddUser}
onCancel={() => setShowAddModal(false)}
submitButtonText={isSubmitting ? t('adminUserMandates.hinzufuegen') : t('adminUserMandates.hinzufuegen')}
cancelButtonText={t('adminUserMandates.abbrechen')}
submitButtonText={isSubmitting ? t('Hinzufügen') : t('Hinzufügen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -431,8 +431,8 @@ export const AdminUserMandatesPage: React.FC = () => {
mode="edit"
onSubmit={handleEditRoles}
onCancel={() => setEditingUser(null)}
submitButtonText={isSubmitting ? t('adminUserMandates.speichern') : t('adminUserMandates.speichern')}
cancelButtonText={t('adminUserMandates.abbrechen')}
submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>

View file

@ -145,8 +145,8 @@ export const AdminUsersPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('adminUsers.benutzer')}</h1>
<p className={styles.pageSubtitle}>{t('adminUsers.verwaltenSieAlleBenutzerIm')}</p>
<h1 className={styles.pageTitle}>{t('Benutzer')}</h1>
<p className={styles.pageSubtitle}>{t('Verwalten Sie alle Benutzer im')}</p>
</div>
<div className={styles.headerActions}>
<button
@ -197,11 +197,11 @@ export const AdminUsersPage: React.FC = () => {
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('adminUsersPage.edit'),
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('adminUsersPage.delete'),
title: t('Löschen'),
}] : []),
]}
customActions={canUpdate ? [
@ -209,7 +209,7 @@ export const AdminUsersPage: React.FC = () => {
id: 'sendPasswordLink',
icon: <FaKey />,
onClick: handleSendPassword,
title: t('adminUsersPage.sendPasswordLink'),
title: t('Passwort-Link senden'),
loading: (row: User) => sendingPasswordLinkState.has(row.id),
}
] : []}
@ -222,7 +222,7 @@ export const AdminUsersPage: React.FC = () => {
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('adminUsers.keineBenutzerGefunden')}
emptyMessage={t('Keine Benutzer gefunden')}
/>
</div>
@ -231,7 +231,7 @@ export const AdminUsersPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminUsers.neuerBenutzer')}</h2>
<h2 className={styles.modalTitle}>{t('Neuer Benutzer')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -243,7 +243,7 @@ export const AdminUsersPage: React.FC = () => {
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminUsers.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
@ -251,8 +251,8 @@ export const AdminUsersPage: React.FC = () => {
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText={t('adminUsers.erstellen')}
cancelButtonText={t('adminUsers.abbrechen')}
submitButtonText={t('Erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -265,7 +265,7 @@ export const AdminUsersPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('adminUsers.benutzerBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Benutzer bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingUser(null)}
@ -277,7 +277,7 @@ export const AdminUsersPage: React.FC = () => {
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('adminUsers.ladeFormular')}</span>
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
@ -286,8 +286,8 @@ export const AdminUsersPage: React.FC = () => {
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingUser(null)}
submitButtonText={t('adminUsers.speichern')}
cancelButtonText={t('adminUsers.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>

View file

@ -56,7 +56,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
}, []);
const availableConnectors = [
{ id: 'preprocessor', label: t('chatbotConfigSection.althausPreprocessor'), value: 'preprocessor' }
{ id: 'preprocessor', label: t('Althaus Preprocessor'), value: 'preprocessor' }
];
const handleConnectorToggle = (connectorValue: string) => {
@ -112,7 +112,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
onChange={(e) => onEnableWebResearchChange(e.target.checked)}
className={styles.multiselectCheckbox}
/>
<span>{t('chatbotConfigSection.webResearchAktivierenTavily')}</span>
<span>{t('Web Research aktivieren (Tavily)')}</span>
</label>
<p className={styles.configHelpText}>
Wenn aktiviert, führt der Chatbot zusätzlich Web-Recherchen mit Tavily durch, um aktuelle Informationen aus dem Internet zu finden.
@ -125,7 +125,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
</label>
<div className={styles.multiselectContainer}>
{providersLoading ? (
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>{t('chatbotConfigSection.ladeAnbieter')}</span>
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>{t('Anbieter laden')}</span>
) : (
availableProviders.map(provider => (
<label key={provider} className={styles.multiselectOption}>
@ -157,7 +157,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
type="text"
value={systemPrompt}
onChange={onSystemPromptChange}
placeholder={t('chatbotConfigSection.benutzerdefinierterSystempromptFuerDenChatbot')}
placeholder={t('Benutzerdefinierter Systemprompt für den Chatbot')}
className={styles.configTextArea}
size="md"
rows={6}

View file

@ -184,7 +184,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
() => [
{
name: 'userId',
label: t('instanceDetailModal.benutzer'),
label: t('Benutzer'),
type: 'enum' as const,
required: true,
options: availableUsers.map((u) => ({
@ -194,7 +194,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
},
{
name: 'roleIds',
label: t('instanceDetailModal.rollen'),
label: t('Rollen'),
type: 'multiselect' as const,
required: true,
options: roleOptions as AttributeDefinition['options'],
@ -207,14 +207,14 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
() => [
{
name: 'roleIds',
label: t('instanceDetailModal.rollen'),
label: t('Rollen'),
type: 'multiselect' as const,
required: true,
options: roleOptions as AttributeDefinition['options'],
},
{
name: 'enabled',
label: t('instanceDetailModal.aktiv'),
label: t('aktiv'),
type: 'checkbox' as const,
required: false,
},
@ -225,13 +225,13 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
const tabs = [
{
id: 'users',
label: t('instanceDetailModal.benutzer'),
label: t('Benutzer'),
content: (
<div className={modalStyles.tabContent}>
{loading ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('instanceDetailModal.ladeBenutzer')}</span>
<span>{t('lade Benutzer')}</span>
</div>
) : (
<PermissionMatrix
@ -247,7 +247,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
},
{
id: 'roles',
label: t('instanceDetailModal.rollen'),
label: t('Rollen'),
content: (
<div className={modalStyles.tabContent}>
<p className={modalStyles.rolesIntro}>
@ -271,19 +271,19 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
},
{
id: 'settings',
label: t('instanceDetailModal.einstellungen'),
label: t('Einstellungen'),
content: (
<div className={modalStyles.tabContent}>
<FormGeneratorForm
attributes={[
{ name: 'label', type: 'string' as const, label: t('instanceDetailModal.bezeichnung'), required: true, editable: true },
{ name: 'enabled', type: 'boolean' as const, label: t('instanceDetailModal.aktiviert'), required: false, editable: true },
{ name: 'label', type: 'string' as const, label: t('Bezeichnung'), required: true, editable: true },
{ name: 'enabled', type: 'boolean' as const, label: t('aktiviert'), required: false, editable: true },
]}
data={instance}
mode="edit"
onSubmit={handleUpdateInstance}
submitButtonText={t('instanceDetailModal.speichern')}
cancelButtonText={t('instanceDetailModal.abbrechen')}
submitButtonText={t('speichern')}
cancelButtonText={t('abbrechen')}
onCancel={() => {}}
/>
</div>
@ -301,7 +301,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
{mandateName} · {featureLabel}
</p>
</div>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('instanceDetailModal.schliessen')}>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('schließen')}>
</button>
</div>
@ -314,24 +314,24 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('instanceDetailModal.benutzerHinzufuegen')}</h2>
<h2 className={styles.modalTitle}>{t('Benutzer hinzufügen')}</h2>
<button type="button" className={styles.modalClose} onClick={() => setShowAddModal(false)}>
</button>
</div>
<div className={styles.modalContent}>
{availableUsers.length === 0 ? (
<p>{t('instanceDetailModal.alleMandantenbenutzerHabenBereitsZugriff')}</p>
<p>{t('Alle Mandantenbenutzer haben bereits Zugriff')}</p>
) : addUserFields.length < 2 || !roleOptions?.length ? (
<p>{t('instanceDetailModal.laden')}</p>
<p>{t('laden')}</p>
) : (
<FormGeneratorForm
attributes={addUserFields}
mode="create"
onSubmit={handleAddUser}
onCancel={() => setShowAddModal(false)}
submitButtonText={t('instanceDetailModal.hinzufuegen')}
cancelButtonText={t('instanceDetailModal.abbrechen')}
submitButtonText={t('hinzufügen')}
cancelButtonText={t('abbrechen')}
/>
)}
</div>
@ -355,8 +355,8 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instan
mode="edit"
onSubmit={handleUpdateRoles}
onCancel={() => setEditingUser(null)}
submitButtonText={t('instanceDetailModal.speichern')}
cancelButtonText={t('instanceDetailModal.abbrechen')}
submitButtonText={t('speichern')}
cancelButtonText={t('abbrechen')}
/>
</div>
</div>

View file

@ -115,7 +115,7 @@ function MandateContent({
<span
className={`${hubStyles.instanceBadge} ${inst.enabled ? hubStyles.badgeActive : hubStyles.badgeInactive}`}
>
{inst.enabled ? t('instanceHierarchy.aktiv') : t('instanceHierarchy.inaktiv')}
{inst.enabled ? t('aktiv') : t('inaktiv')}
</span>
<span className={hierarchyStyles.instanceUserCount}>
<FaUsers /> {users.length}
@ -128,7 +128,7 @@ function MandateContent({
e.stopPropagation();
onOpenDetail(inst, mandateId);
}}
title={t('instanceHierarchy.benutzerVerwalten')}
title={t('Benutzer verwalten')}
>
<FaUsers /> Benutzer verwalten
</button>
@ -189,7 +189,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
<section className={hubStyles.section}>
<div className={hierarchyStyles.hierarchyLoading}>
<span className={hierarchyStyles.spinner} />
<span>{t('instanceHierarchy.ladeHierarchieUndBenutzer')}</span>
<span>{t('lade Hierarchie und Benutzer')}</span>
</div>
</section>
);
@ -198,7 +198,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
if (mandates.length === 0) {
return (
<section className={hubStyles.section}>
<h2 className={hubStyles.sectionTitle}>{t('instanceHierarchy.hierarchie')}</h2>
<h2 className={hubStyles.sectionTitle}>{t('Hierarchie')}</h2>
<div className={hierarchyStyles.emptyHierarchy}>
Keine Mandanten vorhanden. Legen Sie unter &quot;Mandanten verwalten&quot; einen Mandanten an.
</div>
@ -208,7 +208,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
return (
<section className={hubStyles.section}>
<h2 className={hubStyles.sectionTitle}>{t('instanceHierarchy.hierarchie')}</h2>
<h2 className={hubStyles.sectionTitle}>{t('Hierarchie')}</h2>
<div className={hierarchyStyles.hierarchyRoot}>
{mandates.map((mandate) => {
const mandateId = mandate.id;
@ -268,7 +268,7 @@ function UserRow({ user }: UserRowProps) {
user.roleLabels && user.roleLabels.length > 0
? user.roleLabels.join(', ')
: 'Keine Rollen';
const statusText = user.enabled ? t('instanceHierarchy.aktiv') : t('instanceHierarchy.inaktiv');
const statusText = user.enabled ? t('aktiv') : t('inaktiv');
return (
<div className={hierarchyStyles.userRowWrapper}>

View file

@ -37,7 +37,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
const handleRemove = useCallback(async (user: FeatureAccessUser) => {
if (removingId) return;
const ok = await confirm(`"${user.username}" aus dieser Instanz entfernen?`, {
title: t('permissionMatrix.removeUser'),
title: t('Benutzer entfernen'),
confirmLabel: 'Entfernen',
variant: 'danger',
});
@ -50,7 +50,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
if (roles.length === 0) {
return (
<div className={matrixStyles.empty}>
<p>{t('permissionMatrix.keineRollenInDieserInstanz')}</p>
<p>{t('Keine Rollen in dieser Instanz')}</p>
</div>
);
}
@ -61,13 +61,13 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
<table className={matrixStyles.table}>
<thead>
<tr>
<th className={matrixStyles.cellUser}>{t('permissionMatrix.benutzer')}</th>
<th className={matrixStyles.cellUser}>{t('Benutzer')}</th>
{roles.map((r) => (
<th key={r.id} className={matrixStyles.cellRole}>
{r.roleLabel}
</th>
))}
<th className={matrixStyles.cellActive}>{t('permissionMatrix.aktiv')}</th>
<th className={matrixStyles.cellActive}>{t('Aktiv')}</th>
<th className={matrixStyles.cellActions}>Aktionen</th>
</tr>
</thead>
@ -113,7 +113,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
className={matrixStyles.actionBtn}
onClick={() => onEditUser(user)}
disabled={disabled}
title={t('permissionMatrix.rollenBearbeiten')}
title={t('Rollen bearbeiten')}
>
<FaEdit />
</button>
@ -122,7 +122,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
className={`${matrixStyles.actionBtn} ${matrixStyles.actionBtnDanger}`}
onClick={() => handleRemove(user)}
disabled={disabled || removingId === user.userId}
title={t('permissionMatrix.ausInstanzEntfernen')}
title={t('Aus Instanz entfernen')}
>
<FaTrash />
</button>

View file

@ -173,17 +173,17 @@ export const AdminInvitationWizardPage: React.FC = () => {
const email = inviteeForm.email.trim();
const username = inviteeForm.username.trim();
if (!email && !username) {
setError(t('adminInvitationWizard.bitteMindestensEineEmailadresseOder'));
setError(t('Bitte mindestens eine E-Mail-Adresse oder'));
return;
}
const emailLower = email.toLowerCase();
const userLower = username.toLowerCase();
if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) {
setError(t('adminInvitationWizard.dieseEmailIstBereitsIn'));
setError(t('Diese E-Mail ist bereits in'));
return;
}
if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) {
setError(t('adminInvitationWizard.dieserBenutzernameIstBereitsIn'));
setError(t('Dieser Benutzername ist bereits in'));
return;
}
setInvitees(prev => [...prev, {
@ -198,14 +198,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
const addInviteeExisting = () => {
if (!selectedExistingUserId) {
setError(t('adminInvitationWizard.bitteWaehlenSieEinenBenutzer'));
setError(t('Bitte wählen Sie einen Benutzer'));
return;
}
const user = allSystemUsers.find(u => u.id === selectedExistingUserId);
if (!user) return;
const email = (user.email || '').trim();
if (invitees.some(i => i.userId === user.id)) {
setError(t('adminInvitationWizard.dieserBenutzerIstBereitsIn'));
setError(t('Dieser Benutzer ist bereits in'));
return;
}
setInvitees(prev => [...prev, {
@ -235,7 +235,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
const handleSend = async () => {
if (!selectedMandate || invitees.length === 0) return;
if (inviteType === 'featureInstance' && !selectedInstance) {
setError(t('adminInvitationWizard.bitteWaehlenSieEineFeatureinstanz'));
setError(t('Bitte wählen Sie eine Feature-Instanz'));
return;
}
setIsLoading(true);
@ -357,7 +357,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{/* ── STEP 1: Invite type ── */}
{step === 1 && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('adminInvitationWizard.wohinMoechtenSieEinladen')}</h3>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('Wohin möchten Sie einladen?')}</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<button
onClick={() => setInviteType('mandate')}
@ -367,7 +367,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
cursor: 'pointer', textAlign: 'left',
}}
>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>{t('adminInvitationWizard.zumMandanten')}</div>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>{t('Zum Mandanten')}</div>
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
Einladung zum Mandanten ohne spezifische Feature-Instanz
</div>
@ -380,7 +380,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
cursor: 'pointer', textAlign: 'left',
}}
>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>{t('adminInvitationWizard.zurFeatureinstanz')}</div>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>{t('Zur Feature-Instanz')}</div>
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
Einladung zu einer bestimmten Feature-Instanz mit Rolle
</div>
@ -398,10 +398,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
{step === 2 && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>
{inviteType === 'mandate' ? t('adminInvitationWizard.mandantAuswaehlen') : t('adminInvitationWizard.mandantUndFeatureinstanzAuswaehlen')}
{inviteType === 'mandate' ? t('Mandant auswählen') : t('Mandant und Feature-Instanz auswählen')}
</h3>
<div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>{t('adminInvitationWizard.mandant')}</label>
<label className={styles.formLabel}>{t('Mandant')}</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
@ -412,7 +412,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
setSelectedInstance(null);
}}
>
<option value="">{t('adminInvitationWizard.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>{getMandateName(m)}</option>
))}
@ -420,9 +420,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
</div>
{inviteType === 'featureInstance' && selectedMandate && (
<div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>{t('adminInvitationWizard.featureinstanz')}</label>
<label className={styles.formLabel}>{t('Feature-Instanz')}</label>
{instances.length === 0 ? (
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{t('adminInvitationWizard.keineFeatureinstanzenFuerDiesenMandanten')}</p>
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{t('Keine Feature-Instanzen für diesen Mandanten')}</p>
) : (
<select
className={styles.filterSelect}
@ -433,7 +433,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
setSelectedInstance(inst || null);
}}
>
<option value="">{t('adminInvitationWizard.featureinstanzWaehlen')}</option>
<option value="">{t('Feature-Instanz wählen')}</option>
{instances.map(inst => {
const baseLabel = inst.label || inst.featureCode;
const suffix = inst.enabled === false ? ' (deaktiviert)' : '';
@ -446,7 +446,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
</div>
)}
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(1)}>{t('adminInvitationWizard.larrZurueck')}</button>
<button className={styles.secondaryButton} onClick={() => setStep(1)}>{t('← Zurück')}</button>
<button
className={styles.primaryButton}
disabled={!canProceedStep3}
@ -462,7 +462,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{step === 3 && selectedMandate && (
<div>
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{t('adminInvitationWizard.einladungenHinzufuegen')}</h3>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{t('Einladungen hinzufügen')}</h3>
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', margin: '0 0 16px 0' }}>
Für neue Benutzer: mindestens eine E-Mail <em>oder</em> ein Benutzername (vorgegeben). Ohne E-Mail wird kein Link per Mail versendet der Einladungslink kann manuell geteilt werden. Bestehende Benutzer wählen Sie im zweiten Tab.
</p>
@ -488,7 +488,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{addMode === 'email' ? (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div>
<label className={styles.formLabel}>{t('adminInvitationWizard.emailOptional')}</label>
<label className={styles.formLabel}>{t('E-Mail (optional)')}</label>
<input
className={styles.formInput}
type="email"
@ -498,14 +498,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
/>
</div>
<div>
<label className={styles.formLabel}>{t('adminInvitationWizard.benutzernameOptional')}</label>
<label className={styles.formLabel}>{t('Benutzername (optional)')}</label>
<input
className={styles.formInput}
type="text"
autoComplete="off"
value={inviteeForm.username}
onChange={e => setInviteeForm(p => ({ ...p, username: e.target.value }))}
placeholder={t('adminInvitationWizard.zBVornamenachname')}
placeholder={t('z.B. Vorname Nachname')}
/>
<p style={{ fontSize: '11px', color: 'var(--text-secondary)', marginTop: '4px' }}>
Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden.
@ -553,25 +553,25 @@ export const AdminInvitationWizardPage: React.FC = () => {
) : (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div>
<label className={styles.formLabel}>{t('adminInvitationWizard.bestehenderBenutzer')}</label>
<label className={styles.formLabel}>{t('Bestehender Benutzer')}</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={selectedExistingUserId}
onChange={e => setSelectedExistingUserId(e.target.value)}
>
<option value="">{t('adminInvitationWizard.benutzerWaehlen')}</option>
<option value="">{t('Benutzer wählen')}</option>
{availableExistingUsers.map(u => (
<option key={u.id} value={u.id}>
{u.username} {u.email ? `(${u.email})` : ''}
</option>
))}
</select>
{availableExistingUsers.length === 0 && <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '4px' }}>{t('adminInvitationWizard.keineWeiterenBenutzerVerfuegbar')}</p>}
{availableExistingUsers.length === 0 && <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '4px' }}>{t('Keine weiteren Benutzer verfügbar')}</p>}
</div>
{roles.length > 0 && (
<div>
<label className={styles.formLabel}>{t('adminInvitationWizard.rolle')}</label>
<label className={styles.formLabel}>{t('Rolle')}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{roles.map(r => (
<label key={r.id} className={styles.checkboxLabel} style={{
@ -612,9 +612,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailBenutzer')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('E-Mail Benutzer')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Benutzername</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.rollen')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('Rollen')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Typ</th>
<th style={{ textAlign: 'right', padding: '8px' }}>Aktion</th>
</tr>
@ -645,12 +645,12 @@ export const AdminInvitationWizardPage: React.FC = () => {
</tbody>
</table>
) : (
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', marginBottom: '16px' }}>{t('adminInvitationWizard.nochKeineEinladungenHinzugefuegt')}</p>
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', marginBottom: '16px' }}>{t('Noch keine Einladungen hinzugefügt')}</p>
)}
</div>
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(2)}>{t('adminInvitationWizard.larrZurueck')}</button>
<button className={styles.secondaryButton} onClick={() => setStep(2)}>{t('← Zurück')}</button>
<button
className={styles.primaryButton}
disabled={invitees.length === 0}
@ -665,12 +665,12 @@ export const AdminInvitationWizardPage: React.FC = () => {
{/* ── STEP 4: Summary and send ── */}
{step === 4 && selectedMandate && !dispatchResults && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('adminInvitationWizard.zusammenfassungVersand')}</h3>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('Zusammenfassung Versand')}</h3>
<div style={{ marginBottom: '16px' }}>
<strong>Art:</strong> {inviteType === 'mandate' ? t('adminInvitationWizard.einladungZumMandanten') : t('adminInvitationWizard.einladungZurFeatureinstanz')}
<strong>Art:</strong> {inviteType === 'mandate' ? t('Einladung zum Mandanten') : t('Einladung zur Feature-Instanz')}
</div>
<div style={{ marginBottom: '16px' }}>
<strong>{t('adminInvitationWizard.mandant')}</strong> {getMandateName(selectedMandate)}
<strong>{t('Mandant')}</strong> {getMandateName(selectedMandate)}
</div>
{inviteType === 'featureInstance' && selectedInstance && (
<div style={{ marginBottom: '16px' }}>
@ -690,7 +690,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
</ul>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
<button className={styles.secondaryButton} onClick={() => setStep(3)}>{t('adminInvitationWizard.larrZurueck')}</button>
<button className={styles.secondaryButton} onClick={() => setStep(3)}>{t('← Zurück')}</button>
<button
className={styles.primaryButton}
disabled={isLoading}
@ -705,13 +705,13 @@ export const AdminInvitationWizardPage: React.FC = () => {
{/* ── Results ── */}
{dispatchResults && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('adminInvitationWizard.ergebnis')}</h3>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('Ergebnis')}</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailBenutzer')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.status')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailGesendet')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('E-Mail Benutzer')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('Status')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('E-Mail gesendet')}</th>
</tr>
</thead>
<tbody>

View file

@ -136,7 +136,7 @@ export const AdminMandateWizardPage: React.FC = () => {
const data = await fetchMandatesList();
setMandates(data);
} catch {
setError(t('adminMandateWizard.fehlerBeimLadenDerMandanten'));
setError(t('Fehler beim Laden der Mandanten'));
}
}, [fetchMandatesList]);
@ -459,10 +459,10 @@ export const AdminMandateWizardPage: React.FC = () => {
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: 'var(--bg-secondary, #f8fafc)' }}>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>{t('adminMandateWizard.benutzer')}</th>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>{t('Benutzer')}</th>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>E-Mail</th>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>{t('adminMandateWizard.rollen')}</th>
<th style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px', fontWeight: 600 }}>{t('adminMandateWizard.status')}</th>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>{t('Rollen')}</th>
<th style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px', fontWeight: 600 }}>{t('Status')}</th>
<th style={{ padding: '8px 12px', textAlign: 'right', fontSize: '12px', fontWeight: 600 }}>Aktion</th>
</tr>
</thead>
@ -499,7 +499,7 @@ export const AdminMandateWizardPage: React.FC = () => {
background: u.enabled !== false ? '#dcfce7' : 'var(--bg-secondary)',
color: u.enabled !== false ? '#166534' : 'var(--text-secondary)',
}}>
{u.enabled !== false ? t('adminMandateWizard.aktiv') : t('adminMandateWizard.inaktiv')}
{u.enabled !== false ? t('Aktiv') : t('Inaktiv')}
</span>
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
@ -535,7 +535,7 @@ export const AdminMandateWizardPage: React.FC = () => {
) => (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div>
<label className={styles.formLabel}>{t('adminMandateWizard.benutzerMehrfachMoeglich')}</label>
<label className={styles.formLabel}>{t('Benutzer mehrfach möglich')}</label>
<div
style={{
maxHeight: '220px',
@ -576,7 +576,7 @@ export const AdminMandateWizardPage: React.FC = () => {
</div>
{roles.length > 0 && (
<div>
<label className={styles.formLabel}>{t('adminMandateWizard.rollenFuerAlleAusgewaehltenBenutzer')}</label>
<label className={styles.formLabel}>{t('Rollen für alle ausgewählten Benutzer')}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{roles.map(r => (
<label key={r.id} className={styles.checkboxLabel}>
@ -604,7 +604,7 @@ export const AdminMandateWizardPage: React.FC = () => {
onClick={onSubmit}
disabled={isLoading || formValue.userIds.length === 0}
>
{isLoading ? t('adminMandateWizard.hinzufuegen') : t('adminMandateWizard.hinzufuegen')}
{isLoading ? t('Hinzufügen') : t('Hinzufügen')}
</button>
<button className={styles.secondaryButton} onClick={onCancel}>
Abbrechen
@ -671,7 +671,7 @@ export const AdminMandateWizardPage: React.FC = () => {
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Mandanten-Verwaltung</h1>
<p className={styles.pageSubtitle}>{t('adminMandateWizard.schrittfuerschrittWizardZurMandantenkonfiguration')}</p>
<p className={styles.pageSubtitle}>{t('Schritt-für-Schritt-Wizard zur Mandantenkonfiguration')}</p>
</div>
</div>
@ -691,7 +691,7 @@ export const AdminMandateWizardPage: React.FC = () => {
{/* ── STEP 1: MANDATE ── */}
{step === 1 && (
<div style={cardStyle}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>{t('adminMandateWizard.mandantAuswaehlenOderErstellen')}</h3>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>{t('Mandant auswählen oder erstellen')}</h3>
{!isCreatingMandate ? (
<>
@ -726,7 +726,7 @@ export const AdminMandateWizardPage: React.FC = () => {
{mandateAttrLoading || createFormAttributes.length === 0 ? (
<div className={styles.loadingContainer} style={{ padding: '24px' }}>
<div className={styles.spinner} />
<span>{t('adminMandateWizard.formularWirdGeladen')}</span>
<span>{t('Formular wird geladen')}</span>
</div>
) : (
<FormGeneratorForm
@ -734,8 +734,8 @@ export const AdminMandateWizardPage: React.FC = () => {
mode="create"
onSubmit={handleCreateMandate}
onCancel={() => setIsCreatingMandate(false)}
submitButtonText={isLoading ? t('adminMandateWizard.erstellen') : t('adminMandateWizard.mandantErstellen')}
cancelButtonText={t('adminMandateWizard.abbrechen')}
submitButtonText={isLoading ? t('Erstellen') : t('Mandant erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -788,7 +788,7 @@ export const AdminMandateWizardPage: React.FC = () => {
gap: '12px',
border: '1px solid var(--border-color, #e5e7eb)',
}}>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('adminMandateWizard.rollenBearbeiten')}</div>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('Rollen bearbeiten')}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{mandateRoles.map(r => (
<label key={r.id} className={styles.checkboxLabel}>
@ -836,7 +836,7 @@ export const AdminMandateWizardPage: React.FC = () => {
</div>
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(1)}>{t('adminMandateWizard.larrZurueck')}</button>
<button className={styles.secondaryButton} onClick={() => setStep(1)}>{t('← Zurück')}</button>
<button className={styles.primaryButton} onClick={() => setStep(3)}>
Weiter &rarr;
</button>
@ -858,13 +858,13 @@ export const AdminMandateWizardPage: React.FC = () => {
{/* Feature Filter */}
<div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>{t('adminMandateWizard.featureFiltern')}</label>
<label className={styles.formLabel}>{t('Feature filtern')}</label>
<select
className={styles.filterSelect}
value={selectedFeatureCode}
onChange={e => setSelectedFeatureCode(e.target.value)}
>
<option value="">{t('adminMandateWizard.alleFeatures')}</option>
<option value="">{t('Alle Features')}</option>
{features.map(f => (
<option key={f.code} value={f.code}>{getFeatureLabel(f.code)}</option>
))}
@ -882,7 +882,7 @@ export const AdminMandateWizardPage: React.FC = () => {
<div>
<span style={{ fontWeight: 600 }}>{inst.label}</span>
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', marginLeft: '8px' }}>
{getFeatureLabel(inst.featureCode)} | {inst.enabled ? t('adminMandateWizard.aktiv') : t('adminMandateWizard.inaktiv')}
{getFeatureLabel(inst.featureCode)} | {inst.enabled ? t('Aktiv') : t('Inaktiv')}
</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
@ -914,26 +914,26 @@ export const AdminMandateWizardPage: React.FC = () => {
) : (
<div style={{ display: 'grid', gap: '12px', padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px' }}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>{t('adminMandateWizard.feature')}</label>
<label className={styles.formLabel}>{t('Feature')}</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={selectedFeatureCode}
onChange={e => setSelectedFeatureCode(e.target.value)}
>
<option value="">{t('adminMandateWizard.featureWaehlen')}</option>
<option value="">{t('Feature wählen')}</option>
{features.map(f => (
<option key={f.code} value={f.code}>{getFeatureLabel(f.code)}</option>
))}
</select>
</div>
<div className={styles.formGroup}>
<label className={`${styles.formLabel} ${styles.required}`}>{t('adminMandateWizard.bezeichnung')}</label>
<label className={`${styles.formLabel} ${styles.required}`}>{t('Bezeichnung')}</label>
<input
className={styles.formInput}
value={instanceForm.label}
onChange={e => setInstanceForm(p => ({ ...p, label: e.target.value }))}
placeholder={t('adminMandateWizard.zbKundeA')}
placeholder={t('z.B. Kunde A')}
/>
</div>
<label className={styles.checkboxLabel}>
@ -946,15 +946,15 @@ export const AdminMandateWizardPage: React.FC = () => {
</label>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={styles.primaryButton} onClick={handleCreateInstance} disabled={isLoading || !selectedFeatureCode}>
{isLoading ? t('adminMandateWizard.erstellen') : t('adminMandateWizard.erstellen')}
{isLoading ? t('Erstellen') : t('Erstellen')}
</button>
<button className={styles.secondaryButton} onClick={() => setIsCreatingInstance(false)}>{t('adminMandateWizard.abbrechen')}</button>
<button className={styles.secondaryButton} onClick={() => setIsCreatingInstance(false)}>{t('Abbrechen')}</button>
</div>
</div>
)}
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(2)}>{t('adminMandateWizard.larrZurueck')}</button>
<button className={styles.secondaryButton} onClick={() => setStep(2)}>{t('← Zurück')}</button>
<button
className={styles.primaryButton}
onClick={() => { if (instances.length > 0) { setSelectedInstance(instances[0]); setStep(4); } }}
@ -1005,7 +1005,7 @@ export const AdminMandateWizardPage: React.FC = () => {
gap: '12px',
border: '1px solid var(--border-color, #e5e7eb)',
}}>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('adminMandateWizard.rollenBearbeitenFeatureinstanz')}</div>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('Rollen bearbeiten (Featureinstanz)')}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{instanceRoles.map(r => (
<label key={r.id} className={styles.checkboxLabel}>
@ -1058,7 +1058,7 @@ export const AdminMandateWizardPage: React.FC = () => {
</div>
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(3)}>{t('adminMandateWizard.larrZurueck')}</button>
<button className={styles.secondaryButton} onClick={() => setStep(3)}>{t('← Zurück')}</button>
<button className={styles.primaryButton} onClick={() => {
showSuccess('Fertig', 'Konfiguration abgeschlossen!');
setSelectedInstance(null);

View file

@ -69,10 +69,10 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
const createFields: AttributeDefinition[] = useMemo(
() => [
{ name: 'mandateId', label: t('featureInstanceWizard.mandant'), type: 'enum' as const, required: true, options: mandateOptions },
{ name: 'featureCode', label: t('featureInstanceWizard.feature'), type: 'enum' as const, required: true, options: featureOptions },
{ name: 'label', label: t('featureInstanceWizard.bezeichnung'), type: 'string' as const, required: true, editable: true },
{ name: 'enabled', label: t('featureInstanceWizard.aktiv'), type: 'boolean' as const, required: false, editable: true },
{ name: 'mandateId', label: t('Mandant'), type: 'enum' as const, required: true, options: mandateOptions },
{ name: 'featureCode', label: t('Feature'), type: 'enum' as const, required: true, options: featureOptions },
{ name: 'label', label: t('Bezeichnung'), type: 'string' as const, required: true, editable: true },
{ name: 'enabled', label: t('Aktiv'), type: 'boolean' as const, required: false, editable: true },
],
[mandateOptions, featureOptions]
);
@ -170,8 +170,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
<div className={styles.modalOverlay} onClick={onClose}>
<div className={`${styles.modal} ${wizardStyles.modal}`} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('featureInstanceWizard.neueFeatureinstanz')}</h2>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('featureInstanceWizard.schliessen')}>
<h2 className={styles.modalTitle}>{t('Neue Feature-Instanz')}</h2>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('Schließen')}>
</button>
</div>
@ -202,8 +202,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
}}
onSubmit={handleStep1Submit}
onCancel={onClose}
submitButtonText={t('featureInstanceWizard.weiter')}
cancelButtonText={t('featureInstanceWizard.abbrechen')}
submitButtonText={t('Weiter')}
cancelButtonText={t('Abbrechen')}
/>
<label className={wizardStyles.checkLabel}>
<input
@ -238,7 +238,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
Optional: Weisen Sie Benutzern Rollen zu. Sie können dies auch später in der Zugriffsverwaltung tun.
</p>
{mandateUsers.length === 0 ? (
<p className={wizardStyles.stepText}>{t('featureInstanceWizard.keineMandantenbenutzerVorhanden')}</p>
<p className={wizardStyles.stepText}>{t('Keine Mandantenbenutzer vorhanden')}</p>
) : (
<div className={wizardStyles.userList}>
{mandateUsers.map((u) => {
@ -256,7 +256,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
handleAddUserRole(u.id, rids);
}}
>
<option value="">{t('featureInstanceWizard.keineRolle')}</option>
<option value="">{t('Keine Rolle')}</option>
{instanceRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.roleLabel}

View file

@ -18,6 +18,7 @@ import { useLanguage } from '../../providers/language/LanguageContext';
/** Wenn false: keine neue ClickUp-Verbindung über diese Seite (Buttons inaktiv). */
const isClickupConnectionUiEnabled = false;
export const ConnectionsPage: React.FC = () => {
const { t } = useLanguage();
@ -74,16 +75,15 @@ export const ConnectionsPage: React.FC = () => {
fkDisplayField: (attr as any).fkDisplayField,
};
// Resolve userId to username via FK
if (attr.name === 'userId') {
col.fkSource = '/api/users/';
col.fkDisplayField = 'username';
col.label = 'User';
col.label = t('Benutzer');
}
return col;
});
}, [attributes]);
}, [attributes, t]);
// Check permissions
const canCreate = permissions?.create !== 'n';
@ -251,10 +251,11 @@ export const ConnectionsPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('connections.verbindungen')}</h1>
<h1 className={styles.pageTitle}>{t('Verbindungen')}</h1>
<p className={styles.pageSubtitle}>
Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft
{isClickupConnectionUiEnabled ? ', ClickUp' : ''})
{isClickupConnectionUiEnabled
? t('Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft, ClickUp)')
: t('Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft)')}
</p>
</div>
<div className={styles.headerActions}>
@ -262,16 +263,16 @@ export const ConnectionsPage: React.FC = () => {
className={styles.secondaryButton}
onClick={handleAdminConsent}
disabled={adminConsentPending}
title={t('connections.microsoftAdminConsentErteiltDer')}
title={t('Microsoft Admin-Zustimmung erteilt der')}
>
<FaShieldAlt /> Admin Consent
<FaShieldAlt /> {t('Admin-Zustimmung')}
</button>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<>
@ -295,7 +296,7 @@ export const ConnectionsPage: React.FC = () => {
className={styles.clickupButton}
onClick={handleCreateClickup}
disabled={isConnecting}
title={t('connections.clickupkontoVerbinden')}
title={t('ClickUp-Konto verbinden')}
>
<FaTasks /> ClickUp
</button>
@ -321,11 +322,11 @@ export const ConnectionsPage: React.FC = () => {
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('connectionsPage.edit'),
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('connectionsPage.delete'),
title: t('Löschen'),
loading: (row: Connection) => deletingConnections.has(row.id),
}] : []),
]}
@ -334,7 +335,7 @@ export const ConnectionsPage: React.FC = () => {
id: 'connect',
icon: <FaLink />,
onClick: handleConnect,
title: t('connectionsPage.connect'),
title: t('Verbinden'),
visible: (row: Connection) =>
row.status !== 'active' &&
(isClickupConnectionUiEnabled || row.authority !== 'clickup'),
@ -344,7 +345,7 @@ export const ConnectionsPage: React.FC = () => {
id: 'refresh',
icon: <FaRedo />,
onClick: handleRefresh,
title: t('connectionsPage.refreshToken'),
title: t('Token aktualisieren'),
visible: (row: Connection) => row.status === 'active',
loading: (row: Connection) => refreshingConnections.has(row.id),
},
@ -358,7 +359,7 @@ export const ConnectionsPage: React.FC = () => {
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('connections.keineVerbindungenGefunden')}
emptyMessage={t('Keine Verbindungen gefunden')}
/>
</div>
@ -367,7 +368,7 @@ export const ConnectionsPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingConnection(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('connections.verbindungBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Verbindung bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingConnection(null)}
@ -379,7 +380,7 @@ export const ConnectionsPage: React.FC = () => {
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('connections.ladeFormular')}</span>
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
@ -388,8 +389,8 @@ export const ConnectionsPage: React.FC = () => {
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingConnection(null)}
submitButtonText={t('connections.speichern')}
cancelButtonText={t('connections.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>

View file

@ -149,7 +149,7 @@ export const FilesPage: React.FC = () => {
cols.push({
key: 'sysCreatedBy',
label: t('filesPage.createdBy'),
label: t('Erstellt von'),
type: 'text' as any,
sortable: true,
filterable: false,
@ -229,7 +229,7 @@ export const FilesPage: React.FC = () => {
};
const _handleNewFolder = useCallback(async () => {
const name = await promptInput('Neuer Ordnername:', { title: t('filesPage.newFolder'), placeholder: 'Ordnername' });
const name = await promptInput('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
if (name?.trim()) {
await handleCreateFolder(name.trim(), selectedFolderId);
}
@ -325,7 +325,7 @@ export const FilesPage: React.FC = () => {
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('files.dateien')}</h1>
<h1 className={styles.pageTitle}>{t('Dateien')}</h1>
<p className={styles.pageSubtitle}>Dateiverwaltung</p>
</div>
<div className={styles.headerActions}>
@ -403,7 +403,7 @@ export const FilesPage: React.FC = () => {
flexShrink: 0, alignItems: 'center', flexWrap: 'wrap',
}}>
<button className={styles.secondaryButton} onClick={_handleNewFolder}>
<FaFolderPlus /> {t('filesPage.newFolder')}
<FaFolderPlus /> {t('Neuer Ordner')}
</button>
{canCreate && (
<button className={styles.primaryButton} onClick={handleUploadClick} disabled={uploadingFile}>
@ -435,11 +435,11 @@ export const FilesPage: React.FC = () => {
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('filesPage.edit'),
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('filesPage.delete'),
title: t('Löschen'),
loading: (row: UserFile) => deletingFiles.has(row.id),
}] : []),
]}
@ -448,14 +448,14 @@ export const FilesPage: React.FC = () => {
id: 'download',
icon: <FaDownload />,
onClick: handleDownload,
title: t('filesPage.download'),
title: t('Herunterladen'),
loading: (row: UserFile) => downloadingFiles.has(row.id),
},
{
id: 'preview',
icon: <FaEye />,
onClick: handlePreview,
title: t('filesPage.preview'),
title: t('Vorschau'),
loading: (row: UserFile) => previewingFiles.has(row.id),
},
]}
@ -469,7 +469,7 @@ export const FilesPage: React.FC = () => {
handleInlineUpdate,
updateOptimistically: updateFileOptimistically,
}}
emptyMessage={t('files.keineDateienGefunden')}
emptyMessage={t('Keine Dateien gefunden')}
/>
</div>
</div>
@ -480,14 +480,14 @@ export const FilesPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingFile(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('files.dateiBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Datei bearbeiten')}</h2>
<button className={styles.modalClose} onClick={() => setEditingFile(null)}></button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('files.ladeFormular')}</span>
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
@ -496,8 +496,8 @@ export const FilesPage: React.FC = () => {
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingFile(null)}
submitButtonText={t('files.speichern')}
cancelButtonText={t('files.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>

View file

@ -78,7 +78,7 @@ export const PromptsPage: React.FC = () => {
// Add sysCreatedBy column with FK resolution to show username
cols.push({
key: 'sysCreatedBy',
label: t('promptsPage.createdBy'),
label: t('Erstellt von'),
type: 'text' as any,
sortable: true,
filterable: false,
@ -175,8 +175,8 @@ export const PromptsPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('prompts.prompts')}</h1>
<p className={styles.pageSubtitle}>{t('prompts.prompttemplatesVerwalten')}</p>
<h1 className={styles.pageTitle}>{t('Prompts')}</h1>
<p className={styles.pageSubtitle}>{t('Prompt-Templates verwalten')}</p>
</div>
<div className={styles.headerActions}>
<button
@ -212,17 +212,17 @@ export const PromptsPage: React.FC = () => {
actionButtons={[
...(canCreate ? [{
type: 'copy' as const,
title: t('promptsPage.duplicate'),
title: t('Duplizieren'),
onAction: handleDuplicate,
}] : []),
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('promptsPage.edit'),
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('promptsPage.delete'),
title: t('Löschen'),
loading: (row: Prompt) => deletingPrompts.has(row.id),
}] : []),
]}
@ -235,7 +235,7 @@ export const PromptsPage: React.FC = () => {
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('prompts.keinePromptsGefunden')}
emptyMessage={t('Keine Prompts gefunden')}
/>
</div>
@ -244,7 +244,7 @@ export const PromptsPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('prompts.neuerPrompt')}</h2>
<h2 className={styles.modalTitle}>{t('Neuer Prompt')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
@ -256,7 +256,7 @@ export const PromptsPage: React.FC = () => {
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('prompts.ladeFormular')}</span>
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
@ -264,8 +264,8 @@ export const PromptsPage: React.FC = () => {
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText={t('prompts.erstellen')}
cancelButtonText={t('prompts.abbrechen')}
submitButtonText={t('Erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
@ -278,7 +278,7 @@ export const PromptsPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingPrompt(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('prompts.promptBearbeiten')}</h2>
<h2 className={styles.modalTitle}>{t('Prompt bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingPrompt(null)}
@ -290,7 +290,7 @@ export const PromptsPage: React.FC = () => {
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('prompts.ladeFormular')}</span>
<span>{t('Formular laden')}</span>
</div>
) : (
<FormGeneratorForm
@ -299,8 +299,8 @@ export const PromptsPage: React.FC = () => {
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingPrompt(null)}
submitButtonText={t('prompts.speichern')}
cancelButtonText={t('prompts.abbrechen')}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>

View file

@ -11,17 +11,17 @@ const _TERMINAL_STATUSES = new Set(['EXPIRED']);
function _getColumns(t: (key: string) => string): ColumnConfig[] {
return [
{ key: 'mandateName', label: t('adminSubscriptions.mandant'), type: 'text', sortable: true, filterable: true, width: 180 },
{ key: 'planTitle', label: t('adminSubscriptions.plan'), type: 'text', sortable: true, filterable: true, width: 180 },
{ key: 'status', label: t('adminSubscriptions.status'), type: 'text', sortable: true, filterable: true, width: 110 },
{ key: 'recurring', label: t('adminSubscriptions.wiederkehrend'), type: 'boolean', sortable: true, filterable: true, width: 120 },
{ key: 'activeUsers', label: t('adminSubscriptions.user'), type: 'number', sortable: true, width: 70 },
{ key: 'activeInstances', label: t('adminSubscriptions.module'), type: 'number', sortable: true, width: 90 },
{ key: 'monthlyRevenueCHF', label: t('adminSubscriptions.revenueProMonat'), type: 'number', sortable: true, width: 140 },
{ key: 'startedAt', label: t('adminSubscriptions.gestartet'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'currentPeriodEnd', label: t('adminSubscriptions.periodenende'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'snapshotPricePerUserCHF', label: t('adminSubscriptions.preisProUser'), type: 'number', sortable: true, width: 100 },
{ key: 'snapshotPricePerInstanceCHF', label: t('adminSubscriptions.preisProModul'), type: 'number', sortable: true, width: 110 },
{ key: 'mandateName', label: t('Mandant'), type: 'text', sortable: true, filterable: true, width: 180 },
{ key: 'planTitle', label: t('Plan'), type: 'text', sortable: true, filterable: true, width: 180 },
{ key: 'status', label: t('Status'), type: 'text', sortable: true, filterable: true, width: 110 },
{ key: 'recurring', label: t('Wiederkehrend'), type: 'boolean', sortable: true, filterable: true, width: 120 },
{ key: 'activeUsers', label: t('Benutzer'), type: 'number', sortable: true, width: 70 },
{ key: 'activeInstances', label: t('Module'), type: 'number', sortable: true, width: 90 },
{ key: 'monthlyRevenueCHF', label: t('Umsatz pro Monat'), type: 'number', sortable: true, width: 140 },
{ key: 'startedAt', label: t('Gestartet'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'currentPeriodEnd', label: t('Periodenende'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'snapshotPricePerUserCHF', label: t('Preis pro Benutzer'), type: 'number', sortable: true, width: 100 },
{ key: 'snapshotPricePerInstanceCHF', label: t('Preis pro Modul'), type: 'number', sortable: true, width: 110 },
];
}
@ -49,8 +49,8 @@ const AdminSubscriptionsPage: React.FC = () => {
return (
<div className={styles.billingDashboard} style={{ minHeight: 0 }}>
<header className={styles.pageHeader} style={{ flexShrink: 0 }}>
<h1>{t('adminSubscriptions.subscriptionuebersicht')}</h1>
<p className={styles.subtitle}>{t('adminSubscriptions.alleAbonnementsAllerMandanten')}</p>
<h1>{t('Abonnementübersicht')}</h1>
<p className={styles.subtitle}>{t('Alle Abonnements aller Mandanten')}</p>
</header>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
@ -66,13 +66,13 @@ const AdminSubscriptionsPage: React.FC = () => {
customActions={[
{
id: 'forceCancel',
title: t('adminSubscriptionsPage.cancelImmediately'),
title: t('Sofort stornieren'),
icon: '✕',
onClick: (row: any) => _handleForceCancel(row),
visible: (row: any) => !_TERMINAL_STATUSES.has(row._rawStatus),
},
]}
emptyMessage={t('adminSubscriptions.keineSubscriptionsVorhanden')}
emptyMessage={t('Keine Abonnements vorhanden')}
/>
</div>

View file

@ -64,7 +64,7 @@ const MandateSelector: React.FC<MandateSelectorProps> = ({
onChange={e => onSelect(e.target.value)}
disabled={loading}
>
<option value="">{t('billingAdmin.mandantWaehlen')}</option>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(mandate => (
<option key={mandate.id} value={mandate.id}>
{_mandateDisplayLabel(mandate)}
@ -179,7 +179,7 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({ settings, onSave, loadi
{formData.autoRechargeEnabled && (
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>{t('billingAdmin.betragProNachladungChf')}</label>
<label>{t('Betrag pro Nachladung (CHF)')}</label>
<input
type="number"
className={styles.input}
@ -418,13 +418,13 @@ const MandateStripeTopUp: React.FC<MandateStripeTopUpProps> = ({ mandateId, crea
<form onSubmit={_handleSubmit}>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>{t('billingAdmin.betragChf')}</label>
<label>{t('Betrag (CHF)')}</label>
<input
type="number"
className={styles.input}
value={amount}
onChange={e => setAmount(e.target.value)}
placeholder={t('billingAdmin.zb50')}
placeholder={t('z. B. 50 oder -20')}
min="0.01"
step="0.01"
required

View file

@ -67,11 +67,11 @@ const StatisticsChart: React.FC<StatisticsChartProps> = ({ statistics, loading }
};
if (loading) {
return <div className={styles.loadingPlaceholder}>{t('billingDashboard.ladeStatistiken')}</div>;
return <div className={styles.loadingPlaceholder}>{t('Statistiken laden')}</div>;
}
if (!statistics) {
return <div className={styles.noData}>{t('billingDashboard.keineStatistikenVerfuegbar')}</div>;
return <div className={styles.noData}>{t('Keine Statistiken verfügbar')}</div>;
}
// Calculate max cost for bar scaling
@ -85,9 +85,9 @@ const StatisticsChart: React.FC<StatisticsChartProps> = ({ statistics, loading }
</div>
<div className={styles.chartSection}>
<h4>{t('billingDashboard.kostenNachAnbieter')}</h4>
<h4>{t('Kosten nach Anbieter')}</h4>
{Object.entries(statistics.costByProvider).length === 0 ? (
<div className={styles.noData}>{t('billingDashboard.keineDaten')}</div>
<div className={styles.noData}>{t('Keine Daten')}</div>
) : (
<div className={styles.barChart}>
{Object.entries(statistics.costByProvider).map(([provider, cost]) => (
@ -107,9 +107,9 @@ const StatisticsChart: React.FC<StatisticsChartProps> = ({ statistics, loading }
</div>
<div className={styles.chartSection}>
<h4>{t('billingDashboard.kostenNachModell')}</h4>
<h4>{t('Kosten nach Modell')}</h4>
{Object.entries(statistics.costByModel || {}).length === 0 ? (
<div className={styles.noData}>{t('billingDashboard.keineDaten')}</div>
<div className={styles.noData}>{t('Keine Daten')}</div>
) : (
<div className={styles.barChart}>
{Object.entries(statistics.costByModel || {}).map(([model, cost]) => (
@ -129,9 +129,9 @@ const StatisticsChart: React.FC<StatisticsChartProps> = ({ statistics, loading }
</div>
<div className={styles.chartSection}>
<h4>{t('billingDashboard.kostenNachFeature')}</h4>
<h4>{t('Kosten nach Feature')}</h4>
{Object.entries(statistics.costByFeature).length === 0 ? (
<div className={styles.noData}>{t('billingDashboard.keineDaten')}</div>
<div className={styles.noData}>{t('Keine Daten')}</div>
) : (
<div className={styles.featureList}>
{Object.entries(statistics.costByFeature).map(([feature, cost]) => (
@ -188,19 +188,19 @@ export const BillingDashboard: React.FC = () => {
return (
<div className={styles.billingDashboard}>
<header className={styles.pageHeader}>
<h1>{t('billingDashboard.billing')}</h1>
<p className={styles.subtitle}>{t('billingDashboard.uebersichtUeberGuthabenUndNutzung')}</p>
<h1>{t('Abrechnung')}</h1>
<p className={styles.subtitle}>{t('Übersicht über Guthaben und Nutzung')}</p>
</header>
<BillingNav />
{/* Balance Cards */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('billingDashboard.guthaben')}</h2>
<h2 className={styles.sectionTitle}>{t('Guthaben')}</h2>
{loading ? (
<div className={styles.loadingPlaceholder}>{t('billingDashboard.ladeGuthaben')}</div>
<div className={styles.loadingPlaceholder}>{t('Guthaben laden')}</div>
) : balances.length === 0 ? (
<div className={styles.noData}>{t('billingDashboard.keineAbrechnungskontenVorhanden')}</div>
<div className={styles.noData}>{t('Keine Abrechnungskonten vorhanden')}</div>
) : (
<div className={styles.balanceGrid}>
{balances.map((balance) => (

View file

@ -519,8 +519,8 @@ export const BillingDataView: React.FC = () => {
<header className={styles.pageHeader}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1>{t('billingData.statistiken')}</h1>
<p className={styles.subtitle}>{t('billingData.nutzungDiagrammeUndTransaktionen')}</p>
<h1>{t('Statistiken')}</h1>
<p className={styles.subtitle}>{t('Nutzung, Diagramme und Transaktionen')}</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>

View file

@ -45,9 +45,9 @@ const MandateBalanceTable: React.FC<MandateBalanceTableProps> = ({ balances,
<table className={styles.transactionsTable}>
<thead>
<tr>
<th>{t('billingMandate.mandant')}</th>
<th>{t('billingMandate.anzahlBenutzer')}</th>
<th>{t('billingMandate.warnschwelle')}</th>
<th>{t('Mandant')}</th>
<th>{t('Anzahl Benutzer')}</th>
<th>{t('Warnschwelle')}</th>
<th style={{ textAlign: 'right' }}>Gesamtguthaben</th>
<th>Aktion</th>
</tr>
@ -70,7 +70,7 @@ const MandateBalanceTable: React.FC<MandateBalanceTableProps> = ({ balances,
)}
style={{ padding: '4px 8px', fontSize: '12px' }}
>
{selectedMandateId === balance.mandateId ? t('billingMandate.alle') : t('billingMandate.filter')}
{selectedMandateId === balance.mandateId ? t('Alle') : t('Filter')}
</button>
</td>
</tr>
@ -133,13 +133,13 @@ const TransactionTable: React.FC<TransactionTableProps> = ({ transactions }) =>
<thead>
<tr>
<th>Datum</th>
<th>{t('billingMandate.mandant')}</th>
<th>{t('Mandant')}</th>
<th>Typ</th>
<th>{t('billingMandate.beschreibung')}</th>
<th>{t('Beschreibung')}</th>
<th>Anbieter</th>
<th>Modell</th>
<th>Feature</th>
<th style={{ textAlign: 'right' }}>{t('billingMandate.betrag')}</th>
<th style={{ textAlign: 'right' }}>{t('Betrag')}</th>
</tr>
</thead>
<tbody>
@ -218,7 +218,7 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
<>
<header className={styles.pageHeader}>
<h1>Mandanten-Billing</h1>
<p className={styles.subtitle}>{t('billingMandate.guthabenUndTransaktionenProMandant')}</p>
<p className={styles.subtitle}>{t('Guthaben und Transaktionen pro Mandant')}</p>
</header>
<BillingNav />
@ -229,9 +229,9 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Mandanten-Guthaben</h2>
{loading && balances.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingMandate.ladeDaten')}</div>
<div className={styles.loadingPlaceholder}>{t('Daten laden')}</div>
) : balances.length === 0 ? (
<div className={styles.noData}>{t('billingMandate.keineMandantenMitBillingsettingsVorhanden')}</div>
<div className={styles.noData}>{t('Keine Mandanten mit Billing-Einstellungen vorhanden')}</div>
) : (
<MandateBalanceTable
balances={balances}
@ -254,9 +254,9 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
</h2>
</div>
{loading && transactions.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingMandate.ladeTransaktionen')}</div>
<div className={styles.loadingPlaceholder}>{t('Transaktionen laden')}</div>
) : filteredTransactions.length === 0 ? (
<div className={styles.noData}>{t('billingMandate.keineTransaktionenVorhanden')}</div>
<div className={styles.noData}>{t('Keine Transaktionen vorhanden')}</div>
) : (
<>
<TransactionTable transactions={filteredTransactions} />
@ -268,7 +268,7 @@ export const BillingMandateView: React.FC<BillingMandateViewProps> = ({ embedded
onClick={handleLoadMore}
disabled={loading}
>
{loading ? t('billingMandate.laden') : t('billingMandate.mehrLaden')}
{loading ? t('Laden') : t('Mehr laden')}
</button>
</div>
)}

View file

@ -96,17 +96,17 @@ export const BillingTransactions: React.FC = () => {
return (
<div className={styles.billingDashboard}>
<header className={styles.pageHeader}>
<h1>{t('billingTransactions.transaktionen')}</h1>
<p className={styles.subtitle}>{t('billingTransactions.uebersichtAllerKontobewegungen')}</p>
<h1>{t('Transaktionen')}</h1>
<p className={styles.subtitle}>{t('Übersicht aller Kontobewegungen')}</p>
</header>
<BillingNav />
<section className={styles.section}>
{loading && transactions.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingTransactions.ladeTransaktionen')}</div>
<div className={styles.loadingPlaceholder}>{t('Transaktionen laden')}</div>
) : transactions.length === 0 ? (
<div className={styles.noData}>{t('billingTransactions.keineTransaktionenVorhanden')}</div>
<div className={styles.noData}>{t('Keine Transaktionen vorhanden')}</div>
) : (
<>
<div style={{ overflowX: 'auto' }}>
@ -114,13 +114,13 @@ export const BillingTransactions: React.FC = () => {
<thead>
<tr>
<th>Datum</th>
<th>{t('billingTransactions.mandant')}</th>
<th>{t('Mandant')}</th>
<th>Typ</th>
<th>{t('billingTransactions.beschreibung')}</th>
<th>{t('Beschreibung')}</th>
<th>Anbieter</th>
<th>Modell</th>
<th>Feature</th>
<th style={{ textAlign: 'right' }}>{t('billingTransactions.betrag')}</th>
<th style={{ textAlign: 'right' }}>{t('Betrag')}</th>
</tr>
</thead>
<tbody>
@ -138,7 +138,7 @@ export const BillingTransactions: React.FC = () => {
onClick={handleLoadMore}
disabled={loading}
>
{loading ? t('billingTransactions.laden') : t('billingTransactions.mehrLaden')}
{loading ? t('Laden') : t('Mehr laden')}
</button>
</div>
)}

View file

@ -91,7 +91,7 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
onChange={(e) => onSelectMandate(e.target.value || null)}
className={styles.select}
>
<option value="">{t('billingUser.alleMandanten')}</option>
<option value="">{t('Alle Mandanten')}</option>
{uniqueMandates.map(([id, name]) => (
<option key={id} value={id}>{name}</option>
))}
@ -106,7 +106,7 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
onChange={(e) => onSelectUser(e.target.value || null)}
className={styles.select}
>
<option value="">{t('billingUser.alleBenutzer')}</option>
<option value="">{t('Alle Benutzer')}</option>
{uniqueUsers.map(([id, name]) => (
<option key={id} value={id}>{name}</option>
))}
@ -119,11 +119,11 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
<table className={styles.transactionsTable}>
<thead>
<tr>
<th>{t('billingUser.mandant')}</th>
<th>{t('billingUser.benutzer')}</th>
<th style={{ textAlign: 'right' }}>{t('billingUser.guthaben')}</th>
<th>{t('Mandant')}</th>
<th>{t('Benutzer')}</th>
<th style={{ textAlign: 'right' }}>{t('Guthaben')}</th>
<th style={{ textAlign: 'right' }}>Warnschwelle</th>
<th>{t('billingUser.status')}</th>
<th>{t('Status')}</th>
</tr>
</thead>
<tbody>
@ -142,7 +142,7 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
Niedrig
</span>
) : balance.enabled ? (
<span style={{ color: 'var(--color-success)' }}>{t('billingUser.aktiv')}</span>
<span style={{ color: 'var(--color-success)' }}>{t('Aktiv')}</span>
) : (
<span style={{ color: 'var(--color-error)' }}>Deaktiviert</span>
)}
@ -226,14 +226,14 @@ const UserTransactionTable: React.FC<UserTransactionTableProps> = ({
<thead>
<tr>
<th>Datum</th>
<th>{t('billingUser.mandant')}</th>
<th>{t('billingUser.benutzer')}</th>
<th>{t('Mandant')}</th>
<th>{t('Benutzer')}</th>
<th>Typ</th>
<th>{t('billingUser.beschreibung')}</th>
<th>{t('Beschreibung')}</th>
<th>Anbieter</th>
<th>Modell</th>
<th>Feature</th>
<th style={{ textAlign: 'right' }}>{t('billingUser.betrag')}</th>
<th style={{ textAlign: 'right' }}>{t('Betrag')}</th>
</tr>
</thead>
<tbody>
@ -314,7 +314,7 @@ export const BillingUserView: React.FC = () => {
<div className={styles.billingDashboard}>
<header className={styles.pageHeader}>
<h1>Benutzer-Billing</h1>
<p className={styles.subtitle}>{t('billingUser.guthabenUndTransaktionenProBenutzer')}</p>
<p className={styles.subtitle}>{t('Guthaben und Transaktionen pro Benutzer')}</p>
</header>
<BillingNav />
@ -323,9 +323,9 @@ export const BillingUserView: React.FC = () => {
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Benutzer-Guthaben</h2>
{loading && balances.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingUser.ladeDaten')}</div>
<div className={styles.loadingPlaceholder}>{t('Daten laden')}</div>
) : balances.length === 0 ? (
<div className={styles.noData}>{t('billingUser.keineBenutzerkontenVorhanden')}</div>
<div className={styles.noData}>{t('Keine Benutzerkonten vorhanden')}</div>
) : (
<UserBalanceTable
balances={balances}
@ -350,9 +350,9 @@ export const BillingUserView: React.FC = () => {
</h2>
</div>
{loading && transactions.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingUser.ladeTransaktionen')}</div>
<div className={styles.loadingPlaceholder}>{t('Transaktionen laden')}</div>
) : transactions.length === 0 ? (
<div className={styles.noData}>{t('billingUser.keineTransaktionenVorhanden')}</div>
<div className={styles.noData}>{t('Keine Transaktionen vorhanden')}</div>
) : (
<>
<UserTransactionTable
@ -368,7 +368,7 @@ export const BillingUserView: React.FC = () => {
onClick={handleLoadMore}
disabled={loading}
>
{loading ? t('billingUser.laden') : t('billingUser.mehrLaden')}
{loading ? t('Laden') : t('Mehr laden')}
</button>
</div>
)}

View file

@ -44,19 +44,19 @@ const _formatDate = (iso: string | null | undefined): string => {
function _getStatusLabel(t: (key: string) => string): Record<string, { label: string; color: string }> {
return {
PENDING: { label: t('subscriptionTab.zahlungAusstehend'), color: '#f59e0b' },
SCHEDULED: { label: t('subscriptionTab.geplant'), color: '#8b5cf6' },
ACTIVE: { label: t('subscriptionTab.aktiv'), color: '#22c55e' },
TRIALING: { label: t('subscriptionTab.testphase'), color: '#38bdf8' },
PAST_DUE: { label: t('subscriptionTab.zahlungAusstehend'), color: '#f59e0b' },
EXPIRED: { label: t('subscriptionTab.abgelaufen'), color: '#6b7280' },
PENDING: { label: t('Zahlung ausstehend'), color: '#f59e0b' },
SCHEDULED: { label: t('Geplant'), color: '#8b5cf6' },
ACTIVE: { label: t('Aktiv'), color: '#22c55e' },
TRIALING: { label: t('Testphase'), color: '#38bdf8' },
PAST_DUE: { label: t('Zahlung ausstehend'), color: '#f59e0b' },
EXPIRED: { label: t('Abgelaufen'), color: '#6b7280' },
};
}
function _getPeriodLabel(t: (key: string) => string): Record<string, string> {
return {
MONTHLY: t('subscriptionTab.monatlich'),
YEARLY: t('subscriptionTab.jaehrlich'),
MONTHLY: t('Monatlich'),
YEARLY: t('hrlich'),
NONE: '—',
};
}
@ -164,7 +164,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
>
{activating
? 'Weiterleitung...'
: (!isFreePlan && !plan.trialDays) ? t('subscriptionTab.kostenpflichtigAbonnieren') : t('subscriptionTab.auswaehlen')}
: (!isFreePlan && !plan.trialDays) ? t('Kostenpflichtig abonnieren') : t('Auswählen')}
</button>
)}
</div>
@ -215,7 +215,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
<span style={{
fontSize: '0.7rem', padding: '2px 8px', borderRadius: '4px',
background: '#ef4444', color: '#fff', fontWeight: 600,
}}>{t('subscriptionTab.gekuendigt')}</span>
}}>{t('Gekündigt')}</span>
)}
<span style={{
fontSize: '0.75rem', padding: '2px 10px', borderRadius: '4px',
@ -232,8 +232,8 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
color: justPaid ? '#22c55e' : '#f59e0b', fontSize: '0.85rem',
}}>
{justPaid
? t('subscriptionTab.zahlungErfolgreichAbonnementWirdAktiviert')
: t('subscriptionTab.dieZahlungWurdeNochNicht')}
? t('Zahlung erfolgreich Abonnement wird aktiviert')
: t('Zahlung noch nicht eingegangen')}
</div>
)}
@ -288,7 +288,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
fontWeight: 600, cursor: reactivating ? 'wait' : 'pointer', fontSize: '0.85rem',
}}
>
{reactivating ? t('subscriptionTab.wirdReaktiviert') : t('subscriptionTab.reaktivieren')}
{reactivating ? t('Wird reaktiviert') : t('Reaktivieren')}
</button>
)}
@ -303,7 +303,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
cursor: cancelling ? 'wait' : 'pointer', fontSize: '0.85rem',
}}
>
{cancelling ? t('subscriptionTab.wirdGekuendigt') : t('subscriptionTab.kuendigen')}
{cancelling ? t('Wird gekündigt') : t('Kündigen')}
</button>
)}
@ -318,7 +318,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
cursor: cancelling ? 'wait' : 'pointer', fontSize: '0.85rem',
}}
>
{cancelling ? t('subscriptionTab.wirdAbgebrochen') : t('subscriptionTab.abbrechen')}
{cancelling ? t('Wird abgebrochen') : t('Abbrechen')}
</button>
)}
</div>
@ -421,12 +421,12 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
const isPendingOrScheduled = sub?.status === 'PENDING' || sub?.status === 'SCHEDULED';
const ok = await confirm(
isPendingOrScheduled
? t('subscriptionTab.diesenVorgangAbbrechen')
: t('subscriptionTab.abonnementKuendigenEsBleibtBis'),
? t('Diesen Vorgang abbrechen?')
: t('Abonnement kündigen? Es bleibt bis zum Periodenende aktiv.'),
{
title: isPendingOrScheduled ? t('subscriptionTab.vorgangAbbrechen') : t('subscriptionTab.abonnementKuendigen'),
confirmLabel: isPendingOrScheduled ? t('subscriptionTab.jaAbbrechen') : t('subscriptionTab.kuendigen'),
cancelLabel: isPendingOrScheduled ? t('subscriptionTab.neinZurueck') : t('subscriptionTab.abbrechen'),
title: isPendingOrScheduled ? t('Vorgang abbrechen') : t('Abonnement kündigen'),
confirmLabel: isPendingOrScheduled ? t('Ja, abbrechen') : t('Kündigen'),
cancelLabel: isPendingOrScheduled ? t('Nein, zurück') : t('Abbrechen'),
variant: 'danger',
},
);
@ -456,7 +456,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
}, [reactivateSubscription]);
if (loading && !subscription) {
return <div className={styles.loadingPlaceholder}>{t('subscriptionTab.ladeAbonnementdaten')}</div>;
return <div className={styles.loadingPlaceholder}>{t('Abonnementdaten werden geladen…')}</div>;
}
return (
@ -483,13 +483,13 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{/* Current subscription */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('subscriptionTab.aktuellesAbonnement')}</h2>
<h2 className={styles.sectionTitle}>{t('Aktuelles Abonnement')}</h2>
{subscription ? (
<SubInfoCard
sub={subscription}
plan={currentPlan}
label={subscription.status === 'PENDING'
? (justPaid ? t('subscriptionTab.zahlungWirdVerarbeitet') : t('subscriptionTab.checkoutInBearbeitung'))
? (justPaid ? t('Zahlung wird verarbeitet…') : t('Checkout läuft…'))
: 'Operatives Abonnement'}
onCancel={_handleCancel}
onReactivate={_handleReactivate}
@ -507,11 +507,11 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{/* Scheduled successor */}
{scheduled && (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('subscriptionTab.geplanterNachfolger')}</h2>
<h2 className={styles.sectionTitle}>{t('Geplanter Nachfolgeplan')}</h2>
<SubInfoCard
sub={scheduled}
plan={null}
label={t('subscriptionTab.startetNachAblaufDesAktuellen')}
label={t('Startet nach Ablauf des aktuellen Plans')}
onCancel={_handleCancel}
cancelling={cancelling}
reactivating={false}
@ -521,9 +521,9 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{/* Available plans */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('subscriptionTab.verfuegbarePlaene')}</h2>
<h2 className={styles.sectionTitle}>{t('Verfügbare Pläne')}</h2>
{plans.length === 0 ? (
<div className={styles.noData}>{t('subscriptionTab.keinePlaeneVerfuegbar')}</div>
<div className={styles.noData}>{t('Keine Pläne verfügbar')}</div>
) : (
<div style={{
display: 'grid',

View file

@ -85,7 +85,7 @@ export const ChatbotConversationsView: React.FC = () => {
const handleDeleteThread = useCallback(async (e: React.MouseEvent, workflowId: string) => {
e.stopPropagation();
const ok = await confirm('Möchten Sie diese Konversation wirklich löschen?', {
title: t('chatbotConversationsView.deleteConversation'),
title: t('Konversation löschen'),
confirmLabel: 'Löschen',
variant: 'danger',
});
@ -137,7 +137,7 @@ export const ChatbotConversationsView: React.FC = () => {
<button
className={styles.newChatButton}
onClick={createNewThread}
title={t('chatbotConversations.neueKonversation')}
title={t('Neues Gespräch')}
>
<LuMessageSquare /> Neu
</button>
@ -146,7 +146,7 @@ export const ChatbotConversationsView: React.FC = () => {
{loadingThreads ? (
<div className={styles.loading}>
<div className={styles.spinner} />
<span>{t('chatbotConversations.ladeKonversationen')}</span>
<span>{t('Konversationen laden')}</span>
</div>
) : error ? (
<div className={styles.error}>
@ -158,8 +158,8 @@ export const ChatbotConversationsView: React.FC = () => {
) : threads.length === 0 ? (
<div className={styles.emptyState}>
<LuMessageSquare className={styles.emptyIcon} />
<p>{t('chatbotConversations.nochKeineKonversationenVorhanden')}</p>
<p className={styles.emptyHint}>{t('chatbotConversations.starteEineNeueKonversationUm')}</p>
<p>{t('Noch keine Gespräche')}</p>
<p className={styles.emptyHint}>{t('Starten Sie ein neues Gespräch, um loszulegen.')}</p>
</div>
) : (
<div className={styles.threadList}>
@ -179,7 +179,7 @@ export const ChatbotConversationsView: React.FC = () => {
className={styles.deleteButton}
onClick={(e) => handleDeleteThread(e, thread.id)}
disabled={deletingId === thread.id}
title={t('chatbotConversations.loeschen')}
title={t('schen')}
>
{deletingId === thread.id ? (
<div className={styles.spinner} />
@ -200,7 +200,7 @@ export const ChatbotConversationsView: React.FC = () => {
{loadingMessages && messages.length === 0 ? (
<div className={styles.loading}>
<div className={styles.spinner} />
<span>{t('chatbotConversations.ladeNachrichten')}</span>
<span>{t('Nachrichten laden')}</span>
</div>
) : messages.length === 0 ? (
<div className={`${messagesStyles.messagesContainer} ${messagesStyles.emptyContainer}`}>
@ -249,7 +249,7 @@ export const ChatbotConversationsView: React.FC = () => {
value={inputValue}
onChange={setInputValue}
onKeyDown={handleKeyDown}
placeholder={t('chatbotConversations.nachrichtEingeben')}
placeholder={t('Nachricht eingeben')}
disabled={isStreaming}
className={styles.inputField}
size="md"

View file

@ -26,7 +26,7 @@ export const CommcoachDashboardView: React.FC = () => {
};
if (loading && !dashboard) {
return <div className={styles.loading}>{t('commcoachDashboard.dashboardWirdGeladen')}</div>;
return <div className={styles.loading}>{t('Dashboard wird geladen…')}</div>;
}
if (error) {
@ -34,7 +34,7 @@ export const CommcoachDashboardView: React.FC = () => {
}
if (!dashboard) {
return <div className={styles.empty}>{t('commcoachDashboard.keineDatenVerfuegbar')}</div>;
return <div className={styles.empty}>{t('Keine Daten verfügbar')}</div>;
}
return (
@ -43,7 +43,7 @@ export const CommcoachDashboardView: React.FC = () => {
<div className={styles.kpiGrid}>
<div className={styles.kpiCard}>
<div className={styles.kpiValue}>{dashboard.streakDays}</div>
<div className={styles.kpiLabel}>{t('commcoachDashboard.tageStreak')}</div>
<div className={styles.kpiLabel}>{t('Tage in Folge')}</div>
<div className={styles.kpiSub}>Rekord: {dashboard.longestStreak}</div>
</div>
<div className={styles.kpiCard}>
@ -69,11 +69,11 @@ export const CommcoachDashboardView: React.FC = () => {
{/* Active Contexts */}
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t('commcoachDashboard.aktiveCoachingthemen')}</h3>
<h3 className={styles.sectionTitle}>{t('Aktive Coaching-Themen')}</h3>
{dashboard.contexts.length === 0 ? (
<div className={styles.emptyState}>
<p>{t('commcoachDashboard.nochKeineCoachingthemenErstellt')}</p>
<p>{t('commcoachDashboard.wechsleZumCoachingtabUmDein')}</p>
<p>{t('Noch keine Coaching-Themen angelegt.')}</p>
<p>{t('Wechseln Sie zum Tab Coaching, um ein Thema anzulegen.')}</p>
</div>
) : (
<div className={styles.contextGrid}>
@ -126,7 +126,7 @@ export const CommcoachDashboardView: React.FC = () => {
{/* Quick Start */}
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t('commcoachDashboard.tippDesTages')}</h3>
<h3 className={styles.sectionTitle}>{t('Tipp des Tages')}</h3>
<div className={styles.tipCard}>
<p>Konsistenz schlägt Intensität. Auch 10 Minuten tägliches Coaching-Gespräch
bringt messbare Fortschritte in deiner Kommunikationskompetenz.</p>

View file

@ -250,7 +250,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
}, [newTaskTitle, coach]);
if (coach.loadingContexts) {
return <div className={styles.empty}><p>{t('commcoachDossier.lade')}</p></div>;
return <div className={styles.empty}><p>{t('lade')}</p></div>;
}
return (
@ -261,7 +261,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button
className={styles.udbToggle}
onClick={() => setUdbCollapsed(v => !v)}
title={udbCollapsed ? t('commcoachDossier.seitenleisteEinblenden') : t('commcoachDossier.seitenleisteAusblenden')}
title={udbCollapsed ? t('Seitenleiste einblenden') : t('Seitenleiste ausblenden')}
>
{udbCollapsed ? '\u25B6' : '\u25C0'}
</button>
@ -293,7 +293,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button
className={styles.contextChipNew}
onClick={() => setShowNewContext(!showNewContext)}
title={t('commcoachDossier.neuesThema')}
title={t('Neues Thema')}
>
+
</button>
@ -304,7 +304,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.newContextForm}>
<input
className={styles.newContextInput}
placeholder={t('commcoachDossier.themaTitel')}
placeholder={t('Thema Titel')}
value={newTitle}
onChange={e => setNewTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreateContext()}
@ -312,25 +312,25 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
/>
<input
className={styles.newContextInput}
placeholder={t('commcoachDossier.beschreibungOptional')}
placeholder={t('Beschreibung (optional)')}
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
/>
<select className={styles.newContextInput} value={newCategory} onChange={e => setNewCategory(e.target.value)}>
<option value="custom">Individuell</option>
<option value="leadership">{t('commcoachDossier.fuehrung')}</option>
<option value="leadership">{t('hrung')}</option>
<option value="conflict">Konflikt</option>
<option value="negotiation">Verhandlung</option>
<option value="presentation">{t('commcoachDossier.praesentation')}</option>
<option value="presentation">{t('Präsentation')}</option>
<option value="feedback">Feedback</option>
<option value="delegation">Delegation</option>
<option value="changeManagement">{t('commcoachDossier.changeManagement')}</option>
<option value="changeManagement">{t('Change Management')}</option>
</select>
<div className={styles.newContextActions}>
<button className={styles.btnPrimary} onClick={handleCreateContext} disabled={!newTitle.trim() || !!coach.actionLoading}>
{coach.actionLoading === 'creating' ? t('commcoachDossier.wirdErstellt') : t('commcoachDossier.erstellen')}
{coach.actionLoading === 'creating' ? t('wird erstellt') : t('erstellen')}
</button>
<button className={styles.btnSecondary} onClick={() => setShowNewContext(false)}>{t('commcoachDossier.abbrechen')}</button>
<button className={styles.btnSecondary} onClick={() => setShowNewContext(false)}>{t('Abbrechen')}</button>
</div>
</div>
)}
@ -338,9 +338,9 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{/* No context selected */}
{!coach.selectedContextId && !showNewContext && coach.contexts.length === 0 && (
<div className={styles.empty}>
<h3>{t('commcoachDossier.willkommenBeimKommunikationscoach')}</h3>
<p>{t('commcoachDossier.erstelleEinThemaUmZu')}</p>
<button className={styles.btnPrimary} onClick={() => setShowNewContext(true)}>{t('commcoachDossier.neuesThemaErstellen')}</button>
<h3>{t('Willkommen beim Kommunikationscoach')}</h3>
<p>{t('Erstelle ein Thema, um zu')}</p>
<button className={styles.btnPrimary} onClick={() => setShowNewContext(true)}>{t('Neues Thema erstellen')}</button>
</div>
)}
@ -356,12 +356,12 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.headerActions}>
{instanceId && (
<>
<a className={styles.btnExport} href={getDossierExportUrl(instanceId, coach.selectedContextId, 'md')} target="_blank" rel="noopener noreferrer">{t('commcoachDossier.exportMd')}</a>
<a className={styles.btnExport} href={getDossierExportUrl(instanceId, coach.selectedContextId, 'pdf')} target="_blank" rel="noopener noreferrer">{t('commcoachDossier.exportPdf')}</a>
<a className={styles.btnExport} href={getDossierExportUrl(instanceId, coach.selectedContextId, 'md')} target="_blank" rel="noopener noreferrer">{t('Export MD')}</a>
<a className={styles.btnExport} href={getDossierExportUrl(instanceId, coach.selectedContextId, 'pdf')} target="_blank" rel="noopener noreferrer">{t('Export PDF')}</a>
</>
)}
<button className={styles.btnArchive} onClick={() => coach.archiveContext(coach.selectedContextId!)} disabled={!!coach.actionLoading}>
{coach.actionLoading === 'archiving' ? t('commcoachDossier.wirdArchiviert') : t('commcoachDossier.archivieren')}
{coach.actionLoading === 'archiving' ? t('wird archiviert') : t('archivieren')}
</button>
</div>
</div>
@ -386,10 +386,10 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.coachingTab}>
{!coach.session ? (
<div className={styles.sessionStart}>
<p>{t('commcoachDossier.starteEineNeueCoachingsessionZu')}</p>
<p>{t('Starte eine neue Coachingsession zu')}</p>
{personas.length > 0 && (
<div className={styles.personaSelector}>
<label className={styles.personaLabel}>{t('commcoachDossier.gespraechspartnerWaehlen')}</label>
<label className={styles.personaLabel}>{t('Gesprächspartner wählen')}</label>
<div className={styles.personaGrid}>
{personas.map(p => (
<button
@ -417,7 +417,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<>
{/* Session Header */}
<div className={styles.sessionHeader}>
<span className={styles.sessionLabel}>{t('commcoachDossier.sessionAktiv')}</span>
<span className={styles.sessionLabel}>{t('Session aktiv')}</span>
<div className={styles.sessionActions}>
{voice.state === 'botSpeaking' && (
<>
@ -431,15 +431,15 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button
className={`${styles.btnSmall} ${voice.muted ? styles.mutedActive : ''}`}
onClick={voice.toggleMute}
title={voice.muted ? t('commcoachDossier.stummschaltungAufheben') : t('commcoachDossier.stummschalten')}
title={voice.muted ? t('Stummschaltung aufheben') : t('stummschalten')}
>
{voice.muted ? t('commcoachDossier.u1f507Stumm') : t('commcoachDossier.u1f3a4TonAn')}
{voice.muted ? t('🔇 Stumm') : t('🎤 Ton an')}
</button>
<button className={styles.btnSmall} onClick={coach.completeSession} disabled={!!coach.actionLoading}>
{coach.actionLoading === 'completing' ? t('commcoachDossier.wirdAbgeschlossen') : t('commcoachDossier.abschliessen')}
{coach.actionLoading === 'completing' ? t('wird abgeschlossen') : t('abschließen')}
</button>
<button className={styles.btnSmallDanger} onClick={coach.cancelSession} disabled={!!coach.actionLoading}>
{coach.actionLoading === 'cancelling' ? t('commcoachDossier.wirdAbgebrochen') : t('commcoachDossier.abbrechen')}
{coach.actionLoading === 'cancelling' ? t('wird abgebrochen') : t('Abbrechen')}
</button>
</div>
</div>
@ -488,7 +488,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{coach.agentToolCalls.length > 0 ? ` (${coach.agentToolCalls.length})` : ''}
</span>
<span className={styles.agentActivityStatus}>
{coach.streamingStatus || (coach.agentToolCalls.length > 0 ? t('commcoachDossier.toolaufrufeVorhanden') : t('commcoachDossier.warteAufAgent'))}
{coach.streamingStatus || (coach.agentToolCalls.length > 0 ? t('Toolaufrufe vorhanden') : t('Warte auf Agent'))}
</span>
<span className={styles.agentActivityChevron}>{showAgentActivity ? '▾' : '▸'}</span>
</button>
@ -601,7 +601,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<textarea
ref={inputRef}
className={styles.textInput}
placeholder={t('commcoachDossier.nachrichtEingeben')}
placeholder={t('Nachricht eingeben')}
value={coach.inputValue}
onChange={e => coach.setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
@ -615,7 +615,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button
onClick={() => { setShowFilePicker(v => !v); setShowSourcePicker(false); }}
disabled={coach.isStreaming}
title={t('commcoachDossier.dateiAnhaengen')}
title={t('Datei anhängen')}
style={{
width: 36, height: 36, borderRadius: 8,
border: `1px solid ${attachedFileIds.length ? '#1565c0' : 'var(--border-color, #ddd)'}`,
@ -640,7 +640,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
borderRadius: 8, boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', zIndex: 20,
minWidth: 220, maxHeight: 240, overflowY: 'auto',
}}>
<div style={{ padding: '6px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>{t('commcoachDossier.dateienAnhaengen')}</div>
<div style={{ padding: '6px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>{t('Dateien anhängen')}</div>
{wsFiles.map(f => {
const sel = attachedFileIds.includes(f.id);
return (
@ -670,7 +670,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button
onClick={() => { setShowSourcePicker(v => !v); setShowFilePicker(false); }}
disabled={coach.isStreaming}
title={t('commcoachDossier.datenquellenAnhaengen')}
title={t('Datenquellen anhängen')}
style={{
width: 36, height: 36, borderRadius: 8,
border: `1px solid ${(attachedDsIds.length + attachedFdsIds.length) ? '#2e7d32' : 'var(--border-color, #ddd)'}`,
@ -697,7 +697,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
}}>
{wsDataSources.length > 0 && (
<>
<div style={{ padding: '6px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>{t('commcoachDossier.persoenlicheQuellen')}</div>
<div style={{ padding: '6px 12px', fontSize: 11, color: '#999', fontWeight: 600, borderBottom: '1px solid #f0f0f0' }}>{t('Persönliche Quellen')}</div>
{wsDataSources.map(ds => {
const sel = attachedDsIds.includes(ds.id);
return (
@ -774,17 +774,17 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.addTaskRow}>
<input
className={styles.addTaskInput}
placeholder={t('commcoachDossier.neueAufgabe')}
placeholder={t('Neue Aufgabe')}
value={newTaskTitle}
onChange={e => setNewTaskTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleAddTask()}
/>
<button className={styles.addTaskBtn} onClick={handleAddTask} disabled={!newTaskTitle.trim() || !!coach.actionLoading}>
{coach.actionLoading === 'addingTask' ? t('commcoachDossier.wirdHinzugefuegt') : t('commcoachDossier.hinzufuegen')}
{coach.actionLoading === 'addingTask' ? t('wird hinzugefügt') : t('hinzufügen')}
</button>
</div>
{coach.tasks.length === 0 ? (
<div className={styles.emptyTab}>{t('commcoachDossier.nochKeineAufgabenDerCoach')}</div>
<div className={styles.emptyTab}>{t('Noch keine Aufgaben der Coach')}</div>
) : (
<div className={styles.taskList}>
{coach.tasks.map(task => (
@ -813,14 +813,14 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{activeTab === 'sessions' && (
<div className={styles.tabContent}>
{coach.sessions.length === 0 ? (
<div className={styles.emptyTab}>{t('commcoachDossier.nochKeineAbgeschlossenenSessions')}</div>
<div className={styles.emptyTab}>{t('Noch keine abgeschlossenen Sessions')}</div>
) : (
<div className={styles.sessionTimeline}>
{coach.sessions.map(s => (
<div key={s.id} className={styles.sessionItem}>
<div className={styles.sessionItemHeader}>
<span className={`${styles.sessionStatus} ${styles[`status_${s.status}`]}`}>
{s.status === 'completed' ? 'Abgeschlossen' : s.status === 'active' ? t('commcoachDossier.aktiv') : t('commcoachDossier.abgebrochen')}
{s.status === 'completed' ? 'Abgeschlossen' : s.status === 'active' ? t('aktiv') : t('Abgebrochen')}
</span>
<span className={styles.sessionDate}>{s.startedAt ? new Date(s.startedAt).toLocaleDateString('de-CH') : ''}</span>
{s.competenceScore != null && <span className={styles.sessionScore}>Score: {Math.round(s.competenceScore)}</span>}
@ -830,7 +830,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
)}
<div className={styles.sessionMeta}>
{s.messageCount} Nachrichten | {Math.round(s.durationSeconds / 60)} Min.
{s.personaId && <span> {t('commcoachDossier.persona')}</span>}
{s.personaId && <span> {t('Persona')}</span>}
{instanceId && s.status === 'completed' && (
<a className={styles.sessionExport} href={getSessionExportUrl(instanceId, s.id, 'md')} target="_blank" rel="noopener noreferrer" onClick={e => e.stopPropagation()}>Export</a>
)}
@ -848,7 +848,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{activeTab === 'scores' && (
<div className={styles.tabContent}>
{coach.scores.length === 0 ? (
<div className={styles.emptyTab}>{t('commcoachDossier.nochKeineBewertungenSchliesseEine')}</div>
<div className={styles.emptyTab}>{t('Noch keine Bewertungen, schließe eine')}</div>
) : (
<div className={styles.scoreList}>
{_groupScoresByDimension(coach.scores).map(group => (
@ -898,10 +898,10 @@ function _categoryIcon(category: string): string {
function _tabLabel(tab: TabKey, coach: any, t: (key: string) => string): string {
switch (tab) {
case 'coaching': return coach.session ? t('commcoachDossier.coachingAktiv') : t('commcoachDossier.coaching');
case 'tasks': return `${t('commcoachDossier.aufgaben')} (${coach.tasks.length})`;
case 'coaching': return coach.session ? t('Coaching aktiv') : t('Coaching');
case 'tasks': return `${t('Aufgaben')} (${coach.tasks.length})`;
case 'sessions': return `Sessions (${coach.sessions.length})`;
case 'scores': return `${t('commcoachDossier.bewertungen')} (${coach.scores.length})`;
case 'scores': return `${t('Bewertungen')} (${coach.scores.length})`;
}
}

View file

@ -66,7 +66,7 @@ export const CommcoachSettingsView: React.FC = () => {
emailSummaryEnabled: emailEnabled,
});
setProfile(updated);
setSuccess(t('commcoachSettings.einstellungenGespeichert'));
setSuccess(t('Einstellungen gespeichert'));
setTimeout(() => setSuccess(null), 3000);
} catch (err: any) {
setError(err.message || 'Fehler beim Speichern');
@ -76,7 +76,7 @@ export const CommcoachSettingsView: React.FC = () => {
}, [request, instanceId, reminderEnabled, reminderTime, emailEnabled]);
if (loading) {
return <div className={styles.loading}>{t('commcoachSettings.einstellungenWerdenGeladen')}</div>;
return <div className={styles.loading}>{t('Einstellungen werden geladen')}</div>;
}
return (
@ -85,7 +85,7 @@ export const CommcoachSettingsView: React.FC = () => {
{success && <div className={styles.success}>{success}</div>}
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{t('commcoachSettings.stimmeSprache')}</h3>
<h3 className={styles.sectionTitle}>{t('Stimme/Sprache')}</h3>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #888)', margin: '0 0 0.5rem' }}>
Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert.
</p>
@ -120,16 +120,16 @@ export const CommcoachSettingsView: React.FC = () => {
<div className={styles.section}>
<h3 className={styles.sectionTitle}>Statistik</h3>
<div className={styles.statsGrid}>
<div className={styles.statItem}><span className={styles.statValue}>{profile.totalSessions}</span><span className={styles.statLabel}>{t('commcoachSettings.sessionsGesamt')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.totalMinutes}</span><span className={styles.statLabel}>{t('commcoachSettings.minutenGesamt')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.streakDays}</span><span className={styles.statLabel}>{t('commcoachSettings.aktuellerStreak')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.longestStreak}</span><span className={styles.statLabel}>{t('commcoachSettings.laengsterStreak')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.totalSessions}</span><span className={styles.statLabel}>{t('Sessions gesamt')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.totalMinutes}</span><span className={styles.statLabel}>{t('Minuten gesamt')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.streakDays}</span><span className={styles.statLabel}>{t('Aktueller Streak')}</span></div>
<div className={styles.statItem}><span className={styles.statValue}>{profile.longestStreak}</span><span className={styles.statLabel}>{t('Längster Streak')}</span></div>
</div>
</div>
)}
<button className={styles.saveBtn} onClick={handleSave} disabled={saving}>
{saving ? t('commcoachSettings.speichern') : t('commcoachSettings.einstellungenSpeichern')}
{saving ? t('speichern') : t('Einstellungen speichern')}
</button>
</div>
);

View file

@ -102,7 +102,7 @@ export const GraphicalEditorPage: React.FC<GraphicalEditorPageProps> = ({
if (!instanceId) {
return (
<div style={{ padding: '2rem', textAlign: 'center' }}>
<p>{t('graphicalEditor.keineFeatureinstanzGefunden')}</p>
<p>{t('Keine Feature-Instanz gefunden')}</p>
</div>
);
}

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