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

View file

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

View file

@ -61,11 +61,11 @@ export function ContentPreview({
if (isOpen && fileId) { if (isOpen && fileId) {
// Check if we have valid data // Check if we have valid data
if (!fileId || fileId === 'undefined' || fileId === 'null') { if (!fileId || fileId === 'undefined' || fileId === 'null') {
setError(t('contentPreview.invalidFileId')); setError(t('Ungültige Datei-ID'));
return; return;
} }
if (!fileName || fileName === 'Unknown Item') { if (!fileName || fileName === 'Unknown Item') {
setError(t('contentPreview.fileNameNotAvailable')); setError(t('Dateiname nicht verfügbar'));
return; return;
} }
loadPreview(); loadPreview();
@ -98,7 +98,7 @@ export function ContentPreview({
setError(result.error || 'Failed to load preview'); setError(result.error || 'Failed to load preview');
} }
} catch (err) { } catch (err) {
setError(t('contentPreview.anUnexpectedErrorOccurredWhile')); setError(t('Ein unerwarteter Fehler ist aufgetreten, während'));
} }
}; };
@ -168,7 +168,7 @@ export function ContentPreview({
previewUrl={undefined} previewUrl={undefined}
previewContent={previewContent} previewContent={previewContent}
fileName={fileName} fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadPdfPreview'))} onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
/> />
); );
} }
@ -194,9 +194,9 @@ export function ContentPreview({
return ( return (
<div className={styles.jsonContainer}> <div className={styles.jsonContainer}>
<div className={styles.jsonHeader}> <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}> <div className={styles.jsonHeaderRight}>
<span className={styles.jsonSize}>{t('contentPreview.rawContent')}</span> <span className={styles.jsonSize}>{t('Rohinhalt')}</span>
</div> </div>
</div> </div>
<pre className={styles.jsonPreview}> <pre className={styles.jsonPreview}>
@ -219,7 +219,7 @@ export function ContentPreview({
<ImageRenderer <ImageRenderer
previewUrl={previewUrl} previewUrl={previewUrl}
fileName={fileName} fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadImagePreview'))} onError={() => setError(t('Bildvorschau konnte nicht geladen werden'))}
/> />
); );
@ -230,7 +230,7 @@ export function ContentPreview({
<HtmlRenderer <HtmlRenderer
previewUrl={previewUrl} previewUrl={previewUrl}
fileName={fileName} 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} previewUrl={previewUrl}
fileName={fileName} fileName={fileName}
mimeType={mimeType} mimeType={mimeType}
onError={() => setError(t('contentPreview.failedToLoadTextPreview'))} onError={() => setError(t('Textvorschau konnte nicht geladen werden'))}
/> />
); );
@ -257,7 +257,7 @@ export function ContentPreview({
previewUrl={previewUrl} previewUrl={previewUrl}
previewContent={previewContent || undefined} previewContent={previewContent || undefined}
fileName={fileName} fileName={fileName}
onError={() => setError(t('contentPreview.failedToLoadPdfPreview'))} onError={() => setError(t('PDF-Vorschau konnte nicht geladen werden'))}
/> />
); );
} }
@ -267,7 +267,7 @@ export function ContentPreview({
<HtmlRenderer <HtmlRenderer
previewUrl={previewUrl} previewUrl={previewUrl}
fileName={fileName} 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} previewUrl={previewUrl}
fileName={fileName} fileName={fileName}
mimeType={mimeType} 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 // If PDF.js also fails, show error
setIsLoading(false); setIsLoading(false);
setError(t('urlContentPreview.failedToLoadPdfThis')); setError(t('Fehler beim Laden des PDFs'));
setShowPdfAnyway(true); setShowPdfAnyway(true);
}; };
@ -111,7 +111,7 @@ export function UrlContentPreview({
} else if (isLoading && !hasLoaded && usePdfJs) { } else if (isLoading && !hasLoaded && usePdfJs) {
// PDF.js also failed, show error // PDF.js also failed, show error
setShowPdfAnyway(true); setShowPdfAnyway(true);
setError(t('urlContentPreview.pdfLaedtLangsamBitteVerwenden')); setError(t('PDF lädt langsam, bitte verwenden'));
setIsLoading(false); setIsLoading(false);
} }
}, QUICK_TIMEOUT); }, QUICK_TIMEOUT);
@ -129,7 +129,7 @@ export function UrlContentPreview({
try { try {
new URL(url); new URL(url);
} catch (e) { } catch (e) {
setError(t('urlContentPreview.invalidUrl')); setError(t('Ungültige URL'));
setIsLoading(false); setIsLoading(false);
} }
} }
@ -314,7 +314,7 @@ export function UrlContentPreview({
<div className={styles.unsupportedContainer}> <div className={styles.unsupportedContainer}>
<div className={styles.unsupportedIcon}>📄</div> <div className={styles.unsupportedIcon}>📄</div>
<div className={styles.fileName}>{fileName}</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}> <button onClick={handleDownload} className={styles.retryButton}>
Download File Download File
</button> </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] ? typeof data.values[index] === 'object' && data.values[index] !== null && 'keys' in data.values[index] ?
renderTable(data.values[index], level + 1, rowPath) : 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> </div>

View file

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

View file

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

View file

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

View file

@ -38,9 +38,9 @@ interface CanvasHeaderProps {
function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> { function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> {
return { return {
draft: { label: t('canvasHeader.entwurf'), color: 'var(--warning-color, #ffc107)' }, draft: { label: t('Entwurf'), color: 'var(--warning-color, #ffc107)' },
published: { label: t('canvasHeader.veroeffentlicht'), color: 'var(--success-color, #28a745)' }, published: { label: t('Veröffentlicht'), color: 'var(--success-color, #28a745)' },
archived: { label: t('canvasHeader.archiviert'), color: 'var(--text-secondary, #666)' }, archived: { label: t('Archiviert'), color: 'var(--text-secondary, #666)' },
}; };
} }
@ -153,7 +153,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
<button <button
type="button" type="button"
className={styles.canvasGearBtn} className={styles.canvasGearBtn}
title={t('canvasHeader.workflowkonfigurationEinstiegStarts')} title={t('Workflowkonfiguration Einstieg/Starts')}
aria-label="Workflow-Konfiguration" aria-label="Workflow-Konfiguration"
onClick={onWorkflowSettings} onClick={onWorkflowSettings}
> >
@ -172,7 +172,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton} className={styles.retryButton}
onClick={() => setNewMenuOpen((p) => !p)} onClick={() => setNewMenuOpen((p) => !p)}
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, paddingLeft: 4, paddingRight: 6, borderLeft: '1px solid rgba(0,0,0,0.15)' }} 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' }} /> <FaCaretDown style={{ fontSize: '0.7rem' }} />
</button> </button>
@ -216,9 +216,9 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton} className={styles.retryButton}
onClick={() => setTemplateMenuOpen((p) => !p)} onClick={() => setTemplateMenuOpen((p) => !p)}
disabled={templateSaving} 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> </button>
{templateMenuOpen && ( {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 }}> <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 }} style={{ padding: '0.4rem', minWidth: 180 }}
> >
<option value="">{t('canvasHeader.workflowLaden')}</option> <option value="">{t('Workflow laden')}</option>
{workflows.map((w) => ( {workflows.map((w) => (
<option key={w.id} value={w.id}> <option key={w.id} value={w.id}>
{w.label} {w.label}
@ -270,7 +270,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
)} )}
</button> </button>
{onToggleChat && ( {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' }} /> <FaDatabase style={{ marginRight: '0.4rem' }} />
Workspace Workspace
</button> </button>
@ -287,7 +287,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
style={{ padding: '0.3rem', minWidth: 140, fontSize: '0.85rem' }} style={{ padding: '0.3rem', minWidth: 140, fontSize: '0.85rem' }}
disabled={versionLoading} disabled={versionLoading}
> >
<option value="">{t('canvasHeader.aktuelle')}</option> <option value="">{t('Aktuelle')}</option>
{versions.map((v) => ( {versions.map((v) => (
<option key={v.id} value={v.id}> <option key={v.id} value={v.id}>
v{v.versionNumber} ({statusBadge[v.status]?.label ?? v.status}) v{v.versionNumber} ({statusBadge[v.status]?.label ?? v.status})
@ -312,7 +312,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton} className={styles.retryButton}
onClick={() => onPublishVersion(currentVersion.id)} onClick={() => onPublishVersion(currentVersion.id)}
disabled={versionLoading} disabled={versionLoading}
title={t('canvasHeader.versionVeroeffentlichen')} title={t('Version veröffentlichen')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }} style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
> >
<FaCloudUploadAlt style={{ marginRight: 4 }} /> <FaCloudUploadAlt style={{ marginRight: 4 }} />
@ -325,7 +325,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton} className={styles.retryButton}
onClick={() => onUnpublishVersion(currentVersion.id)} onClick={() => onUnpublishVersion(currentVersion.id)}
disabled={versionLoading} disabled={versionLoading}
title={t('canvasHeader.veroeffentlichungZuruecknehmen')} title={t('Veröffentlichung zurücknehmen')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }} style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
> >
<FaCloudDownloadAlt style={{ marginRight: 4 }} /> <FaCloudDownloadAlt style={{ marginRight: 4 }} />
@ -338,7 +338,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton} className={styles.retryButton}
onClick={() => onArchiveVersion(currentVersion.id)} onClick={() => onArchiveVersion(currentVersion.id)}
disabled={versionLoading} disabled={versionLoading}
title={t('canvasHeader.versionArchivieren')} title={t('Version archivieren')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }} style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
> >
<FaArchive style={{ marginRight: 4 }} /> <FaArchive style={{ marginRight: 4 }} />
@ -351,7 +351,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
className={styles.retryButton} className={styles.retryButton}
onClick={onCreateDraft} onClick={onCreateDraft}
disabled={versionLoading} disabled={versionLoading}
title={t('canvasHeader.neuenEntwurfErstellen')} title={t('Neuen Entwurf erstellen')}
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }} style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
> >
+ Entwurf + Entwurf
@ -381,10 +381,10 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
}} }}
> >
{executeResult.success ? ( {executeResult.success ? (
<>{t('canvasHeader.ausfuehrungAbgeschlossen')}</> <>{t('Ausführung abgeschlossen')}</>
) : (executeResult as { paused?: boolean }).paused ? ( ) : (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. Task zu bearbeiten.
</> </>
) : ( ) : (

View file

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

View file

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

View file

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

View file

@ -88,7 +88,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({ nodeTypes,
<input <input
type="text" type="text"
className={styles.sidebarSearch} className={styles.sidebarSearch}
placeholder={t('nodeSidebar.nodesDurchsuchen')} placeholder={t('Nodes durchsuchen')}
value={filter} value={filter}
onChange={(e) => onFilterChange(e.target.value)} 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>} Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
</div> </div>
{steps.length === 0 && !loading && ( {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) => { {steps.map((step: any) => {
const startStr = _formatTimestamp(step.startedAt); const startStr = _formatTimestamp(step.startedAt);
@ -222,7 +222,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
</span> </span>
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> <span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{step.retryCount > 0 && ( {step.retryCount > 0 && (
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('runTracingPanel.retryCount')}> <span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('Wiederholungsanzahl')}>
{step.retryCount}x retry {step.retryCount}x retry
</span> </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, …). */ /** Vier Einstiege; bei „Immer aktiv“ folgt später die Listener-Konfiguration (E-Mail, Webhook, …). */
function _getKindOptions(t: (key: string) => string): { value: string; label: string }[] { function _getKindOptions(t: (key: string) => string): { value: string; label: string }[] {
return [ return [
{ value: 'manual', label: t('workflowConfigurationModal.manuellerTrigger') }, { value: 'manual', label: t('Manueller Trigger') },
{ value: 'form', label: t('workflowConfigurationModal.formular') }, { value: 'form', label: t('Formular') },
{ value: 'schedule', label: t('workflowConfigurationModal.zeitplan') }, { value: 'schedule', label: t('Zeitplan') },
{ value: 'always_on', label: t('workflowConfigurationModal.immerAktiv') }, { value: 'always_on', label: t('Immer aktiv') },
]; ];
} }
@ -89,7 +89,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
className={styles.workflowModalInput} className={styles.workflowModalInput}
value={titleDe} value={titleDe}
onChange={(e) => setTitleDe(e.target.value)} 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"> <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}> <div className={styles.formFieldRowHeader}>
<span <span
className={styles.formFieldDragHandle} className={styles.formFieldDragHandle}
title={t('formNodeConfig.zumVerschiebenZiehen')} title={t('Zum Verschieben ziehen')}
draggable draggable
onDragStart={(e) => { onDragStart={(e) => {
e.dataTransfer.setData('text/plain', String(i)); 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="number">Number</option>
<option value="date">Date</option> <option value="date">Date</option>
<option value="boolean">Checkbox</option> <option value="boolean">Checkbox</option>
<option value="clickup_tasks">{t('formNodeConfig.clickupaufgabeReferenz')}</option> <option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
<option value="clickup_status">{t('formNodeConfig.clickupstatusListe')}</option> <option value="clickup_status">{t('ClickUp-Status Liste')}</option>
</select> </select>
<label className={styles.formFieldRequiredLabel}> <label className={styles.formFieldRequiredLabel}>
<input <input
@ -151,7 +151,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<button <button
type="button" type="button"
onClick={() => removeField(i)} onClick={() => removeField(i)}
title={t('formNodeConfig.feldEntfernen')} title={t('Feld entfernen')}
className={styles.formFieldRemoveButton} className={styles.formFieldRemoveButton}
> >
<FaTimes /> <FaTimes />
@ -198,7 +198,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
Listen-ID (verknüpfte Liste / Ziel-Liste) Listen-ID (verknüpfte Liste / Ziel-Liste)
</label> </label>
<input <input
placeholder={t('formNodeConfig.zBAusClickupurlList123456789')} placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
value={f.clickupListId ?? ''} value={f.clickupListId ?? ''}
onChange={(e) => { onChange={(e) => {
const next = [...fields]; const next = [...fields];

View file

@ -170,7 +170,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc' }} 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) => ( {connections.map((c) => (
<option key={c.id} value={c.id}>{c.label}</option> <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 }}> <div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}> <select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
<option value="eq">equals</option> <option value="eq">equals</option>
<option value="neq">{t('index.notEquals')}</option> <option value="neq">{t('ungleich')}</option>
<option value="contains">contains</option> <option value="contains">contains</option>
<option value="gt">{t('index.greaterThan')}</option> <option value="gt">{t('größer als')}</option>
<option value="lt">{t('index.lessThan')}</option> <option value="lt">{t('kleiner als')}</option>
</select> </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' }} /> <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> <button onClick={() => removeCase(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div> </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> </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> <button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div> </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> </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> <button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div> </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> </div>
); );
}; };
@ -302,7 +302,7 @@ const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) =
placeholder={t('index.5')} placeholder={t('index.5')}
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }} style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }}
/> />
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('index.cronMinHourDayMonth')}</p> <p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('Cron: Min Stunde Tag Monat')}</p>
</div> </div>
); );
}; };
@ -317,14 +317,14 @@ const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange
<div style={{ display: 'flex', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}> <select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
<option value="eq">equals</option> <option value="eq">equals</option>
<option value="neq">{t('index.notEquals')}</option> <option value="neq">{t('ungleich')}</option>
<option value="gt">{t('index.greaterThan')}</option> <option value="gt">{t('größer als')}</option>
<option value="lt">{t('index.lessThan')}</option> <option value="lt">{t('kleiner als')}</option>
<option value="contains">contains</option> <option value="contains">contains</option>
<option value="empty">{t('index.isEmpty')}</option> <option value="empty">{t('ist leer')}</option>
<option value="not_empty">{t('index.isNotEmpty')}</option> <option value="not_empty">{t('ist nicht leer')}</option>
<option value="is_true">{t('index.isTrue')}</option> <option value="is_true">{t('ist wahr')}</option>
<option value="is_false">{t('index.isFalse')}</option> <option value="is_false">{t('ist falsch')}</option>
</select> </select>
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> <input type="text" placeholder="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> </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> <label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
{mappings.map((m: Record<string, unknown>, i: number) => ( {mappings.map((m: Record<string, unknown>, i: number) => (
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}> <div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<input type="text" placeholder={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> <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> <button onClick={() => removeMapping(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
</div> </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> </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' }} /> <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' }}> <select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
<option value="eq">equals</option> <option value="eq">equals</option>
<option value="neq">{t('index.notEquals')}</option> <option value="neq">{t('ungleich')}</option>
<option value="contains">contains</option> <option value="contains">contains</option>
<option value="startsWith">{t('index.startsWith')}</option> <option value="startsWith">{t('beginnt mit')}</option>
<option value="isEmpty">{t('index.isEmpty')}</option> <option value="isEmpty">{t('ist leer')}</option>
<option value="isNotEmpty">{t('index.isNotEmpty')}</option> <option value="isNotEmpty">{t('ist nicht leer')}</option>
<option value="gt">{t('index.greaterThan')}</option> <option value="gt">{t('größer als')}</option>
<option value="lt">{t('index.lessThan')}</option> <option value="lt">{t('kleiner als')}</option>
</select> </select>
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} /> <input type="text" placeholder="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> </div>

View file

@ -100,7 +100,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
<div className={styles.ifElseConditionEditor}> <div className={styles.ifElseConditionEditor}>
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>Datenquelle</label> <label>Datenquelle</label>
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('ifElseNodeConfig.formularfeldWaehlen')} /> <RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
</div> </div>
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>Vergleich</label> <label>Vergleich</label>
@ -120,7 +120,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
value={String(value ?? '')} value={String(value ?? '')}
onChange={(e) => handleValueChange(e.target.value)} onChange={(e) => handleValueChange(e.target.value)}
> >
<option value="">{t('ifElseNodeConfig.mimetypeWaehlen')}</option> <option value="">{t('MIME-Typ wählen')}</option>
{mimeTypeOptions.map((o) => ( {mimeTypeOptions.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
{o.label} ({o.value}) {o.label} ({o.value})
@ -142,8 +142,8 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
: fieldType === 'date' : fieldType === 'date'
? 'TT.MM.JJJJ' ? 'TT.MM.JJJJ'
: isMimeTypeRef : isMimeTypeRef
? t('ifElseNodeConfig.zbApplicationpdf') ? t('z.B. application/pdf')
: t('ifElseNodeConfig.zbCh') : 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.dataPickerOverlay} onClick={onClose}>
<div className={styles.dataPickerModal} onClick={(e) => e.stopPropagation()}> <div className={styles.dataPickerModal} onClick={(e) => e.stopPropagation()}>
<div className={styles.dataPickerHeader}> <div className={styles.dataPickerHeader}>
<h4 className={styles.dataPickerTitle}>{t('dataPicker.datenquelleWaehlen')}</h4> <h4 className={styles.dataPickerTitle}>{t('Datenquelle wählen')}</h4>
<button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label={t('dataPicker.schliessen')}> <button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label={t('Schließen')}>
× ×
</button> </button>
</div> </div>
@ -187,7 +187,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
return node?.type !== 'trigger.manual'; return node?.type !== 'trigger.manual';
}); });
if (filteredIds.length === 0 && Object.keys(systemVars).length === 0) { 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) => { return filteredIds.map((nodeId) => {
const node = nodes.find((n) => n.id === nodeId); const node = nodes.find((n) => n.id === nodeId);

View file

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

View file

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

View file

@ -184,7 +184,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
return ( return (
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>{t('loopItemsSelect.datenquelleFuerIteration')}</label> <label>{t('Datenquelle für Iteration')}</label>
<select <select
value={currentValue} value={currentValue}
onChange={(e) => { 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[] { function _parseFields(params: Record<string, unknown>, t: (key: string) => string): FormField[] {
const raw = params.formFields; 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) => { return raw.map((f, i) => {
if (f && typeof f === 'object' && !Array.isArray(f)) { if (f && typeof f === 'object' && !Array.isArray(f)) {
const o = f as Record<string, unknown>; const o = f as Record<string, unknown>;
@ -62,7 +62,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<div key={idx} className={styles.formFieldRow}> <div key={idx} className={styles.formFieldRow}>
<input <input
className={styles.startsInput} className={styles.startsInput}
placeholder={t('formStartNodeConfig.namePayloadkey')} placeholder={t('Name (Payload-Key)')}
value={f.name} value={f.name}
onChange={(e) => { onChange={(e) => {
const next = [...fields]; const next = [...fields];
@ -99,7 +99,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
<option value="email">E-Mail</option> <option value="email">E-Mail</option>
<option value="date">Datum</option> <option value="date">Datum</option>
<option value="boolean">Ja/Nein</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> </select>
<button <button
type="button" type="button"
@ -114,7 +114,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
type="button" type="button"
className={styles.startsAddBtn} className={styles.startsAddBtn}
onClick={() => 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 + 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 }[] { function _getModeOptions(t: (key: string) => string): { value: ScheduleMode; title: string; subtitle: string }[] {
return [ return [
{ value: 'daily', title: t('scheduleStartNodeConfig.taeglich'), subtitle: t('scheduleStartNodeConfig.jedenTagZurGleichenZeit') }, { value: 'daily', title: t('Täglich'), subtitle: t('Jeden Tag zur gleichen Zeit') },
{ value: 'weekdays', title: t('scheduleStartNodeConfig.werktage'), subtitle: t('scheduleStartNodeConfig.montagBisFreitag') }, { value: 'weekdays', title: t('Werktage'), subtitle: t('Montag bis Freitag') },
{ value: 'weekly', title: t('scheduleStartNodeConfig.bestimmteTage'), subtitle: t('scheduleStartNodeConfig.wochentageAuswaehlen') }, { value: 'weekly', title: t('Bestimmte Tage'), subtitle: t('Wochentage auswählen') },
{ value: 'calendar', title: t('scheduleStartNodeConfig.einAndererZeitraum'), subtitle: t('scheduleStartNodeConfig.monatlichOderJaehrlich') }, { value: 'calendar', title: t('Ein anderer Zeitraum'), subtitle: t('Monatlich oder jährlich') },
{ value: 'interval', title: t('scheduleStartNodeConfig.intervall'), subtitle: t('scheduleStartNodeConfig.inRegelmaessigenAbstaenden') }, { 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 }[] { function _getIntervalUnits(t: (key: string) => string): { value: IntervalUnit; label: string; title: string }[] {
return [ return [
{ value: 'seconds', label: t('scheduleStartNodeConfig.sek'), title: t('scheduleStartNodeConfig.sekunden') }, { value: 'seconds', label: t('Sekunde'), title: t('Sekunden') },
{ value: 'minutes', label: t('scheduleStartNodeConfig.min'), title: t('scheduleStartNodeConfig.minuten') }, { value: 'minutes', label: t('Minute'), title: t('Minuten') },
{ value: 'hours', label: t('scheduleStartNodeConfig.h'), title: t('scheduleStartNodeConfig.stunden') }, { value: 'hours', label: t('Stunde'), title: t('Stunden') },
{ value: 'days', label: t('scheduleStartNodeConfig.d'), title: t('scheduleStartNodeConfig.tage') }, { value: 'days', label: t('Tag'), title: t('Tage') },
{ value: 'years', label: t('scheduleStartNodeConfig.a'), title: t('scheduleStartNodeConfig.jahre') }, { value: 'years', label: t('Jahr'), title: t('Jahre') },
]; ];
} }
@ -403,7 +403,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
{o.value === 'interval' && ( {o.value === 'interval' && (
<div className={styles.scheduleIntervalRow}> <div className={styles.scheduleIntervalRow}>
<span className={styles.scheduleFieldLabel}>{t('scheduleStartNodeConfig.alle')}</span> <span className={styles.scheduleFieldLabel}>{t('Alle')}</span>
<input <input
type="number" type="number"
min={1} min={1}

View file

@ -114,7 +114,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
onChange={(e) => handleCaseValueChange(index, e.target.value)} onChange={(e) => handleCaseValueChange(index, e.target.value)}
className={styles.startsInput} className={styles.startsInput}
> >
<option value="">{t('switchNodeConfig.mimetypeWaehlen')}</option> <option value="">{t('MIME-Typ wählen')}</option>
{mimeTypeOptions.map((o) => ( {mimeTypeOptions.map((o) => (
<option key={o.value} value={o.value}> <option key={o.value} value={o.value}>
{o.label} ({o.value}) {o.label} ({o.value})
@ -154,9 +154,9 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
}} }}
className={styles.startsInput} className={styles.startsInput}
> >
<option value="">{t('switchNodeConfig.waehlen')}</option> <option value="">{t('hlen')}</option>
<option value="true">{t('switchNodeConfig.jaWahr')}</option> <option value="true">{t('Ja (true)')}</option>
<option value="false">{t('switchNodeConfig.neinFalsch')}</option> <option value="false">{t('Nein (false)')}</option>
</select> </select>
); );
} }
@ -189,24 +189,24 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
<RefSourceSelect <RefSourceSelect
value={ref} value={ref}
onChange={handleRefChange} onChange={handleRefChange}
placeholder={t('switchNodeConfig.feldZumVergleichenWaehlen')} placeholder={t('Feld zum Vergleich wählen')}
/> />
</div> </div>
{!ref && ( {!ref && (
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>{t('switchNodeConfig.festerWertFallsKeineReferenz')}</label> <label>{t('Fester Wert (ohne Referenz)')}</label>
<input <input
type="text" type="text"
value={String(staticValue ?? '')} value={String(staticValue ?? '')}
onChange={(e) => handleStaticValueChange(e.target.value)} onChange={(e) => handleStaticValueChange(e.target.value)}
placeholder={t('switchNodeConfig.zbChOder42')} placeholder={t('z. B. CH oder 42')}
/> />
</div> </div>
)} )}
<div className={styles.ifElseConditionRow}> <div className={styles.ifElseConditionRow}>
<label>{t('switchNodeConfig.faelleReihenfolgeAusgang')}</label> <label>{t('Fälle / Reihenfolge / Ausgabe')}</label>
<div className={styles.formFieldsList}> <div className={styles.formFieldsList}>
{cases.map((c, i) => { {cases.map((c, i) => {
const opDef = operators.find((o) => o.value === c.operator) ?? operators[0]; 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> </button>
)} )}
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && ( {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 /> <FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span> <span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
</button> </button>
@ -278,7 +278,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
</> </>
) : ( ) : (
(sel.onDeleteFile || sel.onDeleteFiles) && ( (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 /> <FaTrash />
</button> </button>
) )
@ -311,7 +311,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
e.stopPropagation(); e.stopPropagation();
sel.onNeutralizeToggle?.(file.id, !file.neutralize); 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 }} style={{ fontSize: 14, opacity: file.neutralize ? 1 : 0.4 }}
> >
{'\uD83D\uDD12'} {'\uD83D\uDD12'}
@ -381,7 +381,7 @@ function _TreeNode({
const _handleAdd = useCallback(async (e: React.MouseEvent) => { const _handleAdd = useCallback(async (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!onCreateFolder) return; if (!onCreateFolder) return;
const name = await promptFolderName('Neuer Ordnername:', { title: t('folderTree.newFolder'), placeholder: 'Ordnername' }); const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
if (name?.trim()) { if (name?.trim()) {
await onCreateFolder(name.trim(), node.id); await onCreateFolder(name.trim(), node.id);
if (!expandedIds.has(node.id)) onToggle(node.id); if (!expandedIds.has(node.id)) onToggle(node.id);
@ -500,37 +500,37 @@ function _TreeNode({
)} )}
<span className={styles.actions}> <span className={styles.actions}>
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( {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 /> <FaDownload />
</button> </button>
)} )}
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( {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 /> <FaPlus />
</button> </button>
)} )}
{onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && ( {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 /> <FaPen />
</button> </button>
)} )}
{isMultiSelected && sel.selectedItemIds.size > 1 ? ( {isMultiSelected && sel.selectedItemIds.size > 1 ? (
<> <>
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && ( {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 /> <FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span> <span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
</button> </button>
)} )}
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && ( {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 /> <FaTrash />
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span> <span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
</button> </button>
)} )}
</> </>
) : onDeleteFolder && ( ) : 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 /> <FaTrash />
</button> </button>
)} )}
@ -756,7 +756,7 @@ export default function FolderTree({
<span className={`${styles.folderName} ${styles.rootLabel}`}>(Global)</span> <span className={`${styles.folderName} ${styles.rootLabel}`}>(Global)</span>
<span className={styles.rootActions}> <span className={styles.rootActions}>
{onRefresh && ( {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 /> <FaSyncAlt />
</button> </button>
)} )}
@ -765,10 +765,10 @@ export default function FolderTree({
className={styles.actionBtn} className={styles.actionBtn}
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
const name = await promptFolderName('Neuer Ordnername:', { title: t('folderTree.newFolder'), placeholder: 'Ordnername' }); const name = await promptFolderName('Neuer Ordnername:', { title: t('Neuer Ordner'), placeholder: 'Ordnername' });
if (name?.trim()) await onCreateFolder(name.trim(), null); if (name?.trim()) await onCreateFolder(name.trim(), null);
}} }}
title={t('folderTree.neuerOrdner')} title={t('Neuer Ordner')}
> >
<FaPlus /> <FaPlus />
</button> </button>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -42,7 +42,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
<div className={styles.bauvorschriftenGrid}> <div className={styles.bauvorschriftenGrid}>
{bauvorschriften.ausnuetzungsziffer !== undefined && bauvorschriften.ausnuetzungsziffer !== null && ( {bauvorschriften.ausnuetzungsziffer !== undefined && bauvorschriften.ausnuetzungsziffer !== null && (
<div className={styles.bauvorschriftItem}> <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> <span className={styles.value}>{bauvorschriften.ausnuetzungsziffer}%</span>
</div> </div>
)} )}
@ -54,7 +54,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
)} )}
{bauvorschriften.gebaeudelaengeMax !== undefined && bauvorschriften.gebaeudelaengeMax !== null && ( {bauvorschriften.gebaeudelaengeMax !== undefined && bauvorschriften.gebaeudelaengeMax !== null && (
<div className={styles.bauvorschriftItem}> <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> <span className={styles.value}>{bauvorschriften.gebaeudelaengeMax} m</span>
</div> </div>
)} )}
@ -66,19 +66,19 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
)} )}
{bauvorschriften.mehrlaengenzuschlag && ( {bauvorschriften.mehrlaengenzuschlag && (
<div className={styles.bauvorschriftItem}> <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> <span className={styles.value}>{bauvorschriften.mehrlaengenzuschlag}</span>
</div> </div>
)} )}
{bauvorschriften.hoechstmassMax !== undefined && bauvorschriften.hoechstmassMax !== null && ( {bauvorschriften.hoechstmassMax !== undefined && bauvorschriften.hoechstmassMax !== null && (
<div className={styles.bauvorschriftItem}> <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> <span className={styles.value}>{bauvorschriften.hoechstmassMax} m</span>
</div> </div>
)} )}
{bauvorschriften.fassadenhoehe && ( {bauvorschriften.fassadenhoehe && (
<div className={styles.bauvorschriftItem}> <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> <span className={styles.value}>{bauvorschriften.fassadenhoehe}</span>
</div> </div>
)} )}

View file

@ -184,7 +184,7 @@ export function ConnectedFilesList({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <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> <span className={styles.count}>({allFiles.length})</span>
</div> </div>
<div className={styles.fileList}> <div className={styles.fileList}>
@ -231,14 +231,14 @@ export function ConnectedFilesList({
cursor: onAttach ? 'pointer' : 'default', cursor: onAttach ? 'pointer' : 'default',
userSelect: 'none' // Prevent text selection on click 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.fileInfo}>
<div className={styles.fileName} title={file.fileName}> <div className={styles.fileName} title={file.fileName}>
{file.fileName} {file.fileName}
{onAttach && ( {onAttach && (
<span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: '#666' }}> <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> </span>
)} )}
</div> </div>
@ -248,7 +248,7 @@ export function ConnectedFilesList({
</span> </span>
{file.source && ( {file.source && (
<span className={styles.fileSource}> <span className={styles.fileSource}>
{file.source === 'user_uploaded' ? t('connectedFilesList.uploaded') : t('connectedFilesList.aiCreated')} {file.source === 'user_uploaded' ? t('Hochgeladen') : t('KI erstellt')}
</span> </span>
)} )}
{isPendingFile && ( {isPendingFile && (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -853,14 +853,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
fontSize: 13, padding: '0 2px', lineHeight: 1, fontSize: 13, padding: '0 2px', lineHeight: 1,
opacity: ds.neutralize ? 1 : 0.35, 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'} {'\uD83D\uDD12'}
</button> </button>
<button <button
onClick={() => _removeDatasource(ds.id)} onClick={() => _removeDatasource(ds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.entfernen')} title={t('Entfernen')}
> >
{'\u2715'} {'\u2715'}
</button> </button>
@ -956,7 +956,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button <button
onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }} onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }} 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'} {'\u2715'}
</button> </button>
@ -986,14 +986,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button <button
onClick={() => _toggleFeatureNeutralize(fds)} onClick={() => _toggleFeatureNeutralize(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }} 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'} {'\uD83D\uDD12'}
</button> </button>
<button <button
onClick={() => _removeFeatureDataSource(fds.id)} onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.remove')} title={t('Entfernen')}
> >
{'\u2715'} {'\u2715'}
</button> </button>
@ -1029,14 +1029,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
<button <button
onClick={() => _toggleFeatureNeutralize(fds)} onClick={() => _toggleFeatureNeutralize(fds)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }} 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'} {'\uD83D\uDD12'}
</button> </button>
<button <button
onClick={() => _removeFeatureDataSource(fds.id)} onClick={() => _removeFeatureDataSource(fds.id)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
title={t('sourcesTab.entfernen')} title={t('Entfernen')}
> >
{'\u2715'} {'\u2715'}
</button> </button>
@ -1167,13 +1167,13 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
opacity: isAdding ? 0.5 : 1, opacity: isAdding ? 0.5 : 1,
flexShrink: 0, flexShrink: 0,
}} }}
title={t('sourcesTab.addAsDataSource')} title={t('Als Datenquelle hinzufügen')}
> >
{isAdding ? '...' : '+ Add'} {isAdding ? '...' : '+ Add'}
</button> </button>
)} )}
{canAdd && alreadyAdded && ( {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'} {'\u2713'}
</span> </span>
)} )}
@ -1431,13 +1431,13 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
fontSize: 10, color: '#7b1fa2', padding: '1px 5px', fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
opacity: isAdding ? 0.5 : 1, flexShrink: 0, opacity: isAdding ? 0.5 : 1, flexShrink: 0,
}} }}
title={t('sourcesTab.addAsFeatureDataSource')} title={t('Als Feature-Datenquelle hinzufügen')}
> >
{isAdding ? '...' : '+ Add'} {isAdding ? '...' : '+ Add'}
</button> </button>
)} )}
{isAdded && ( {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'} {'\u2713'}
</span> </span>
)} )}
@ -1578,13 +1578,13 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
fontSize: 10, color: '#7b1fa2', padding: '1px 5px', fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
opacity: isAdding ? 0.5 : 1, flexShrink: 0, opacity: isAdding ? 0.5 : 1, flexShrink: 0,
}} }}
title={t('sourcesTab.addAllTablesForThis')} title={t('Alle Tabellen für diese Quelle hinzufügen')}
> >
{isAdding ? '...' : '+ Add'} {isAdding ? '...' : '+ Add'}
</button> </button>
)} )}
{isAdded && ( {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'} {'\u2713'}
</span> </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 }[] { export function _getAccessLevelOptions(t: (key: string) => string): { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] {
return [ return [
{ value: 'n', label: t('useAccessRules.keine'), color: '#e53e3e' }, { value: 'n', label: t('Keine'), color: '#e53e3e' },
{ value: 'm', label: t('useAccessRules.eigene'), color: '#d69e2e' }, { value: 'm', label: t('Eigene'), color: '#d69e2e' },
{ value: 'g', label: t('useAccessRules.gruppe'), color: '#3182ce' }, { value: 'g', label: t('Gruppe'), color: '#3182ce' },
{ value: 'a', label: t('useAccessRules.alle'), color: '#38a169' }, { 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 resolveRef = useRef<((v: boolean) => void) | null>(null);
const _defaults: Required<ConfirmOptions> = useMemo(() => ({ const _defaults: Required<ConfirmOptions> = useMemo(() => ({
title: t('confirm.title'), title: t('Bestätigung'),
confirmLabel: t('confirm.confirmLabel'), confirmLabel: t('Bestätigen'),
cancelLabel: t('confirm.cancelLabel'), cancelLabel: t('Abbrechen'),
variant: 'primary' as const, variant: 'primary' as const,
}), [t]); }), [t]);

View file

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

View file

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

View file

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

View file

@ -234,7 +234,7 @@ export const AutomationsDashboardPage: React.FC = () => {
{metrics?.runsByStatus && Object.keys(metrics.runsByStatus).length > 0 && ( {metrics?.runsByStatus && Object.keys(metrics.runsByStatus).length > 0 && (
<div style={{ marginBottom: 24 }}> <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' }}> <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{Object.entries(metrics.runsByStatus).map(([status, count]) => ( {Object.entries(metrics.runsByStatus).map(([status, count]) => (
<span <span

View file

@ -72,8 +72,8 @@ export const DashboardPage: React.FC = () => {
return ( return (
<div className={styles.dashboard}> <div className={styles.dashboard}>
<header className={styles.header}> <header className={styles.header}>
<h1>{t('dashboard.uebersicht')}</h1> <h1>{t('Übersicht')}</h1>
<p className={styles.subtitle}>{t('dashboard.lade')}</p> <p className={styles.subtitle}>{t('Lade')}</p>
</header> </header>
</div> </div>
); );
@ -82,7 +82,7 @@ export const DashboardPage: React.FC = () => {
return ( return (
<div className={styles.dashboard}> <div className={styles.dashboard}>
<header className={styles.header}> <header className={styles.header}>
<h1>{t('dashboard.uebersicht')}</h1> <h1>{t('Übersicht')}</h1>
{totalInstances > 0 && ( {totalInstances > 0 && (
<p className={styles.subtitle}> <p className={styles.subtitle}>
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}. 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 ChatworkflowDashboard: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
return ( 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 ChatworkflowRuns: React.FC = () => {
const { t } = useLanguage(); 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 ChatworkflowFiles: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
return <PlaceholderView title={t('feature.dateien')} description="Workflow-Dateien" />; return <PlaceholderView title={t('Dateien')} description="Workflow-Dateien" />;
}; };
// Chatbot Views // Chatbot Views
@ -87,7 +87,7 @@ const ChatworkflowFiles: React.FC = () => {
const ChatbotSettings: React.FC = () => { const ChatbotSettings: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
return ( 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(); const { t } = useLanguage();
return ( return (
<div className={styles.notFound}> <div className={styles.notFound}>
<h2>{t('feature.seiteNichtGefunden')}</h2> <h2>{t('Seite nicht gefunden')}</h2>
<p>{t('feature.dieseViewExistiertNichtOder')}</p> <p>{t('Diese Ansicht existiert nicht oder')}</p>
</div> </div>
); );
}; };
@ -106,8 +106,8 @@ const AccessDenied: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
return ( return (
<div className={styles.accessDenied}> <div className={styles.accessDenied}>
<h2>{t('feature.zugriffVerweigert')}</h2> <h2>{t('Zugriff verweigert')}</h2>
<p>{t('feature.duHastKeineBerechtigungFuer')}</p> <p>{t('Du hast keine Berechtigung für')}</p>
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -107,7 +107,7 @@ function Reset() {
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <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.loginForm}>
<div className={styles.error}>{tokenError}</div> <div className={styles.error}>{tokenError}</div>
<div className={styles.registerLink}> <div className={styles.registerLink}>
@ -119,7 +119,7 @@ function Reset() {
</button> </button>
</div> </div>
<div className={styles.registerLink}> <div className={styles.registerLink}>
<span>{t('reset.oderZurueckZum')}</span> <span>{t('Oder zurück zum')}</span>
<button <button
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/login")} onClick={() => navigate("/login")}
@ -147,7 +147,7 @@ function Reset() {
</div> </div>
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <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.loginForm}>
{(validationError || error) && ( {(validationError || error) && (
<div className={styles.error}>{validationError || error}</div> <div className={styles.error}>{validationError || error}</div>
@ -159,7 +159,7 @@ function Reset() {
{!successMessage && ( {!successMessage && (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className={styles.passwordHint}>{t('reset.mindestens8Zeichen')}</div> <div className={styles.passwordHint}>{t('Mindestens 8 Zeichen')}</div>
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
<input <input
type="password" type="password"
@ -174,7 +174,7 @@ function Reset() {
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`} className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
autoComplete="new-password" 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> </div>
@ -192,7 +192,7 @@ function Reset() {
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`} className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
autoComplete="new-password" 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> </div>
<button <button
@ -206,7 +206,7 @@ function Reset() {
)} )}
<div className={styles.registerLink}> <div className={styles.registerLink}>
<span>{t('reset.zurueckZum')}</span> <span>{t('Zurück zum')}</span>
<button <button
className={styles.textButton} className={styles.textButton}
onClick={() => navigate("/login")} 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 }[] { function _getTabs(t: (key: string) => string): { key: SettingsTab; label: string }[] {
return [ return [
{ key: 'profile', label: t('settings.tabProfil') }, { key: 'profile', label: t('Tab Profil') },
{ key: 'appearance', label: t('settings.tabDarstellung') }, { key: 'appearance', label: t('Tab Darstellung') },
{ key: 'voice', label: t('settings.tabStimmeSprache') }, { key: 'voice', label: t('Tab Stimme & Sprache') },
{ key: 'neutralization', label: t('settings.tabNeutralisierung') }, { key: 'neutralization', label: t('Tab Neutralisierung') },
{ key: 'privacy', label: t('settings.tabDatenschutz') }, { 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 languageOptions = availableLanguages.map((l) => ({ value: l.code, label: l.label || l.code }));
const profileAttributes: AttributeDefinition[] = [ const profileAttributes: AttributeDefinition[] = [
{ name: 'fullName', type: 'string', label: t('settings.vollstaendigerName'), description: t('settings.ihrVollstaendigerName'), required: false, placeholder: t('settings.placeholderName') }, { 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('settings.emailAdresse'), description: t('settings.emailBeschreibung'), required: true, placeholder: t('settings.placeholderEmail') }, { 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('settings.sprache'), description: t('settings.anzeigespracheDerAnwendung'), required: true, options: languageOptions }, { name: 'language', type: 'select', label: t('Sprache'), description: t('Anzeigesprache der Anwendung'), required: true, options: languageOptions },
]; ];
const handleSubmit = async (formData: any) => { 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.modalOverlay} onClick={onClose}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}> <div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2>{t('settings.profilBearbeiten')}</h2> <h2>{t('Profil bearbeiten')}</h2>
<button className={styles.closeButton} onClick={onClose}>&times;</button> <button className={styles.closeButton} onClick={onClose}>&times;</button>
</div> </div>
<div className={styles.modalBody}> <div className={styles.modalBody}>
{error && <div className={styles.errorMessage}>{error}</div>} {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> </div>
</div> </div>
@ -177,7 +177,7 @@ const VoiceSettingsTab: React.FC = () => {
method: 'put', method: 'put',
data: { sttLanguage, ttsLanguage: sttLanguage, ttsVoiceMap: mapObj }, data: { sttLanguage, ttsLanguage: sttLanguage, ttsVoiceMap: mapObj },
}); });
setSuccess(t('settings.einstellungenGespeichert')); setSuccess(t('Einstellungen gespeichert'));
setTimeout(() => setSuccess(null), 3000); setTimeout(() => setSuccess(null), 3000);
await _loadSettings(); await _loadSettings();
} catch (err: any) { } catch (err: any) {
@ -199,7 +199,7 @@ const VoiceSettingsTab: React.FC = () => {
const audio = new Audio(`data:audio/mp3;base64,${result.audio}`); const audio = new Audio(`data:audio/mp3;base64,${result.audio}`);
audio.play(); audio.play();
} }
} catch { setError(t('settings.stimmtestFehlgeschlagen')); } } catch { setError(t('Stimmtest fehlgeschlagen')); }
finally { setTesting(null); } finally { setTesting(null); }
}, [request]); }, [request]);
@ -215,7 +215,7 @@ const VoiceSettingsTab: React.FC = () => {
]; ];
const _displayLanguages = languages.length > 0 ? languages : _defaultLangs; 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 ( 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>} {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}> <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.settingRow}>
<div className={styles.settingInfo}> <div className={styles.settingInfo}>
<label className={styles.settingLabel}>{t('settings.spracheFuerSpracherkennung')}</label> <label className={styles.settingLabel}>{t('Sprache für Spracherkennung')}</label>
<p className={styles.settingDescription}>{t('settings.wirdFuerDieSprachezutexterkennungVerwendet')}</p> <p className={styles.settingDescription}>{t('Wird für Sprach- und Texterkennung verwendet')}</p>
</div> </div>
<div className={styles.settingControl}> <div className={styles.settingControl}>
<select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}> <select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}>
@ -240,7 +240,7 @@ const VoiceSettingsTab: React.FC = () => {
</section> </section>
<section className={styles.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' }}> <p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
Die Sprache wird automatisch erkannt. Hier kann pro Sprache eine bevorzugte Stimme festgelegt werden. Die Sprache wird automatisch erkannt. Hier kann pro Sprache eine bevorzugte Stimme festgelegt werden.
</p> </p>
@ -251,7 +251,7 @@ const VoiceSettingsTab: React.FC = () => {
</div> </div>
) : ( ) : (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}> <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> <tbody>
{voiceMap.map(entry => ( {voiceMap.map(entry => (
<tr key={entry.language} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}> <tr key={entry.language} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
@ -263,7 +263,7 @@ const VoiceSettingsTab: React.FC = () => {
</button> </button>
</td> </td>
<td style={{ padding: '0.5rem' }}> <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> </td>
</tr> </tr>
))} ))}
@ -273,7 +273,7 @@ const VoiceSettingsTab: React.FC = () => {
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem', alignItems: 'flex-end', flexWrap: 'wrap' }}> <div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div> <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)}> <select className={styles.select} value={addLanguage} onChange={e => setAddLanguage(e.target.value)}>
{_displayLanguages.map((lang: any) => ( {_displayLanguages.map((lang: any) => (
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option> <option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option>
@ -281,15 +281,15 @@ const VoiceSettingsTab: React.FC = () => {
</select> </select>
</div> </div>
<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}> <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) => ( {addVoices.map((v: any) => (
<option key={v.name || v} value={v.name || v}>{v.displayName || v.name || v}</option> <option key={v.name || v} value={v.name || v}>{v.displayName || v.name || v}</option>
))} ))}
</select> </select>
</div> </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' }}> <button className={styles.button} onClick={() => _handleTestVoice(addLanguage, addVoiceName)} disabled={testing !== null} style={{ padding: '0.5rem 1rem' }}>
{testing === addLanguage ? '...' : 'Testen'} {testing === addLanguage ? '...' : 'Testen'}
</button> </button>
@ -297,7 +297,7 @@ const VoiceSettingsTab: React.FC = () => {
</section> </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 }}> <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> </button>
</> </>
); );
@ -358,14 +358,14 @@ const NeutralizationMappingsTab: React.FC = () => {
return text.slice(0, 2) + '*'.repeat(Math.min(text.length - 4, 20)) + text.slice(-2); 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 ( return (
<> <>
{error && <div className={styles.errorMessage}>{error}</div>} {error && <div className={styles.errorMessage}>{error}</div>}
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('settings.platzhaltermappingsLokal')}</h2> <h2 className={styles.sectionTitle}>{t('Platzhaltermappings lokal')}</h2>
<div <div
style={{ style={{
marginBottom: '1rem', 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>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>. Seite). Dieser Tab zeigt nur die <strong>lokale</strong> Liste über <code>/api/local/neutralization-mappings</code>.
</div> </div>
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}> <p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
@ -488,8 +488,8 @@ export const SettingsPage: React.FC = () => {
return ( return (
<div className={styles.settings}> <div className={styles.settings}>
<header className={styles.header}> <header className={styles.header}>
<h1>{t('settings.einstellungen')}</h1> <h1>{t('Einstellungen')}</h1>
<p className={styles.subtitle}>{t('settings.persoenlicheEinstellungenUndPraeferenzen')}</p> <p className={styles.subtitle}>{t('Persönliche Einstellungen und Präferenzen')}</p>
</header> </header>
<nav style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--border-color, #e0e0e0)', marginBottom: '1.5rem' }}> <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' && ( {activeTab === 'profile' && (
<> <>
<section className={styles.section}> <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.settingRow}>
<div className={styles.settingInfo}> <div className={styles.settingInfo}>
<label className={styles.settingLabel}>{t('settings.profilBearbeiten')}</label> <label className={styles.settingLabel}>{t('Profil bearbeiten')}</label>
<p className={styles.settingDescription}>{t('settings.aendernSieIhrenNamenUnd')}</p> <p className={styles.settingDescription}>{t('Ändern Sie Ihren Namen und')}</p>
</div> </div>
<div className={styles.settingControl}> <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>
</div> </div>
{currentUser && ( {currentUser && (
@ -538,18 +538,18 @@ export const SettingsPage: React.FC = () => {
{activeTab === 'appearance' && ( {activeTab === 'appearance' && (
<section className={styles.section}> <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.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.settingControl}>
<div className={styles.themeToggle}> <div className={styles.themeToggle}>
<button className={`${styles.themeButton} ${theme === 'light' ? styles.active : ''}`} onClick={() => handleThemeChange('light')}>{t('settings.themeHell')}</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('settings.themeDunkel')}</button> <button className={`${styles.themeButton} ${theme === 'dark' ? styles.active : ''}`} onClick={() => handleThemeChange('dark')}>{t('Thema Dunkel')}</button>
</div> </div>
</div> </div>
</div> </div>
<div className={styles.settingRow}> <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}> <div className={styles.settingControl}>
<select className={styles.select} value={currentLanguage} onChange={(e) => handleLanguageChange(e.target.value)} disabled={isSavingLanguage}> <select className={styles.select} value={currentLanguage} onChange={(e) => handleLanguageChange(e.target.value)} disabled={isSavingLanguage}>
{availableLanguages.map((l) => ( {availableLanguages.map((l) => (
@ -558,7 +558,7 @@ export const SettingsPage: React.FC = () => {
</option> </option>
))} ))}
</select> </select>
{isSavingLanguage && <span className={styles.savingIndicator}>{t('settings.speichern')}</span>} {isSavingLanguage && <span className={styles.savingIndicator}>{t('Speichern')}</span>}
</div> </div>
</div> </div>
</section> </section>
@ -570,13 +570,13 @@ export const SettingsPage: React.FC = () => {
{activeTab === 'privacy' && ( {activeTab === 'privacy' && (
<section className={styles.section}> <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' }}> <p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
{t('settings.datenschutzBeschreibung')} {t('Datenschutzbeschreibung')}
</p> </p>
<div className={styles.settingRow}> <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.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('settings.gdprOeffnen')}</Link></div> <div className={styles.settingControl}><Link className={`${styles.button} ${styles.linkButton}`} to="/gdpr">{t('GDPR öffnen')}</Link></div>
</div> </div>
</section> </section>
)} )}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FaDownload, FaFileExport, FaFileImport, FaRedo, FaSync, FaTrash } from 'react-icons/fa'; import { FaDownload, FaFileExport, FaFileImport, FaRedo, FaSync, FaTrash } from 'react-icons/fa';
import api from '../../api'; import api from '../../api';
import axios from 'axios';
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable/FormGeneratorTable'; import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable/FormGeneratorTable';
import { useConfirm } from '../../hooks/useConfirm'; import { useConfirm } from '../../hooks/useConfirm';
import { useLanguage } from '../../providers/language/LanguageContext'; import { useLanguage } from '../../providers/language/LanguageContext';
@ -39,19 +40,30 @@ type ProgressInfo = {
function _getColumns(t: (key: string) => string): ColumnConfig[] { function _getColumns(t: (key: string) => string): ColumnConfig[] {
return [ return [
{ key: 'id', label: t('adminLanguages.code'), type: 'text', sortable: true, filterable: true, width: 90 }, { key: 'id', label: t('Code'), type: 'text', sortable: true, filterable: true, width: 90 },
{ key: 'label', label: t('adminLanguages.bezeichnung'), type: 'text', sortable: true, filterable: true, width: 200 }, { key: 'label', label: t('Bezeichnung'), type: 'text', sortable: true, filterable: true, width: 200 },
{ key: 'status', label: t('adminLanguages.status'), type: 'text', sortable: true, filterable: true, width: 120 }, { key: 'status', label: t('Status'), type: 'text', sortable: true, filterable: true, width: 120 },
{ key: 'uiCount', label: t('adminLanguages.ui'), type: 'number', sortable: true, width: 80 }, { key: 'uiCount', label: t('UI'), type: 'number', sortable: true, width: 80 },
{ key: 'gatewayCount', label: t('adminLanguages.api'), type: 'number', sortable: true, width: 80 }, { key: 'gatewayCount', label: t('API'), type: 'number', sortable: true, width: 80 },
{ key: 'entriesCount', label: t('adminLanguages.total'), 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 }[] = [ 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: 'fr', label: 'fr — Français' }, { value: 'it', label: 'it — Italiano' },
{ value: 'es', label: 'es — Español' }, { value: 'pt', label: 'pt — Português' }, { value: 'es', label: 'es — Español' }, { value: 'pt', label: 'pt — Português' },
{ value: 'nl', label: 'nl — Nederlands' }, { value: 'pl', label: 'pl — Polski' }, { value: 'nl', label: 'nl — Nederlands' }, { value: 'pl', label: 'pl — Polski' },
@ -101,7 +113,12 @@ const _isoChoices: { value: string; label: string }[] = [
// Progress overlay component // 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 master = progress.keysMasterTotal;
const pending = progress.keysPending ?? 0; const pending = progress.keysPending ?? 0;
const cur = progress.keysCurrent ?? 0; const cur = progress.keysCurrent ?? 0;
@ -246,6 +263,24 @@ const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) =>
{progress.error} {progress.error}
</p> </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>
</div> </div>
); );
@ -265,6 +300,19 @@ export const AdminLanguagesPage: React.FC = () => {
const [progress, setProgress] = useState<ProgressInfo | null>(null); const [progress, setProgress] = useState<ProgressInfo | null>(null);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const busyRef = useRef(false); 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 () => { const _load = useCallback(async () => {
try { 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(() => { useEffect(() => {
_load(); _load();
}, [_load]); }, [_load]);
@ -326,10 +397,10 @@ export const AdminLanguagesPage: React.FC = () => {
} }
}, [addChoices, addCode]); }, [addChoices, addCode]);
const _fetchI18nEntriesFromBundle = useCallback(async (): Promise<any[]> => { const _fetchI18nEntriesFromBundle = useCallback(async (signal?: AbortSignal): Promise<any[]> => {
const base = import.meta.env.BASE_URL || '/'; const base = import.meta.env.BASE_URL || '/';
const normalizedBase = base.endsWith('/') ? base : `${base}/`; 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) { if (!res.ok) {
throw new Error( throw new Error(
t('i18n-keys.json nicht gefunden. Bitte Frontend neu bauen oder Dev-Server starten.'), 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 () => { const _syncXx = async () => {
if (busyRef.current) return; if (busyRef.current) return;
busyRef.current = true; busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setError(null); setError(null);
setProgress({ message: t('Basisset wird eingelesen…'), current: 0, total: 1 }); setProgress({ message: t('Basisset wird eingelesen…'), current: 0, total: 1 });
try { try {
const entries = await _fetchI18nEntriesFromBundle(); const entries = await _fetchI18nEntriesFromBundle(signal);
const res = await api.put('/api/i18n/sets/sync-xx', { entries }); const res = await api.put('/api/i18n/sets/sync-xx', { entries }, { signal });
const d = res.data || {}; const d = res.data || {};
const addedCount = d.added?.length ?? 0; const addedCount = d.added?.length ?? 0;
const removedCount = d.removed?.length ?? 0; const removedCount = d.removed?.length ?? 0;
@ -369,29 +443,40 @@ export const AdminLanguagesPage: React.FC = () => {
await _load(); await _load();
await refreshAvailableLanguages(); await refreshAvailableLanguages();
await reloadLanguage(); await reloadLanguage();
_endProgressSoon(2500);
} catch (e: any) { } catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message; const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler beim Einlesen'), current: 0, total: 1, error: msg, done: true }); setProgress({ message: t('Fehler beim Einlesen'), current: 0, total: 1, error: msg, done: true });
setError(msg); setError(msg);
} finally { _endProgressSoon(2500);
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2500);
} }
}; };
const _updateOne = async (code: string) => { const _updateOne = async (code: string) => {
if (busyRef.current) return; if (busyRef.current) return;
busyRef.current = true; busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setError(null); setError(null);
const label = rows.find((r) => r.id === code)?.label || code; const label = rows.find((r) => r.id === code)?.label || code;
let keysCurrent: number | undefined; let keysCurrent: number | undefined;
let keysPending: number | undefined; let keysPending: number | undefined;
let keysMasterTotal: number | undefined; let keysMasterTotal: number | undefined;
try { 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; keysCurrent = dr.data?.currentEntryCount;
keysPending = dr.data?.addedCount; keysPending = dr.data?.addedCount;
keysMasterTotal = dr.data?.masterEntryCount; keysMasterTotal = dr.data?.masterEntryCount;
} catch { } catch (e) {
if (_isAbortError(e)) {
await _finishProgressAborted({ progressHeading: label, current: 0, total: 1 }, 2200, false);
return;
}
/* sync-diff optional */ /* sync-diff optional */
} }
setProgress({ setProgress({
@ -404,7 +489,7 @@ export const AdminLanguagesPage: React.FC = () => {
keysMasterTotal, keysMasterTotal,
}); });
try { 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 d = putRes.data || {};
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0); const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
setProgress({ setProgress({
@ -421,7 +506,12 @@ export const AdminLanguagesPage: React.FC = () => {
await _load(); await _load();
await refreshAvailableLanguages(); await refreshAvailableLanguages();
await reloadLanguage(); await reloadLanguage();
_endProgressSoon(2000);
} catch (e: any) { } 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; const msg = e.response?.data?.detail || e.message;
setProgress({ setProgress({
message: t('Fehler bei {lang}', { lang: label }), message: t('Fehler bei {lang}', { lang: label }),
@ -432,8 +522,7 @@ export const AdminLanguagesPage: React.FC = () => {
done: true, done: true,
}); });
setError(msg); setError(msg);
} finally { _endProgressSoon(2000);
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2000);
} }
}; };
@ -446,6 +535,9 @@ export const AdminLanguagesPage: React.FC = () => {
if (!ok) return; if (!ok) return;
busyRef.current = true; busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setError(null); setError(null);
const langCodes = rows.filter((r) => r.id !== 'xx').map((r) => r.id); 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 }); setProgress({ message: t('Basisset wird eingelesen…'), current: step, total: totalSteps });
try { try {
const entries = await _fetchI18nEntriesFromBundle(); const entries = await _fetchI18nEntriesFromBundle(signal);
await api.put('/api/i18n/sets/sync-xx', { entries }); await api.put('/api/i18n/sets/sync-xx', { entries }, { signal });
step++; step++;
setProgress({ message: t('Basisset synchronisiert.'), current: step, total: totalSteps }); setProgress({ message: t('Basisset synchronisiert.'), current: step, total: totalSteps });
} catch (e: any) { } catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: step, total: totalSteps }, 2800, false);
return;
}
const msg = e.response?.data?.detail || e.message; const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler beim Basisset'), current: step, total: totalSteps, error: msg, done: true }); setProgress({ message: t('Fehler beim Basisset'), current: step, total: totalSteps, error: msg, done: true });
setError(msg); setError(msg);
setTimeout(() => { setProgress(null); busyRef.current = false; }, 3000); _endProgressSoon(3000);
return; return;
} }
const errors: string[] = []; const errors: string[] = [];
for (const code of langCodes) { 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; const label = rows.find((r) => r.id === code)?.label || code;
let keysCurrent: number | undefined; let keysCurrent: number | undefined;
let keysPending: number | undefined; let keysPending: number | undefined;
let keysMasterTotal: number | undefined; let keysMasterTotal: number | undefined;
try { 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; keysCurrent = dr.data?.currentEntryCount;
keysPending = dr.data?.addedCount; keysPending = dr.data?.addedCount;
keysMasterTotal = dr.data?.masterEntryCount; 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 */ /* sync-diff optional */
} }
setProgress({ setProgress({
@ -490,7 +604,7 @@ export const AdminLanguagesPage: React.FC = () => {
keysMasterTotal, keysMasterTotal,
}); });
try { 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 d = putRes.data || {};
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0); const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
setProgress({ setProgress({
@ -504,6 +618,10 @@ export const AdminLanguagesPage: React.FC = () => {
keysTranslated: typeof d.translated === 'number' ? d.translated : undefined, keysTranslated: typeof d.translated === 'number' ? d.translated : undefined,
}); });
} catch (e: any) { } 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}`); errors.push(`${code}: ${e.response?.data?.detail || e.message}`);
} }
step++; step++;
@ -531,7 +649,7 @@ export const AdminLanguagesPage: React.FC = () => {
await _load(); await _load();
await refreshAvailableLanguages(); await refreshAvailableLanguages();
await reloadLanguage(); 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) ----------------- // --- Other actions (unchanged logic, but with busy guard) -----------------
@ -584,18 +702,25 @@ export const AdminLanguagesPage: React.FC = () => {
); );
if (!go) return; if (!go) return;
busyRef.current = true; busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setProgress({ message: t('Sprache wird erstellt…'), current: 0, total: 1 }); setProgress({ message: t('Sprache wird erstellt…'), current: 0, total: 1 });
try { 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 }); setProgress({ message: t('Sprache erstellt. KI-Übersetzung läuft im Hintergrund.'), current: 1, total: 1, done: true });
await _load(); await _load();
await refreshAvailableLanguages(); await refreshAvailableLanguages();
_endProgressSoon(2500);
} catch (e: any) { } catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message; const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler'), current: 0, total: 1, error: msg, done: true }); setProgress({ message: t('Fehler'), current: 0, total: 1, error: msg, done: true });
setError(msg); setError(msg);
} finally { _endProgressSoon(2500);
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2500);
} }
}; };
@ -630,12 +755,16 @@ export const AdminLanguagesPage: React.FC = () => {
); );
if (!ok) return; if (!ok) return;
busyRef.current = true; busyRef.current = true;
const ac = new AbortController();
abortRef.current = ac;
const { signal } = ac;
setProgress({ message: t('Importiere…'), current: 0, total: 1 }); setProgress({ message: t('Importiere…'), current: 0, total: 1 });
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const res = await api.post('/api/i18n/import', formData, { const res = await api.post('/api/i18n/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
signal,
}); });
const d = res.data || {}; const d = res.data || {};
setError(null); setError(null);
@ -651,12 +780,16 @@ export const AdminLanguagesPage: React.FC = () => {
await _load(); await _load();
await refreshAvailableLanguages(); await refreshAvailableLanguages();
await reloadLanguage(); await reloadLanguage();
_endProgressSoon(2500);
} catch (e: any) { } catch (e: any) {
if (_isAbortError(e)) {
await _finishProgressAborted({ current: 0, total: 1 }, 2200, false);
return;
}
const msg = e.response?.data?.detail || e.message; const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Import fehlgeschlagen'), current: 0, total: 1, error: msg, done: true }); setProgress({ message: t('Import fehlgeschlagen'), current: 0, total: 1, error: msg, done: true });
setError(msg); setError(msg);
} finally { _endProgressSoon(2500);
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2500);
} }
}; };
input.click(); input.click();
@ -755,7 +888,7 @@ export const AdminLanguagesPage: React.FC = () => {
emptyMessage={t('Keine Einträge')} emptyMessage={t('Keine Einträge')}
/> />
{progress && <_ProgressOverlay progress={progress} />} {progress && <_ProgressOverlay progress={progress} onAbort={_abortRunning} />}
</div> </div>
<ConfirmDialog /> <ConfirmDialog />

View file

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

View file

@ -216,9 +216,9 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
// Filter options for scope // Filter options for scope
const scopeOptions = useMemo(() => [ const scopeOptions = useMemo(() => [
{ value: 'mandate', label: t('adminMandateRolePermissionsPage.mandantenRollen') }, { value: 'mandate', label: t('Mandanten-Rollen') },
{ value: 'all', label: t('adminMandateRolePermissionsPage.alleInklTemplates') }, { value: 'all', label: t('Alle inkl. Templates') },
{ value: 'global', label: t('adminMandateRolePermissionsPage.nurTemplates') }, { value: 'global', label: t('Nur Templates') },
], [t]); ], [t]);
if (error) { if (error) {
@ -253,7 +253,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
className={styles.secondaryButton} className={styles.secondaryButton}
onClick={_openCleanupModal} onClick={_openCleanupModal}
disabled={loading} disabled={loading}
title={t('adminMandateRolePermissions.doppelteRegelnFindenUndBereinigen')} title={t('Doppelte Regeln finden und bereinigen')}
> >
<FaBroom /> Duplikate bereinigen <FaBroom /> Duplikate bereinigen
</button> </button>
@ -270,7 +270,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{/* Filters */} {/* Filters */}
<div className={styles.filterBar}> <div className={styles.filterBar}>
<div className={styles.filterGroup}> <div className={styles.filterGroup}>
<label className={styles.filterLabel}>{t('adminMandateRolePermissions.mandant')}</label> <label className={styles.filterLabel}>{t('Mandant')}</label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
value={selectedMandateId} value={selectedMandateId}
@ -314,7 +314,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{loading && ( {loading && (
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner} /> <div className={styles.spinner} />
<span>{t('adminMandateRolePermissions.ladeRollen')}</span> <span>{t('Lade Rollen')}</span>
</div> </div>
)} )}
@ -322,13 +322,13 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{!loading && roles.length === 0 && ( {!loading && roles.length === 0 && (
<div className={styles.emptyState}> <div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} /> <FaUserShield className={styles.emptyIcon} />
<p>{t('adminMandateRolePermissions.keineRollenGefunden')}</p> <p>{t('Keine Rollen gefunden')}</p>
<p className={styles.emptyHint}> <p className={styles.emptyHint}>
{scopeFilter === 'mandate' {scopeFilter === 'mandate'
? 'Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.' ? 'Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.'
: scopeFilter === 'global' : scopeFilter === 'global'
? t('adminMandateRolePermissions.esGibtNochKeineRollentemplates') ? t('Es gibt noch keine Rollentemplates')
: t('adminMandateRolePermissions.esGibtNochKeineRollen')} : t('Es gibt noch keine Rollen')}
</p> </p>
</div> </div>
)} )}
@ -403,7 +403,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{cleanupLoading && ( {cleanupLoading && (
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner} /> <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> </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={{ 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={{ 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: '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>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}> <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: '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>
<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={{ 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> <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' && ( {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' }}> <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 /> <FaCheckCircle />
<span><strong>{cleanupResult.deletedCount}</strong> {t('adminMandateRolePermissions.doppelteRegelnWurdenErfolgreichEntfernt')}</span> <span><strong>{cleanupResult.deletedCount}</strong> {t('Doppelte Regeln wurden erfolgreich entfernt')}</span>
</div> </div>
)} )}
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && ( {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' }}> <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 /> <FaCheckCircle />
<span>{t('adminMandateRolePermissions.keineDuplikateGefundenAllesSauber')}</span> <span>{t('Keine Duplikate gefunden, alles sauber')}</span>
</div> </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={{ 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={{ 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: '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>
<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={{ 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: '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>
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}> <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)' }}> <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' }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
<thead> <thead>
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}> <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('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('Mandant')}</th>
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Aktion</th> <th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Aktion</th>
</tr> </tr>
</thead> </thead>
@ -567,7 +567,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
{templateFixResult && templateFixResult.invalidAssignments === 0 && ( {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' }}> <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 /> <FaCheckCircle />
<span>{t('adminMandateRolePermissions.keineFehlerhaftenTemplaterollenzuweisungen')}</span> <span>{t('Keine fehlerhaften Templaterollenzuweisungen')}</span>
</div> </div>
)} )}
</> </>
@ -576,7 +576,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
<div className={styles.modalFooter}> <div className={styles.modalFooter}>
<button className={styles.secondaryButton} onClick={_closeCleanupModal}> <button className={styles.secondaryButton} onClick={_closeCleanupModal}>
{cleanupPhase === 'done' ? t('adminMandateRolePermissions.schliessen') : t('adminMandateRolePermissions.abbrechen')} {cleanupPhase === 'done' ? t('Schließen') : t('Abbrechen')}
</button> </button>
{cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && ( {cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && (
<button <button

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -56,7 +56,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
}, []); }, []);
const availableConnectors = [ const availableConnectors = [
{ id: 'preprocessor', label: t('chatbotConfigSection.althausPreprocessor'), value: 'preprocessor' } { id: 'preprocessor', label: t('Althaus Preprocessor'), value: 'preprocessor' }
]; ];
const handleConnectorToggle = (connectorValue: string) => { const handleConnectorToggle = (connectorValue: string) => {
@ -112,7 +112,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
onChange={(e) => onEnableWebResearchChange(e.target.checked)} onChange={(e) => onEnableWebResearchChange(e.target.checked)}
className={styles.multiselectCheckbox} className={styles.multiselectCheckbox}
/> />
<span>{t('chatbotConfigSection.webResearchAktivierenTavily')}</span> <span>{t('Web Research aktivieren (Tavily)')}</span>
</label> </label>
<p className={styles.configHelpText}> <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. 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> </label>
<div className={styles.multiselectContainer}> <div className={styles.multiselectContainer}>
{providersLoading ? ( {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 => ( availableProviders.map(provider => (
<label key={provider} className={styles.multiselectOption}> <label key={provider} className={styles.multiselectOption}>
@ -157,7 +157,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ conn
type="text" type="text"
value={systemPrompt} value={systemPrompt}
onChange={onSystemPromptChange} onChange={onSystemPromptChange}
placeholder={t('chatbotConfigSection.benutzerdefinierterSystempromptFuerDenChatbot')} placeholder={t('Benutzerdefinierter Systemprompt für den Chatbot')}
className={styles.configTextArea} className={styles.configTextArea}
size="md" size="md"
rows={6} rows={6}

View file

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

View file

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

View file

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

View file

@ -173,17 +173,17 @@ export const AdminInvitationWizardPage: React.FC = () => {
const email = inviteeForm.email.trim(); const email = inviteeForm.email.trim();
const username = inviteeForm.username.trim(); const username = inviteeForm.username.trim();
if (!email && !username) { if (!email && !username) {
setError(t('adminInvitationWizard.bitteMindestensEineEmailadresseOder')); setError(t('Bitte mindestens eine E-Mail-Adresse oder'));
return; return;
} }
const emailLower = email.toLowerCase(); const emailLower = email.toLowerCase();
const userLower = username.toLowerCase(); const userLower = username.toLowerCase();
if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) { if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) {
setError(t('adminInvitationWizard.dieseEmailIstBereitsIn')); setError(t('Diese E-Mail ist bereits in'));
return; return;
} }
if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) { if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) {
setError(t('adminInvitationWizard.dieserBenutzernameIstBereitsIn')); setError(t('Dieser Benutzername ist bereits in'));
return; return;
} }
setInvitees(prev => [...prev, { setInvitees(prev => [...prev, {
@ -198,14 +198,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
const addInviteeExisting = () => { const addInviteeExisting = () => {
if (!selectedExistingUserId) { if (!selectedExistingUserId) {
setError(t('adminInvitationWizard.bitteWaehlenSieEinenBenutzer')); setError(t('Bitte wählen Sie einen Benutzer'));
return; return;
} }
const user = allSystemUsers.find(u => u.id === selectedExistingUserId); const user = allSystemUsers.find(u => u.id === selectedExistingUserId);
if (!user) return; if (!user) return;
const email = (user.email || '').trim(); const email = (user.email || '').trim();
if (invitees.some(i => i.userId === user.id)) { if (invitees.some(i => i.userId === user.id)) {
setError(t('adminInvitationWizard.dieserBenutzerIstBereitsIn')); setError(t('Dieser Benutzer ist bereits in'));
return; return;
} }
setInvitees(prev => [...prev, { setInvitees(prev => [...prev, {
@ -235,7 +235,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
const handleSend = async () => { const handleSend = async () => {
if (!selectedMandate || invitees.length === 0) return; if (!selectedMandate || invitees.length === 0) return;
if (inviteType === 'featureInstance' && !selectedInstance) { if (inviteType === 'featureInstance' && !selectedInstance) {
setError(t('adminInvitationWizard.bitteWaehlenSieEineFeatureinstanz')); setError(t('Bitte wählen Sie eine Feature-Instanz'));
return; return;
} }
setIsLoading(true); setIsLoading(true);
@ -357,7 +357,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{/* ── STEP 1: Invite type ── */} {/* ── STEP 1: Invite type ── */}
{step === 1 && ( {step === 1 && (
<div style={_cardStyle}> <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' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<button <button
onClick={() => setInviteType('mandate')} onClick={() => setInviteType('mandate')}
@ -367,7 +367,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
cursor: 'pointer', textAlign: 'left', 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)' }}> <div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
Einladung zum Mandanten ohne spezifische Feature-Instanz Einladung zum Mandanten ohne spezifische Feature-Instanz
</div> </div>
@ -380,7 +380,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
cursor: 'pointer', textAlign: 'left', 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)' }}> <div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
Einladung zu einer bestimmten Feature-Instanz mit Rolle Einladung zu einer bestimmten Feature-Instanz mit Rolle
</div> </div>
@ -398,10 +398,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
{step === 2 && ( {step === 2 && (
<div style={_cardStyle}> <div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}> <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> </h3>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>{t('adminInvitationWizard.mandant')}</label> <label className={styles.formLabel}>{t('Mandant')}</label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
style={{ width: '100%' }} style={{ width: '100%' }}
@ -412,7 +412,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
setSelectedInstance(null); setSelectedInstance(null);
}} }}
> >
<option value="">{t('adminInvitationWizard.mandantWaehlen')}</option> <option value="">{t('Mandant wählen')}</option>
{mandates.map(m => ( {mandates.map(m => (
<option key={m.id} value={m.id}>{getMandateName(m)}</option> <option key={m.id} value={m.id}>{getMandateName(m)}</option>
))} ))}
@ -420,9 +420,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
</div> </div>
{inviteType === 'featureInstance' && selectedMandate && ( {inviteType === 'featureInstance' && selectedMandate && (
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>{t('adminInvitationWizard.featureinstanz')}</label> <label className={styles.formLabel}>{t('Feature-Instanz')}</label>
{instances.length === 0 ? ( {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 <select
className={styles.filterSelect} className={styles.filterSelect}
@ -433,7 +433,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
setSelectedInstance(inst || null); setSelectedInstance(inst || null);
}} }}
> >
<option value="">{t('adminInvitationWizard.featureinstanzWaehlen')}</option> <option value="">{t('Feature-Instanz wählen')}</option>
{instances.map(inst => { {instances.map(inst => {
const baseLabel = inst.label || inst.featureCode; const baseLabel = inst.label || inst.featureCode;
const suffix = inst.enabled === false ? ' (deaktiviert)' : ''; const suffix = inst.enabled === false ? ' (deaktiviert)' : '';
@ -446,7 +446,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
</div> </div>
)} )}
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}> <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 <button
className={styles.primaryButton} className={styles.primaryButton}
disabled={!canProceedStep3} disabled={!canProceedStep3}
@ -462,7 +462,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{step === 3 && selectedMandate && ( {step === 3 && selectedMandate && (
<div> <div>
<div style={_cardStyle}> <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' }}> <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. 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> </p>
@ -488,7 +488,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{addMode === 'email' ? ( {addMode === 'email' ? (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}> <div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div> <div>
<label className={styles.formLabel}>{t('adminInvitationWizard.emailOptional')}</label> <label className={styles.formLabel}>{t('E-Mail (optional)')}</label>
<input <input
className={styles.formInput} className={styles.formInput}
type="email" type="email"
@ -498,14 +498,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
/> />
</div> </div>
<div> <div>
<label className={styles.formLabel}>{t('adminInvitationWizard.benutzernameOptional')}</label> <label className={styles.formLabel}>{t('Benutzername (optional)')}</label>
<input <input
className={styles.formInput} className={styles.formInput}
type="text" type="text"
autoComplete="off" autoComplete="off"
value={inviteeForm.username} value={inviteeForm.username}
onChange={e => setInviteeForm(p => ({ ...p, username: e.target.value }))} 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' }}> <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. 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 style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div> <div>
<label className={styles.formLabel}>{t('adminInvitationWizard.bestehenderBenutzer')}</label> <label className={styles.formLabel}>{t('Bestehender Benutzer')}</label>
<select <select
className={styles.filterSelect} className={styles.filterSelect}
style={{ width: '100%' }} style={{ width: '100%' }}
value={selectedExistingUserId} value={selectedExistingUserId}
onChange={e => setSelectedExistingUserId(e.target.value)} onChange={e => setSelectedExistingUserId(e.target.value)}
> >
<option value="">{t('adminInvitationWizard.benutzerWaehlen')}</option> <option value="">{t('Benutzer wählen')}</option>
{availableExistingUsers.map(u => ( {availableExistingUsers.map(u => (
<option key={u.id} value={u.id}> <option key={u.id} value={u.id}>
{u.username} {u.email ? `(${u.email})` : ''} {u.username} {u.email ? `(${u.email})` : ''}
</option> </option>
))} ))}
</select> </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> </div>
{roles.length > 0 && ( {roles.length > 0 && (
<div> <div>
<label className={styles.formLabel}>{t('adminInvitationWizard.rolle')}</label> <label className={styles.formLabel}>{t('Rolle')}</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{roles.map(r => ( {roles.map(r => (
<label key={r.id} className={styles.checkboxLabel} style={{ <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' }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead> <thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}> <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' }}>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: 'left', padding: '8px' }}>Typ</th>
<th style={{ textAlign: 'right', padding: '8px' }}>Aktion</th> <th style={{ textAlign: 'right', padding: '8px' }}>Aktion</th>
</tr> </tr>
@ -645,12 +645,12 @@ export const AdminInvitationWizardPage: React.FC = () => {
</tbody> </tbody>
</table> </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>
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'space-between' }}> <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 <button
className={styles.primaryButton} className={styles.primaryButton}
disabled={invitees.length === 0} disabled={invitees.length === 0}
@ -665,12 +665,12 @@ export const AdminInvitationWizardPage: React.FC = () => {
{/* ── STEP 4: Summary and send ── */} {/* ── STEP 4: Summary and send ── */}
{step === 4 && selectedMandate && !dispatchResults && ( {step === 4 && selectedMandate && !dispatchResults && (
<div style={_cardStyle}> <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' }}> <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>
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<strong>{t('adminInvitationWizard.mandant')}</strong> {getMandateName(selectedMandate)} <strong>{t('Mandant')}</strong> {getMandateName(selectedMandate)}
</div> </div>
{inviteType === 'featureInstance' && selectedInstance && ( {inviteType === 'featureInstance' && selectedInstance && (
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
@ -690,7 +690,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
</ul> </ul>
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}> <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 <button
className={styles.primaryButton} className={styles.primaryButton}
disabled={isLoading} disabled={isLoading}
@ -705,13 +705,13 @@ export const AdminInvitationWizardPage: React.FC = () => {
{/* ── Results ── */} {/* ── Results ── */}
{dispatchResults && ( {dispatchResults && (
<div style={_cardStyle}> <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' }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead> <thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}> <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' }}>{t('adminInvitationWizard.status')}</th> <th style={{ textAlign: 'left', padding: '8px' }}>{t('Status')}</th>
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailGesendet')}</th> <th style={{ textAlign: 'left', padding: '8px' }}>{t('E-Mail gesendet')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

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

View file

@ -69,10 +69,10 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
const createFields: AttributeDefinition[] = useMemo( const createFields: AttributeDefinition[] = useMemo(
() => [ () => [
{ name: 'mandateId', label: t('featureInstanceWizard.mandant'), type: 'enum' as const, required: true, options: mandateOptions }, { name: 'mandateId', label: t('Mandant'), type: 'enum' as const, required: true, options: mandateOptions },
{ name: 'featureCode', label: t('featureInstanceWizard.feature'), type: 'enum' as const, required: true, options: featureOptions }, { name: 'featureCode', label: t('Feature'), type: 'enum' as const, required: true, options: featureOptions },
{ name: 'label', label: t('featureInstanceWizard.bezeichnung'), type: 'string' as const, required: true, editable: true }, { name: 'label', label: t('Bezeichnung'), type: 'string' as const, required: true, editable: true },
{ name: 'enabled', label: t('featureInstanceWizard.aktiv'), type: 'boolean' as const, required: false, editable: true }, { name: 'enabled', label: t('Aktiv'), type: 'boolean' as const, required: false, editable: true },
], ],
[mandateOptions, featureOptions] [mandateOptions, featureOptions]
); );
@ -170,8 +170,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
<div className={styles.modalOverlay} onClick={onClose}> <div className={styles.modalOverlay} onClick={onClose}>
<div className={`${styles.modal} ${wizardStyles.modal}`} onClick={(e) => e.stopPropagation()}> <div className={`${styles.modal} ${wizardStyles.modal}`} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('featureInstanceWizard.neueFeatureinstanz')}</h2> <h2 className={styles.modalTitle}>{t('Neue Feature-Instanz')}</h2>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('featureInstanceWizard.schliessen')}> <button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('Schließen')}>
</button> </button>
</div> </div>
@ -202,8 +202,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
}} }}
onSubmit={handleStep1Submit} onSubmit={handleStep1Submit}
onCancel={onClose} onCancel={onClose}
submitButtonText={t('featureInstanceWizard.weiter')} submitButtonText={t('Weiter')}
cancelButtonText={t('featureInstanceWizard.abbrechen')} cancelButtonText={t('Abbrechen')}
/> />
<label className={wizardStyles.checkLabel}> <label className={wizardStyles.checkLabel}>
<input <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. Optional: Weisen Sie Benutzern Rollen zu. Sie können dies auch später in der Zugriffsverwaltung tun.
</p> </p>
{mandateUsers.length === 0 ? ( {mandateUsers.length === 0 ? (
<p className={wizardStyles.stepText}>{t('featureInstanceWizard.keineMandantenbenutzerVorhanden')}</p> <p className={wizardStyles.stepText}>{t('Keine Mandantenbenutzer vorhanden')}</p>
) : ( ) : (
<div className={wizardStyles.userList}> <div className={wizardStyles.userList}>
{mandateUsers.map((u) => { {mandateUsers.map((u) => {
@ -256,7 +256,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
handleAddUserRole(u.id, rids); handleAddUserRole(u.id, rids);
}} }}
> >
<option value="">{t('featureInstanceWizard.keineRolle')}</option> <option value="">{t('Keine Rolle')}</option>
{instanceRoles.map((r) => ( {instanceRoles.map((r) => (
<option key={r.id} value={r.id}> <option key={r.id} value={r.id}>
{r.roleLabel} {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). */ /** Wenn false: keine neue ClickUp-Verbindung über diese Seite (Buttons inaktiv). */
const isClickupConnectionUiEnabled = false; const isClickupConnectionUiEnabled = false;
export const ConnectionsPage: React.FC = () => { export const ConnectionsPage: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
@ -74,16 +75,15 @@ export const ConnectionsPage: React.FC = () => {
fkDisplayField: (attr as any).fkDisplayField, fkDisplayField: (attr as any).fkDisplayField,
}; };
// Resolve userId to username via FK
if (attr.name === 'userId') { if (attr.name === 'userId') {
col.fkSource = '/api/users/'; col.fkSource = '/api/users/';
col.fkDisplayField = 'username'; col.fkDisplayField = 'username';
col.label = 'User'; col.label = t('Benutzer');
} }
return col; return col;
}); });
}, [attributes]); }, [attributes, t]);
// Check permissions // Check permissions
const canCreate = permissions?.create !== 'n'; const canCreate = permissions?.create !== 'n';
@ -251,10 +251,11 @@ export const ConnectionsPage: React.FC = () => {
<div className={`${styles.adminPage} ${styles.adminPageFill}`}> <div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}> <div className={styles.pageHeader}>
<div> <div>
<h1 className={styles.pageTitle}>{t('connections.verbindungen')}</h1> <h1 className={styles.pageTitle}>{t('Verbindungen')}</h1>
<p className={styles.pageSubtitle}> <p className={styles.pageSubtitle}>
Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft {isClickupConnectionUiEnabled
{isClickupConnectionUiEnabled ? ', ClickUp' : ''}) ? t('Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft, ClickUp)')
: t('Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft)')}
</p> </p>
</div> </div>
<div className={styles.headerActions}> <div className={styles.headerActions}>
@ -262,16 +263,16 @@ export const ConnectionsPage: React.FC = () => {
className={styles.secondaryButton} className={styles.secondaryButton}
onClick={handleAdminConsent} onClick={handleAdminConsent}
disabled={adminConsentPending} disabled={adminConsentPending}
title={t('connections.microsoftAdminConsentErteiltDer')} title={t('Microsoft Admin-Zustimmung erteilt der')}
> >
<FaShieldAlt /> Admin Consent <FaShieldAlt /> {t('Admin-Zustimmung')}
</button> </button>
<button <button
className={styles.secondaryButton} className={styles.secondaryButton}
onClick={() => refetch()} onClick={() => refetch()}
disabled={loading} disabled={loading}
> >
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren <FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button> </button>
{canCreate && ( {canCreate && (
<> <>
@ -295,7 +296,7 @@ export const ConnectionsPage: React.FC = () => {
className={styles.clickupButton} className={styles.clickupButton}
onClick={handleCreateClickup} onClick={handleCreateClickup}
disabled={isConnecting} disabled={isConnecting}
title={t('connections.clickupkontoVerbinden')} title={t('ClickUp-Konto verbinden')}
> >
<FaTasks /> ClickUp <FaTasks /> ClickUp
</button> </button>
@ -321,11 +322,11 @@ export const ConnectionsPage: React.FC = () => {
...(canUpdate ? [{ ...(canUpdate ? [{
type: 'edit' as const, type: 'edit' as const,
onAction: handleEditClick, onAction: handleEditClick,
title: t('connectionsPage.edit'), title: t('Bearbeiten'),
}] : []), }] : []),
...(canDelete ? [{ ...(canDelete ? [{
type: 'delete' as const, type: 'delete' as const,
title: t('connectionsPage.delete'), title: t('Löschen'),
loading: (row: Connection) => deletingConnections.has(row.id), loading: (row: Connection) => deletingConnections.has(row.id),
}] : []), }] : []),
]} ]}
@ -334,7 +335,7 @@ export const ConnectionsPage: React.FC = () => {
id: 'connect', id: 'connect',
icon: <FaLink />, icon: <FaLink />,
onClick: handleConnect, onClick: handleConnect,
title: t('connectionsPage.connect'), title: t('Verbinden'),
visible: (row: Connection) => visible: (row: Connection) =>
row.status !== 'active' && row.status !== 'active' &&
(isClickupConnectionUiEnabled || row.authority !== 'clickup'), (isClickupConnectionUiEnabled || row.authority !== 'clickup'),
@ -344,7 +345,7 @@ export const ConnectionsPage: React.FC = () => {
id: 'refresh', id: 'refresh',
icon: <FaRedo />, icon: <FaRedo />,
onClick: handleRefresh, onClick: handleRefresh,
title: t('connectionsPage.refreshToken'), title: t('Token aktualisieren'),
visible: (row: Connection) => row.status === 'active', visible: (row: Connection) => row.status === 'active',
loading: (row: Connection) => refreshingConnections.has(row.id), loading: (row: Connection) => refreshingConnections.has(row.id),
}, },
@ -358,7 +359,7 @@ export const ConnectionsPage: React.FC = () => {
handleInlineUpdate, handleInlineUpdate,
updateOptimistically, updateOptimistically,
}} }}
emptyMessage={t('connections.keineVerbindungenGefunden')} emptyMessage={t('Keine Verbindungen gefunden')}
/> />
</div> </div>
@ -367,7 +368,7 @@ export const ConnectionsPage: React.FC = () => {
<div className={styles.modalOverlay} onClick={() => setEditingConnection(null)}> <div className={styles.modalOverlay} onClick={() => setEditingConnection(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}> <div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('connections.verbindungBearbeiten')}</h2> <h2 className={styles.modalTitle}>{t('Verbindung bearbeiten')}</h2>
<button <button
className={styles.modalClose} className={styles.modalClose}
onClick={() => setEditingConnection(null)} onClick={() => setEditingConnection(null)}
@ -379,7 +380,7 @@ export const ConnectionsPage: React.FC = () => {
{formAttributes.length === 0 ? ( {formAttributes.length === 0 ? (
<div className={styles.loadingContainer}> <div className={styles.loadingContainer}>
<div className={styles.spinner} /> <div className={styles.spinner} />
<span>{t('connections.ladeFormular')}</span> <span>{t('Formular laden')}</span>
</div> </div>
) : ( ) : (
<FormGeneratorForm <FormGeneratorForm
@ -388,8 +389,8 @@ export const ConnectionsPage: React.FC = () => {
mode="edit" mode="edit"
onSubmit={handleEditSubmit} onSubmit={handleEditSubmit}
onCancel={() => setEditingConnection(null)} onCancel={() => setEditingConnection(null)}
submitButtonText={t('connections.speichern')} submitButtonText={t('Speichern')}
cancelButtonText={t('connections.abbrechen')} cancelButtonText={t('Abbrechen')}
/> />
)} )}
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -91,7 +91,7 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
onChange={(e) => onSelectMandate(e.target.value || null)} onChange={(e) => onSelectMandate(e.target.value || null)}
className={styles.select} className={styles.select}
> >
<option value="">{t('billingUser.alleMandanten')}</option> <option value="">{t('Alle Mandanten')}</option>
{uniqueMandates.map(([id, name]) => ( {uniqueMandates.map(([id, name]) => (
<option key={id} value={id}>{name}</option> <option key={id} value={id}>{name}</option>
))} ))}
@ -106,7 +106,7 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
onChange={(e) => onSelectUser(e.target.value || null)} onChange={(e) => onSelectUser(e.target.value || null)}
className={styles.select} className={styles.select}
> >
<option value="">{t('billingUser.alleBenutzer')}</option> <option value="">{t('Alle Benutzer')}</option>
{uniqueUsers.map(([id, name]) => ( {uniqueUsers.map(([id, name]) => (
<option key={id} value={id}>{name}</option> <option key={id} value={id}>{name}</option>
))} ))}
@ -119,11 +119,11 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
<table className={styles.transactionsTable}> <table className={styles.transactionsTable}>
<thead> <thead>
<tr> <tr>
<th>{t('billingUser.mandant')}</th> <th>{t('Mandant')}</th>
<th>{t('billingUser.benutzer')}</th> <th>{t('Benutzer')}</th>
<th style={{ textAlign: 'right' }}>{t('billingUser.guthaben')}</th> <th style={{ textAlign: 'right' }}>{t('Guthaben')}</th>
<th style={{ textAlign: 'right' }}>Warnschwelle</th> <th style={{ textAlign: 'right' }}>Warnschwelle</th>
<th>{t('billingUser.status')}</th> <th>{t('Status')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -142,7 +142,7 @@ const UserBalanceTable: React.FC<UserBalanceTableProps> = ({ balances,
Niedrig Niedrig
</span> </span>
) : balance.enabled ? ( ) : 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> <span style={{ color: 'var(--color-error)' }}>Deaktiviert</span>
)} )}
@ -226,14 +226,14 @@ const UserTransactionTable: React.FC<UserTransactionTableProps> = ({
<thead> <thead>
<tr> <tr>
<th>Datum</th> <th>Datum</th>
<th>{t('billingUser.mandant')}</th> <th>{t('Mandant')}</th>
<th>{t('billingUser.benutzer')}</th> <th>{t('Benutzer')}</th>
<th>Typ</th> <th>Typ</th>
<th>{t('billingUser.beschreibung')}</th> <th>{t('Beschreibung')}</th>
<th>Anbieter</th> <th>Anbieter</th>
<th>Modell</th> <th>Modell</th>
<th>Feature</th> <th>Feature</th>
<th style={{ textAlign: 'right' }}>{t('billingUser.betrag')}</th> <th style={{ textAlign: 'right' }}>{t('Betrag')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -314,7 +314,7 @@ export const BillingUserView: React.FC = () => {
<div className={styles.billingDashboard}> <div className={styles.billingDashboard}>
<header className={styles.pageHeader}> <header className={styles.pageHeader}>
<h1>Benutzer-Billing</h1> <h1>Benutzer-Billing</h1>
<p className={styles.subtitle}>{t('billingUser.guthabenUndTransaktionenProBenutzer')}</p> <p className={styles.subtitle}>{t('Guthaben und Transaktionen pro Benutzer')}</p>
</header> </header>
<BillingNav /> <BillingNav />
@ -323,9 +323,9 @@ export const BillingUserView: React.FC = () => {
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>Benutzer-Guthaben</h2> <h2 className={styles.sectionTitle}>Benutzer-Guthaben</h2>
{loading && balances.length === 0 ? ( {loading && balances.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingUser.ladeDaten')}</div> <div className={styles.loadingPlaceholder}>{t('Daten laden')}</div>
) : balances.length === 0 ? ( ) : balances.length === 0 ? (
<div className={styles.noData}>{t('billingUser.keineBenutzerkontenVorhanden')}</div> <div className={styles.noData}>{t('Keine Benutzerkonten vorhanden')}</div>
) : ( ) : (
<UserBalanceTable <UserBalanceTable
balances={balances} balances={balances}
@ -350,9 +350,9 @@ export const BillingUserView: React.FC = () => {
</h2> </h2>
</div> </div>
{loading && transactions.length === 0 ? ( {loading && transactions.length === 0 ? (
<div className={styles.loadingPlaceholder}>{t('billingUser.ladeTransaktionen')}</div> <div className={styles.loadingPlaceholder}>{t('Transaktionen laden')}</div>
) : transactions.length === 0 ? ( ) : transactions.length === 0 ? (
<div className={styles.noData}>{t('billingUser.keineTransaktionenVorhanden')}</div> <div className={styles.noData}>{t('Keine Transaktionen vorhanden')}</div>
) : ( ) : (
<> <>
<UserTransactionTable <UserTransactionTable
@ -368,7 +368,7 @@ export const BillingUserView: React.FC = () => {
onClick={handleLoadMore} onClick={handleLoadMore}
disabled={loading} disabled={loading}
> >
{loading ? t('billingUser.laden') : t('billingUser.mehrLaden')} {loading ? t('Laden') : t('Mehr laden')}
</button> </button>
</div> </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 }> { function _getStatusLabel(t: (key: string) => string): Record<string, { label: string; color: string }> {
return { return {
PENDING: { label: t('subscriptionTab.zahlungAusstehend'), color: '#f59e0b' }, PENDING: { label: t('Zahlung ausstehend'), color: '#f59e0b' },
SCHEDULED: { label: t('subscriptionTab.geplant'), color: '#8b5cf6' }, SCHEDULED: { label: t('Geplant'), color: '#8b5cf6' },
ACTIVE: { label: t('subscriptionTab.aktiv'), color: '#22c55e' }, ACTIVE: { label: t('Aktiv'), color: '#22c55e' },
TRIALING: { label: t('subscriptionTab.testphase'), color: '#38bdf8' }, TRIALING: { label: t('Testphase'), color: '#38bdf8' },
PAST_DUE: { label: t('subscriptionTab.zahlungAusstehend'), color: '#f59e0b' }, PAST_DUE: { label: t('Zahlung ausstehend'), color: '#f59e0b' },
EXPIRED: { label: t('subscriptionTab.abgelaufen'), color: '#6b7280' }, EXPIRED: { label: t('Abgelaufen'), color: '#6b7280' },
}; };
} }
function _getPeriodLabel(t: (key: string) => string): Record<string, string> { function _getPeriodLabel(t: (key: string) => string): Record<string, string> {
return { return {
MONTHLY: t('subscriptionTab.monatlich'), MONTHLY: t('Monatlich'),
YEARLY: t('subscriptionTab.jaehrlich'), YEARLY: t('hrlich'),
NONE: '—', NONE: '—',
}; };
} }
@ -164,7 +164,7 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
> >
{activating {activating
? 'Weiterleitung...' ? 'Weiterleitung...'
: (!isFreePlan && !plan.trialDays) ? t('subscriptionTab.kostenpflichtigAbonnieren') : t('subscriptionTab.auswaehlen')} : (!isFreePlan && !plan.trialDays) ? t('Kostenpflichtig abonnieren') : t('Auswählen')}
</button> </button>
)} )}
</div> </div>
@ -215,7 +215,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
<span style={{ <span style={{
fontSize: '0.7rem', padding: '2px 8px', borderRadius: '4px', fontSize: '0.7rem', padding: '2px 8px', borderRadius: '4px',
background: '#ef4444', color: '#fff', fontWeight: 600, background: '#ef4444', color: '#fff', fontWeight: 600,
}}>{t('subscriptionTab.gekuendigt')}</span> }}>{t('Gekündigt')}</span>
)} )}
<span style={{ <span style={{
fontSize: '0.75rem', padding: '2px 10px', borderRadius: '4px', 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', color: justPaid ? '#22c55e' : '#f59e0b', fontSize: '0.85rem',
}}> }}>
{justPaid {justPaid
? t('subscriptionTab.zahlungErfolgreichAbonnementWirdAktiviert') ? t('Zahlung erfolgreich Abonnement wird aktiviert')
: t('subscriptionTab.dieZahlungWurdeNochNicht')} : t('Zahlung noch nicht eingegangen')}
</div> </div>
)} )}
@ -288,7 +288,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
fontWeight: 600, cursor: reactivating ? 'wait' : 'pointer', fontSize: '0.85rem', fontWeight: 600, cursor: reactivating ? 'wait' : 'pointer', fontSize: '0.85rem',
}} }}
> >
{reactivating ? t('subscriptionTab.wirdReaktiviert') : t('subscriptionTab.reaktivieren')} {reactivating ? t('Wird reaktiviert') : t('Reaktivieren')}
</button> </button>
)} )}
@ -303,7 +303,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
cursor: cancelling ? 'wait' : 'pointer', fontSize: '0.85rem', cursor: cancelling ? 'wait' : 'pointer', fontSize: '0.85rem',
}} }}
> >
{cancelling ? t('subscriptionTab.wirdGekuendigt') : t('subscriptionTab.kuendigen')} {cancelling ? t('Wird gekündigt') : t('Kündigen')}
</button> </button>
)} )}
@ -318,7 +318,7 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
cursor: cancelling ? 'wait' : 'pointer', fontSize: '0.85rem', cursor: cancelling ? 'wait' : 'pointer', fontSize: '0.85rem',
}} }}
> >
{cancelling ? t('subscriptionTab.wirdAbgebrochen') : t('subscriptionTab.abbrechen')} {cancelling ? t('Wird abgebrochen') : t('Abbrechen')}
</button> </button>
)} )}
</div> </div>
@ -421,12 +421,12 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
const isPendingOrScheduled = sub?.status === 'PENDING' || sub?.status === 'SCHEDULED'; const isPendingOrScheduled = sub?.status === 'PENDING' || sub?.status === 'SCHEDULED';
const ok = await confirm( const ok = await confirm(
isPendingOrScheduled isPendingOrScheduled
? t('subscriptionTab.diesenVorgangAbbrechen') ? t('Diesen Vorgang abbrechen?')
: t('subscriptionTab.abonnementKuendigenEsBleibtBis'), : t('Abonnement kündigen? Es bleibt bis zum Periodenende aktiv.'),
{ {
title: isPendingOrScheduled ? t('subscriptionTab.vorgangAbbrechen') : t('subscriptionTab.abonnementKuendigen'), title: isPendingOrScheduled ? t('Vorgang abbrechen') : t('Abonnement kündigen'),
confirmLabel: isPendingOrScheduled ? t('subscriptionTab.jaAbbrechen') : t('subscriptionTab.kuendigen'), confirmLabel: isPendingOrScheduled ? t('Ja, abbrechen') : t('Kündigen'),
cancelLabel: isPendingOrScheduled ? t('subscriptionTab.neinZurueck') : t('subscriptionTab.abbrechen'), cancelLabel: isPendingOrScheduled ? t('Nein, zurück') : t('Abbrechen'),
variant: 'danger', variant: 'danger',
}, },
); );
@ -456,7 +456,7 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
}, [reactivateSubscription]); }, [reactivateSubscription]);
if (loading && !subscription) { if (loading && !subscription) {
return <div className={styles.loadingPlaceholder}>{t('subscriptionTab.ladeAbonnementdaten')}</div>; return <div className={styles.loadingPlaceholder}>{t('Abonnementdaten werden geladen…')}</div>;
} }
return ( return (
@ -483,13 +483,13 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{/* Current subscription */} {/* Current subscription */}
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('subscriptionTab.aktuellesAbonnement')}</h2> <h2 className={styles.sectionTitle}>{t('Aktuelles Abonnement')}</h2>
{subscription ? ( {subscription ? (
<SubInfoCard <SubInfoCard
sub={subscription} sub={subscription}
plan={currentPlan} plan={currentPlan}
label={subscription.status === 'PENDING' label={subscription.status === 'PENDING'
? (justPaid ? t('subscriptionTab.zahlungWirdVerarbeitet') : t('subscriptionTab.checkoutInBearbeitung')) ? (justPaid ? t('Zahlung wird verarbeitet…') : t('Checkout läuft…'))
: 'Operatives Abonnement'} : 'Operatives Abonnement'}
onCancel={_handleCancel} onCancel={_handleCancel}
onReactivate={_handleReactivate} onReactivate={_handleReactivate}
@ -507,11 +507,11 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{/* Scheduled successor */} {/* Scheduled successor */}
{scheduled && ( {scheduled && (
<section className={styles.section}> <section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('subscriptionTab.geplanterNachfolger')}</h2> <h2 className={styles.sectionTitle}>{t('Geplanter Nachfolgeplan')}</h2>
<SubInfoCard <SubInfoCard
sub={scheduled} sub={scheduled}
plan={null} plan={null}
label={t('subscriptionTab.startetNachAblaufDesAktuellen')} label={t('Startet nach Ablauf des aktuellen Plans')}
onCancel={_handleCancel} onCancel={_handleCancel}
cancelling={cancelling} cancelling={cancelling}
reactivating={false} reactivating={false}
@ -521,9 +521,9 @@ export const SubscriptionTab: React.FC<SubscriptionTabProps> = ({ mandateId }) =
{/* Available plans */} {/* Available plans */}
<section className={styles.section}> <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 ? ( {plans.length === 0 ? (
<div className={styles.noData}>{t('subscriptionTab.keinePlaeneVerfuegbar')}</div> <div className={styles.noData}>{t('Keine Pläne verfügbar')}</div>
) : ( ) : (
<div style={{ <div style={{
display: 'grid', display: 'grid',

View file

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

View file

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

View file

@ -250,7 +250,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
}, [newTaskTitle, coach]); }, [newTaskTitle, coach]);
if (coach.loadingContexts) { 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 ( return (
@ -261,7 +261,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button <button
className={styles.udbToggle} className={styles.udbToggle}
onClick={() => setUdbCollapsed(v => !v)} onClick={() => setUdbCollapsed(v => !v)}
title={udbCollapsed ? t('commcoachDossier.seitenleisteEinblenden') : t('commcoachDossier.seitenleisteAusblenden')} title={udbCollapsed ? t('Seitenleiste einblenden') : t('Seitenleiste ausblenden')}
> >
{udbCollapsed ? '\u25B6' : '\u25C0'} {udbCollapsed ? '\u25B6' : '\u25C0'}
</button> </button>
@ -293,7 +293,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button <button
className={styles.contextChipNew} className={styles.contextChipNew}
onClick={() => setShowNewContext(!showNewContext)} onClick={() => setShowNewContext(!showNewContext)}
title={t('commcoachDossier.neuesThema')} title={t('Neues Thema')}
> >
+ +
</button> </button>
@ -304,7 +304,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.newContextForm}> <div className={styles.newContextForm}>
<input <input
className={styles.newContextInput} className={styles.newContextInput}
placeholder={t('commcoachDossier.themaTitel')} placeholder={t('Thema Titel')}
value={newTitle} value={newTitle}
onChange={e => setNewTitle(e.target.value)} onChange={e => setNewTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreateContext()} onKeyDown={e => e.key === 'Enter' && handleCreateContext()}
@ -312,25 +312,25 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
/> />
<input <input
className={styles.newContextInput} className={styles.newContextInput}
placeholder={t('commcoachDossier.beschreibungOptional')} placeholder={t('Beschreibung (optional)')}
value={newDescription} value={newDescription}
onChange={e => setNewDescription(e.target.value)} onChange={e => setNewDescription(e.target.value)}
/> />
<select className={styles.newContextInput} value={newCategory} onChange={e => setNewCategory(e.target.value)}> <select className={styles.newContextInput} value={newCategory} onChange={e => setNewCategory(e.target.value)}>
<option value="custom">Individuell</option> <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="conflict">Konflikt</option>
<option value="negotiation">Verhandlung</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="feedback">Feedback</option>
<option value="delegation">Delegation</option> <option value="delegation">Delegation</option>
<option value="changeManagement">{t('commcoachDossier.changeManagement')}</option> <option value="changeManagement">{t('Change Management')}</option>
</select> </select>
<div className={styles.newContextActions}> <div className={styles.newContextActions}>
<button className={styles.btnPrimary} onClick={handleCreateContext} disabled={!newTitle.trim() || !!coach.actionLoading}> <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>
<button className={styles.btnSecondary} onClick={() => setShowNewContext(false)}>{t('commcoachDossier.abbrechen')}</button> <button className={styles.btnSecondary} onClick={() => setShowNewContext(false)}>{t('Abbrechen')}</button>
</div> </div>
</div> </div>
)} )}
@ -338,9 +338,9 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{/* No context selected */} {/* No context selected */}
{!coach.selectedContextId && !showNewContext && coach.contexts.length === 0 && ( {!coach.selectedContextId && !showNewContext && coach.contexts.length === 0 && (
<div className={styles.empty}> <div className={styles.empty}>
<h3>{t('commcoachDossier.willkommenBeimKommunikationscoach')}</h3> <h3>{t('Willkommen beim Kommunikationscoach')}</h3>
<p>{t('commcoachDossier.erstelleEinThemaUmZu')}</p> <p>{t('Erstelle ein Thema, um zu')}</p>
<button className={styles.btnPrimary} onClick={() => setShowNewContext(true)}>{t('commcoachDossier.neuesThemaErstellen')}</button> <button className={styles.btnPrimary} onClick={() => setShowNewContext(true)}>{t('Neues Thema erstellen')}</button>
</div> </div>
)} )}
@ -356,12 +356,12 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.headerActions}> <div className={styles.headerActions}>
{instanceId && ( {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, '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('commcoachDossier.exportPdf')}</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}> <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> </button>
</div> </div>
</div> </div>
@ -386,10 +386,10 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.coachingTab}> <div className={styles.coachingTab}>
{!coach.session ? ( {!coach.session ? (
<div className={styles.sessionStart}> <div className={styles.sessionStart}>
<p>{t('commcoachDossier.starteEineNeueCoachingsessionZu')}</p> <p>{t('Starte eine neue Coachingsession zu')}</p>
{personas.length > 0 && ( {personas.length > 0 && (
<div className={styles.personaSelector}> <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}> <div className={styles.personaGrid}>
{personas.map(p => ( {personas.map(p => (
<button <button
@ -417,7 +417,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<> <>
{/* Session Header */} {/* Session Header */}
<div className={styles.sessionHeader}> <div className={styles.sessionHeader}>
<span className={styles.sessionLabel}>{t('commcoachDossier.sessionAktiv')}</span> <span className={styles.sessionLabel}>{t('Session aktiv')}</span>
<div className={styles.sessionActions}> <div className={styles.sessionActions}>
{voice.state === 'botSpeaking' && ( {voice.state === 'botSpeaking' && (
<> <>
@ -431,15 +431,15 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button <button
className={`${styles.btnSmall} ${voice.muted ? styles.mutedActive : ''}`} className={`${styles.btnSmall} ${voice.muted ? styles.mutedActive : ''}`}
onClick={voice.toggleMute} 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>
<button className={styles.btnSmall} onClick={coach.completeSession} disabled={!!coach.actionLoading}> <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>
<button className={styles.btnSmallDanger} onClick={coach.cancelSession} disabled={!!coach.actionLoading}> <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> </button>
</div> </div>
</div> </div>
@ -488,7 +488,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{coach.agentToolCalls.length > 0 ? ` (${coach.agentToolCalls.length})` : ''} {coach.agentToolCalls.length > 0 ? ` (${coach.agentToolCalls.length})` : ''}
</span> </span>
<span className={styles.agentActivityStatus}> <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>
<span className={styles.agentActivityChevron}>{showAgentActivity ? '▾' : '▸'}</span> <span className={styles.agentActivityChevron}>{showAgentActivity ? '▾' : '▸'}</span>
</button> </button>
@ -601,7 +601,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<textarea <textarea
ref={inputRef} ref={inputRef}
className={styles.textInput} className={styles.textInput}
placeholder={t('commcoachDossier.nachrichtEingeben')} placeholder={t('Nachricht eingeben')}
value={coach.inputValue} value={coach.inputValue}
onChange={e => coach.setInputValue(e.target.value)} onChange={e => coach.setInputValue(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -615,7 +615,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button <button
onClick={() => { setShowFilePicker(v => !v); setShowSourcePicker(false); }} onClick={() => { setShowFilePicker(v => !v); setShowSourcePicker(false); }}
disabled={coach.isStreaming} disabled={coach.isStreaming}
title={t('commcoachDossier.dateiAnhaengen')} title={t('Datei anhängen')}
style={{ style={{
width: 36, height: 36, borderRadius: 8, width: 36, height: 36, borderRadius: 8,
border: `1px solid ${attachedFileIds.length ? '#1565c0' : 'var(--border-color, #ddd)'}`, 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, borderRadius: 8, boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', zIndex: 20,
minWidth: 220, maxHeight: 240, overflowY: 'auto', 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 => { {wsFiles.map(f => {
const sel = attachedFileIds.includes(f.id); const sel = attachedFileIds.includes(f.id);
return ( return (
@ -670,7 +670,7 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<button <button
onClick={() => { setShowSourcePicker(v => !v); setShowFilePicker(false); }} onClick={() => { setShowSourcePicker(v => !v); setShowFilePicker(false); }}
disabled={coach.isStreaming} disabled={coach.isStreaming}
title={t('commcoachDossier.datenquellenAnhaengen')} title={t('Datenquellen anhängen')}
style={{ style={{
width: 36, height: 36, borderRadius: 8, width: 36, height: 36, borderRadius: 8,
border: `1px solid ${(attachedDsIds.length + attachedFdsIds.length) ? '#2e7d32' : 'var(--border-color, #ddd)'}`, 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 && ( {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 => { {wsDataSources.map(ds => {
const sel = attachedDsIds.includes(ds.id); const sel = attachedDsIds.includes(ds.id);
return ( return (
@ -774,17 +774,17 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
<div className={styles.addTaskRow}> <div className={styles.addTaskRow}>
<input <input
className={styles.addTaskInput} className={styles.addTaskInput}
placeholder={t('commcoachDossier.neueAufgabe')} placeholder={t('Neue Aufgabe')}
value={newTaskTitle} value={newTaskTitle}
onChange={e => setNewTaskTitle(e.target.value)} onChange={e => setNewTaskTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleAddTask()} onKeyDown={e => e.key === 'Enter' && handleAddTask()}
/> />
<button className={styles.addTaskBtn} onClick={handleAddTask} disabled={!newTaskTitle.trim() || !!coach.actionLoading}> <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> </button>
</div> </div>
{coach.tasks.length === 0 ? ( {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}> <div className={styles.taskList}>
{coach.tasks.map(task => ( {coach.tasks.map(task => (
@ -813,14 +813,14 @@ export const CommcoachDossierView: React.FC<CommcoachDossierViewProps> = ({ pers
{activeTab === 'sessions' && ( {activeTab === 'sessions' && (
<div className={styles.tabContent}> <div className={styles.tabContent}>
{coach.sessions.length === 0 ? ( {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}> <div className={styles.sessionTimeline}>
{coach.sessions.map(s => ( {coach.sessions.map(s => (
<div key={s.id} className={styles.sessionItem}> <div key={s.id} className={styles.sessionItem}>
<div className={styles.sessionItemHeader}> <div className={styles.sessionItemHeader}>
<span className={`${styles.sessionStatus} ${styles[`status_${s.status}`]}`}> <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>
<span className={styles.sessionDate}>{s.startedAt ? new Date(s.startedAt).toLocaleDateString('de-CH') : ''}</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>} {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}> <div className={styles.sessionMeta}>
{s.messageCount} Nachrichten | {Math.round(s.durationSeconds / 60)} Min. {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' && ( {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> <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' && ( {activeTab === 'scores' && (
<div className={styles.tabContent}> <div className={styles.tabContent}>
{coach.scores.length === 0 ? ( {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}> <div className={styles.scoreList}>
{_groupScoresByDimension(coach.scores).map(group => ( {_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 { function _tabLabel(tab: TabKey, coach: any, t: (key: string) => string): string {
switch (tab) { switch (tab) {
case 'coaching': return coach.session ? t('commcoachDossier.coachingAktiv') : t('commcoachDossier.coaching'); case 'coaching': return coach.session ? t('Coaching aktiv') : t('Coaching');
case 'tasks': return `${t('commcoachDossier.aufgaben')} (${coach.tasks.length})`; case 'tasks': return `${t('Aufgaben')} (${coach.tasks.length})`;
case 'sessions': return `Sessions (${coach.sessions.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, emailSummaryEnabled: emailEnabled,
}); });
setProfile(updated); setProfile(updated);
setSuccess(t('commcoachSettings.einstellungenGespeichert')); setSuccess(t('Einstellungen gespeichert'));
setTimeout(() => setSuccess(null), 3000); setTimeout(() => setSuccess(null), 3000);
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Fehler beim Speichern'); setError(err.message || 'Fehler beim Speichern');
@ -76,7 +76,7 @@ export const CommcoachSettingsView: React.FC = () => {
}, [request, instanceId, reminderEnabled, reminderTime, emailEnabled]); }, [request, instanceId, reminderEnabled, reminderTime, emailEnabled]);
if (loading) { if (loading) {
return <div className={styles.loading}>{t('commcoachSettings.einstellungenWerdenGeladen')}</div>; return <div className={styles.loading}>{t('Einstellungen werden geladen')}</div>;
} }
return ( return (
@ -85,7 +85,7 @@ export const CommcoachSettingsView: React.FC = () => {
{success && <div className={styles.success}>{success}</div>} {success && <div className={styles.success}>{success}</div>}
<div className={styles.section}> <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' }}> <p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #888)', margin: '0 0 0.5rem' }}>
Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert. Stimme und Sprache werden zentral in den Benutzereinstellungen konfiguriert.
</p> </p>
@ -120,16 +120,16 @@ export const CommcoachSettingsView: React.FC = () => {
<div className={styles.section}> <div className={styles.section}>
<h3 className={styles.sectionTitle}>Statistik</h3> <h3 className={styles.sectionTitle}>Statistik</h3>
<div className={styles.statsGrid}> <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.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('commcoachSettings.minutenGesamt')}</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('commcoachSettings.aktuellerStreak')}</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('commcoachSettings.laengsterStreak')}</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>
</div> </div>
)} )}
<button className={styles.saveBtn} onClick={handleSave} disabled={saving}> <button className={styles.saveBtn} onClick={handleSave} disabled={saving}>
{saving ? t('commcoachSettings.speichern') : t('commcoachSettings.einstellungenSpeichern')} {saving ? t('speichern') : t('Einstellungen speichern')}
</button> </button>
</div> </div>
); );

View file

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

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