alle sprachtexte mit dot-strings ersetzt
This commit is contained in:
parent
9ac2d5a6c1
commit
7cf0795660
131 changed files with 1410 additions and 1279 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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'))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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('Wä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];
|
||||||
|
|
|
||||||
|
|
@ -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('Lö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('Lö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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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('Lö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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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' : ''}.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")}
|
||||||
|
|
|
||||||
|
|
@ -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 })}
|
||||||
|
|
|
||||||
|
|
@ -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")}
|
||||||
|
|
|
||||||
|
|
@ -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}>×</button>
|
<button className={styles.closeButton} onClick={onClose}>×</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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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'}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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')}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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('Lö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 ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 "Mandanten verwalten" einen Mandanten an.
|
Keine Mandanten vorhanden. Legen Sie unter "Mandanten verwalten" 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}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 →
|
Weiter →
|
||||||
</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);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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' }}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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('Jä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',
|
||||||
|
|
|
||||||
|
|
@ -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('Lö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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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('Fü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})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue