decision subscription

This commit is contained in:
ValueOn AG 2026-04-10 22:44:14 +02:00
parent abe6ba60d4
commit 9ac2d5a6c1
21 changed files with 221 additions and 238 deletions

View file

@ -76,7 +76,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = {
features: [
{
code: 'trustee',
label: { de: 'Treuhand', en: 'Trustee' },
label: 'Treuhand',
icon: 'briefcase',
instances: [
{
@ -101,7 +101,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = {
},
{
code: 'chatworkflow',
label: { de: 'Workflow', en: 'Workflow' },
label: 'Workflow',
icon: 'play_circle',
instances: [
{
@ -124,7 +124,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = {
features: [
{
code: 'trustee',
label: { de: 'Treuhand', en: 'Trustee' },
label: 'Treuhand',
icon: 'briefcase',
instances: [
{
@ -234,9 +234,9 @@ export async function fetchMyFeatures(): Promise<FeaturesMyResponse> {
export async function fetchAvailableFeatures(): Promise<MandateFeature[]> {
if (USE_MOCK) {
return [
{ code: 'trustee', label: { de: 'Treuhand', en: 'Trustee' }, icon: 'briefcase', instances: [] },
{ code: 'chatworkflow', label: { de: 'Workflow', en: 'Workflow' }, icon: 'play_circle', instances: [] },
{ code: 'chatbot', label: { de: 'Chatbot', en: 'Chatbot' }, icon: 'chat', instances: [] },
{ code: 'trustee', label: 'Treuhand', icon: 'briefcase', instances: [] },
{ code: 'chatworkflow', label: 'Workflow', icon: 'play_circle', instances: [] },
{ code: 'chatbot', label: 'Chatbot', icon: 'chat', instances: [] },
];
}

View file

@ -17,9 +17,9 @@ export interface StoreFeatureInstance {
export interface StoreFeature {
featureCode: string;
label: Record<string, string>;
label: string;
icon: string;
description: Record<string, string>;
description: string;
instances: StoreFeatureInstance[];
canActivate: boolean;
}
@ -49,7 +49,9 @@ export interface SubscriptionInfo {
status: string | null;
maxDataVolumeMB: number | null;
maxFeatureInstances: number | null;
includedModules: number;
budgetAiCHF: number | null;
budgetAiPerUserCHF: number | null;
currentFeatureInstances: number;
trialEndsAt: string | null;
}

View file

@ -19,8 +19,10 @@ export interface SubscriptionPlan {
autoRenew: boolean;
maxUsers: number | null;
maxFeatureInstances: number | null;
includedModules: number;
maxDataVolumeMB?: number | null;
budgetAiCHF?: number;
budgetAiPerUserCHF?: number;
trialDays: number | null;
successorPlanKey: string | null;
}

View file

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import ChatsTab from './ChatsTab';
import FilesTab from './FilesTab';
import SourcesTab from './SourcesTab';
import { useLanguage } from '../../providers/language/LanguageContext';
import styles from './UnifiedDataBar.module.css';
export type UdbTab = 'chats' | 'files' | 'sources';
@ -29,10 +30,10 @@ interface UnifiedDataBarProps {
className?: string;
}
const _TAB_LABELS: Record<UdbTab, Record<string, string>> = {
chats: { de: 'Chats', en: 'Chats', fr: 'Chats' },
files: { de: 'Dateien', en: 'Files', fr: 'Fichiers' },
sources: { de: 'Quellen', en: 'Sources', fr: 'Sources' },
const _TAB_KEYS: Record<UdbTab, string> = {
chats: 'Chats',
files: 'Dateien',
sources: 'Quellen',
};
const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
@ -50,8 +51,9 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
onSourcesChanged,
className,
}) => {
const { t } = useLanguage();
const visibleTabs = (['chats', 'files', 'sources'] as UdbTab[]).filter(
t => !hideTabs?.includes(t),
(ubTab) => !hideTabs?.includes(ubTab),
);
const [internalTab, setInternalTab] = useState<UdbTab>(controlledTab ?? visibleTabs[0] ?? 'chats');
const currentTab = controlledTab ?? internalTab;
@ -70,7 +72,7 @@ const UnifiedDataBar: React.FC<UnifiedDataBarProps> = ({
className={`${styles.tab} ${currentTab === tab ? styles.tabActive : ''}`}
onClick={() => _handleTabChange(tab)}
>
{_TAB_LABELS[tab].de}
{t(_TAB_KEYS[tab])}
</button>
))}
</div>

View file

@ -20,47 +20,24 @@ const FEATURE_ICONS: Record<string, React.ReactNode> = {
commcoach: <FaComments />,
};
const FEATURE_DESCRIPTIONS: Record<string, Record<string, string>> = {
automation: {
de: 'Erstelle und verwalte Automatisierungen, um wiederkehrende Aufgaben effizient zu erledigen.',
en: 'Create and manage automations to handle recurring tasks efficiently.',
fr: 'Creer et gerer des automatisations pour traiter efficacement les taches recurrentes.',
},
graphicalEditor: {
de: 'n8n-style Flow-Automatisierung mit grafischem Editor, RAG und Tools.',
en: 'n8n-style flow automation with visual editor, RAG and tools.',
fr: 'Automatisation de flux style n8n avec editeur visuel, RAG et outils.',
},
teamsbot: {
de: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.',
en: 'Integrate an AI bot into your Microsoft Teams meetings and channels.',
fr: 'Integrez un bot IA dans vos reunions et canaux Microsoft Teams.',
},
workspace: {
de: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.',
en: 'Use the shared AI workspace: chats, tools, and context per instance.',
fr: 'Utilisez l\'espace de travail IA partage: chats, outils et contexte par instance.',
},
commcoach: {
de: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.',
en: 'CommCoach: practice communication with AI-assisted coaching and feedback.',
fr: 'CommCoach: entrainer la communication avec un coaching assiste par IA.',
},
/** Fallback when GET /store/features omits description (German i18n keys). */
const STORE_FEATURE_DESCRIPTION_FALLBACK: Record<string, string> = {
automation: 'Erstelle und verwalte Automatisierungen, um wiederkehrende Aufgaben effizient zu erledigen.',
graphicalEditor: 'n8n-style Flow-Automatisierung mit grafischem Editor, RAG und Tools.',
teamsbot: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.',
workspace: 'Nutze den gemeinsamen AI Workspace: Chats, Tools und Kontext pro Instanz.',
commcoach: 'CommCoach: Kommunikation trainieren mit KI-gestütztem Coaching und Feedback.',
};
function _getLabel(labels: Record<string, string>, lang: string): string {
return labels[lang] || labels['en'] || labels['de'] || Object.values(labels)[0] || '';
}
function _getDescription(featureCode: string, lang: string): string {
const desc = FEATURE_DESCRIPTIONS[featureCode];
if (!desc) return '';
return desc[lang] || desc['en'] || desc['de'] || '';
function _storeCardDescription(feature: StoreFeature, t: (key: string, fallback?: string) => string): string {
const raw =
(feature.description && feature.description.trim()) ||
STORE_FEATURE_DESCRIPTION_FALLBACK[feature.featureCode];
return raw ? t(raw) : '';
}
interface FeatureCardProps {
feature: StoreFeature;
language: string;
mandates: UserMandate[];
actionLoading: string | null;
onActivate: (code: string, mandateId?: string) => void;
@ -69,7 +46,6 @@ interface FeatureCardProps {
const FeatureCard: React.FC<FeatureCardProps> = ({
feature,
language,
mandates,
actionLoading,
onActivate,
@ -86,13 +62,13 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
<div className={styles.cardHeader}>
{icon && <span className={styles.cardIcon}>{icon}</span>}
<h3 className={styles.cardTitle}>
{_getLabel(feature.label, language)}
{t(feature.label)}
</h3>
</div>
<div className={styles.cardBody}>
<p className={styles.cardDescription}>
{_getDescription(feature.featureCode, language)}
{_storeCardDescription(feature, t)}
</p>
</div>
@ -111,9 +87,7 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
onClick={() => onDeactivate(feature.featureCode, inst.mandateId, inst.instanceId)}
disabled={isProcessing}
>
{isProcessing
? '...'
: (language === 'de' ? 'Deaktivieren' : language === 'fr' ? 'Desactiver' : 'Deactivate')}
{isProcessing ? '...' : t('Deaktivieren')}
</button>
</div>
))}
@ -124,7 +98,7 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
<div>
<span className={`${styles.statusBadge} ${styles.statusInactive}`}>
<span className={styles.statusDot} />
{language === 'de' ? 'Verfuegbar' : language === 'fr' ? 'Disponible' : 'Available'}
{t('Verfügbar')}
</span>
</div>
)}
@ -138,12 +112,8 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
disabled={isProcessing}
>
{isProcessing
? (language === 'de' ? t('store.wirdAktiviert') : t('store.activating'))
: (language === 'de'
? `Aktivieren fuer ${m.label || m.name}`
: language === 'fr'
? `Activer pour ${m.label || m.name}`
: `Activate for ${m.label || m.name}`)}
? t('store.wirdAktiviert', t('store.activating'))
: t('Aktivieren für {name}', { name: String(m.label || m.name) })}
</button>
))}
</div>
@ -152,44 +122,48 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
};
const StorePage: React.FC = () => {
const { t, currentLanguage } = useLanguage();
const { t } = useLanguage();
const { features, mandates, subscriptionInfo, loading, actionLoading, error, activate, deactivate } = useStore();
return (
<div className={styles.store}>
<div className={styles.header}>
<h1>{currentLanguage === 'de' ? 'Feature Store' : currentLanguage === 'fr' ? t('store.featureStore') : t('store.featureStore')}</h1>
<h1>{t('Feature Store')}</h1>
<p className={styles.subtitle}>
{currentLanguage === 'de'
? 'Aktiviere Features fuer dein Konto. Deine Daten sind isoliert und nur fuer dich sichtbar.'
: currentLanguage === 'fr'
? t('store.activezDesFonctionnalitesPourVotre')
: t('store.activateFeaturesForYourAccount')}
{t(
'Aktiviere Features für dein Konto. Deine Daten sind isoliert und nur für dich sichtbar.',
t('store.activateFeaturesForYourAccount')
)}
</p>
</div>
{subscriptionInfo && subscriptionInfo.plan && (
<div className={styles.subscriptionBanner}>
<span>Plan: <strong>{subscriptionInfo.plan}</strong></span>
{subscriptionInfo.maxFeatureInstances != null && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? 'Instanzen' : 'Instances'}: {subscriptionInfo.currentFeatureInstances}/{subscriptionInfo.maxFeatureInstances}
</span>
)}
<span>{t('Plan:')} <strong>{subscriptionInfo.plan}</strong></span>
<span className={styles.bannerSeparator}>
{subscriptionInfo.maxFeatureInstances != null
? <>{t('Module')}: {subscriptionInfo.currentFeatureInstances}/{subscriptionInfo.maxFeatureInstances}</>
: <>{subscriptionInfo.currentFeatureInstances} {t('Module aktiv')}
{subscriptionInfo.includedModules != null && subscriptionInfo.includedModules > 0 && (
<> ({subscriptionInfo.includedModules} {t('inklusive')})</>
)}
</>
}
</span>
{subscriptionInfo.maxDataVolumeMB != null && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? 'Speicher' : 'Storage'}:{' '}
{t('Speicher')}:{' '}
{formatBinaryDataSizeFromMebibytes(subscriptionInfo.maxDataVolumeMB)}
</span>
)}
{subscriptionInfo.budgetAiCHF != null && subscriptionInfo.budgetAiCHF > 0 && (
{subscriptionInfo.budgetAiPerUserCHF != null && subscriptionInfo.budgetAiPerUserCHF > 0 && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? 'AI-Budget' : 'AI budget'}: {subscriptionInfo.budgetAiCHF} CHF
{t('AI-Budget')}: {subscriptionInfo.budgetAiPerUserCHF} CHF / User
</span>
)}
{subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? t('store.trialEndet') : t('store.trialEnds')}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
{t('store.trialEndet', t('store.trialEnds'))}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
</span>
)}
</div>
@ -199,13 +173,11 @@ const StorePage: React.FC = () => {
{loading ? (
<div className={styles.loading}>
{currentLanguage === 'de' ? t('store.ladeFeatures') : t('store.loadingFeatures')}
{t('store.ladeFeatures', t('store.loadingFeatures'))}
</div>
) : features.length === 0 ? (
<div className={styles.empty}>
{currentLanguage === 'de'
? t('store.keineFeaturesImStoreVerfuegbar')
: t('store.noFeaturesAvailableInThe')}
{t('store.keineFeaturesImStoreVerfuegbar', t('store.noFeaturesAvailableInThe'))}
</div>
) : (
<div className={styles.grid}>
@ -213,7 +185,6 @@ const StorePage: React.FC = () => {
<FeatureCard
key={feature.featureCode}
feature={feature}
language={currentLanguage}
mandates={mandates}
actionLoading={actionLoading}
onActivate={activate}

View file

@ -24,6 +24,7 @@ import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard';
import { InstanceHierarchyView } from './InstanceHierarchyView';
import { useLanguage } from '../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../types/mandate';
function getMandateName(mandate: Mandate): string {
if (mandate.label) return mandate.label;
@ -33,11 +34,8 @@ function getMandateName(mandate: Mandate): string {
return mandate.name || mandate.id;
}
function getFeatureLabel(feature: Feature): string {
if (typeof feature.label === 'object') {
return feature.label.de || feature.label.en || feature.code;
}
return feature.label || feature.code;
function getFeatureLabel(feature: Feature, t: (k: string) => string): string {
return t(labelAsI18nKey(feature.label, feature.code));
}
export interface InstanceWithStats extends FeatureInstance {
@ -168,7 +166,7 @@ export const AccessManagementHub: React.FC = () => {
instance,
mandateId: mandateId || '',
mandateName: mandate ? getMandateName(mandate) : mandateId || '',
featureLabel: feature ? getFeatureLabel(feature) : instance.featureCode,
featureLabel: feature ? getFeatureLabel(feature, t) : instance.featureCode,
});
};
@ -302,7 +300,7 @@ export const AccessManagementHub: React.FC = () => {
};
}),
};
}, [selectedMandateId, mandates, filteredInstances, features]);
}, [selectedMandateId, mandates, filteredInstances, features, t]);
if (error && !selectedMandateId) {
return (
@ -364,7 +362,7 @@ export const AccessManagementHub: React.FC = () => {
<option value="">{t('accessManagementHub.alle')}</option>
{features.map((f) => (
<option key={f.code} value={f.code}>
{getFeatureLabel(f)}
{getFeatureLabel(f, t)}
</option>
))}
</select>
@ -527,7 +525,7 @@ export const AccessManagementHub: React.FC = () => {
</span>
</div>
<div className={hubStyles.instanceMeta}>
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode })}</span>
<span>{getFeatureLabel(features.find((f) => f.code === inst.featureCode) || { code: inst.featureCode, label: inst.featureCode }, t)}</span>
<span>{inst.userCount ?? '—'} Benutzer</span>
<span>{inst.roleCount ?? '—'} Rollen</span>
</div>

View file

@ -20,6 +20,7 @@ import { TextField } from '../../components/UiComponents/TextField';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../types/mandate';
export const AdminFeatureAccessPage: React.FC = () => {
const { t } = useLanguage();
@ -93,10 +94,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
render: (value: string) => {
const feature = features.find(f => f.code === value);
if (feature) {
const label = typeof feature.label === 'object'
? (feature.label.de || feature.label.en || value)
: feature.label;
return label;
return t(labelAsI18nKey(feature.label, value));
}
return value;
}
@ -327,9 +325,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
const getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code);
if (feature) {
return typeof feature.label === 'object'
? (feature.label.de || feature.label.en || code)
: (feature.label || code);
return t(labelAsI18nKey(feature.label, code));
}
return code;
};
@ -514,9 +510,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
<DropdownSelect
items={features.map(f => ({
id: f.code,
label: typeof f.label === 'object'
? (f.label.de || f.label.en || f.code)
: (f.label || f.code),
label: t(labelAsI18nKey(f.label, f.code)),
value: f.code
}))}
selectedItemId={createFeatureCode}

View file

@ -17,6 +17,7 @@ import api from '../../api';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../types/mandate';
export const AdminFeatureInstanceUsersPage: React.FC = () => {
const { t } = useLanguage();
@ -368,9 +369,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
const getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code);
if (feature) {
return typeof feature.label === 'object'
? (feature.label.de || feature.label.en || code)
: (feature.label || code);
return t(labelAsI18nKey(feature.label, code));
}
return code;
};

View file

@ -263,8 +263,11 @@ export const AdminFeatureRolesPage: React.FC = () => {
setEditingRole(role);
};
// Get feature name - Backend uses 'label' field
const getFeatureName = (feature: Feature) => getTextValue(feature.label || feature.name);
// Get feature name - Backend uses 'label' field (German i18n key or legacy multilingual)
const getFeatureName = (feature: Feature) => {
const raw = getTextValue(feature.label || feature.name);
return raw === '-' ? '-' : t(raw);
};
if (error && !selectedFeatureCode) {
return (

View file

@ -22,6 +22,7 @@ import { FormGeneratorForm } from '../../../components/FormGenerator/FormGenerat
import styles from '../Admin.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../../types/mandate';
const TOTAL_STEPS = 4;
const STEP_LABELS = ['Mandant', 'Benutzer', 'Instances', 'Feature-Benutzer'];
@ -115,9 +116,7 @@ export const AdminMandateWizardPage: React.FC = () => {
const getFeatureLabel = (code: string): string => {
const f = features.find(feat => feat.code === code);
if (f) {
return typeof f.label === 'object'
? (f.label.de || f.label.en || code)
: (f.label || code);
return t(labelAsI18nKey(f.label, code));
}
return code;
};

View file

@ -15,17 +15,13 @@ import styles from '../Admin.module.css';
import wizardStyles from './FeatureInstanceWizard.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
import { labelAsI18nKey } from '../../../types/mandate';
function getMandateName(m: Mandate): string {
if (typeof m.name === 'object') return m.name.de || m.name.en || Object.values(m.name)[0] || m.id;
return m.name || m.id;
}
function getFeatureLabel(f: Feature): string {
if (typeof f.label === 'object') return f.label.de || f.label.en || f.code;
return f.label || f.code;
}
export interface FeatureInstanceWizardProps {
mandateId: string;
mandates: Mandate[];
@ -63,8 +59,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
const [selectedUserRoles, setSelectedUserRoles] = useState<Array<{ userId: string; roleIds: string[] }>>([]);
const featureOptions = useMemo(
() => features.map((f) => ({ value: f.code, label: getFeatureLabel(f) })),
[features]
() => features.map((f) => ({ value: f.code, label: t(labelAsI18nKey(f.label, f.code)) })),
[features, t]
);
const mandateOptions = useMemo(
() => mandates.map((m) => ({ value: m.id, label: getMandateName(m) })),

View file

@ -16,12 +16,12 @@ function _getColumns(t: (key: string) => string): ColumnConfig[] {
{ key: 'status', label: t('adminSubscriptions.status'), type: 'text', sortable: true, filterable: true, width: 110 },
{ key: 'recurring', label: t('adminSubscriptions.wiederkehrend'), type: 'boolean', sortable: true, filterable: true, width: 120 },
{ key: 'activeUsers', label: t('adminSubscriptions.user'), type: 'number', sortable: true, width: 70 },
{ key: 'activeInstances', label: t('adminSubscriptions.instanzen'), type: 'number', sortable: true, width: 90 },
{ key: 'activeInstances', label: t('adminSubscriptions.module'), type: 'number', sortable: true, width: 90 },
{ key: 'monthlyRevenueCHF', label: t('adminSubscriptions.revenueProMonat'), type: 'number', sortable: true, width: 140 },
{ key: 'startedAt', label: t('adminSubscriptions.gestartet'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'currentPeriodEnd', label: t('adminSubscriptions.periodenende'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'snapshotPricePerUserCHF', label: t('adminSubscriptions.preisProUser'), type: 'number', sortable: true, width: 100 },
{ key: 'snapshotPricePerInstanceCHF', label: t('adminSubscriptions.preisProInstanz'), type: 'number', sortable: true, width: 110 },
{ key: 'snapshotPricePerInstanceCHF', label: t('adminSubscriptions.preisProModul'), type: 'number', sortable: true, width: 110 },
];
}

View file

@ -109,20 +109,29 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
{!isFreePlan && (
<div style={{ fontSize: '0.85rem' }}>
<div>User: <strong>{_formatCurrency(plan.pricePerUserCHF)}</strong> / {periodLabel[plan.billingPeriod] || plan.billingPeriod}</div>
<div>Instanz: <strong>{_formatCurrency(plan.pricePerFeatureInstanceCHF)}</strong> / {periodLabel[plan.billingPeriod] || plan.billingPeriod}</div>
<div>
Module inkl.: <strong>{plan.includedModules ?? 0}</strong>
{(plan.pricePerFeatureInstanceCHF ?? 0) > 0 && (
<> · Zusatzmodul: <strong>{_formatCurrency(plan.pricePerFeatureInstanceCHF)}</strong> / Monat</>
)}
</div>
<div style={{ marginTop: '0.35rem', color: 'var(--text-secondary)' }}>
AI-Budget (inkl.): <strong>{_formatCurrency(plan.budgetAiCHF ?? 0)}</strong> / Periode
AI-Budget: <strong>{_formatCurrency(plan.budgetAiPerUserCHF ?? 0)}</strong> / User / Monat
{' · '}
Speicher (inkl.):{' '}
Speicher:{' '}
<strong>
{plan.maxDataVolumeMB == null
? 'unbegrenzt'
: formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}
</strong>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat
</div>
{plan.maxUsers != null && (
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
Max. User: {plan.maxUsers}
{' · '}
Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat
</div>
)}
</div>
)}
@ -130,13 +139,13 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, isCurrent, onActivate, activa
<div style={{ fontSize: '0.85rem' }}>
{plan.trialDays} Tage kostenlos
{plan.maxUsers && <> · max. {plan.maxUsers} User</>}
{plan.maxFeatureInstances && <> · max. {plan.maxFeatureInstances} Instanzen</>}
{(plan.maxDataVolumeMB != null || (plan.budgetAiCHF ?? 0) > 0) && (
{(plan.includedModules ?? 0) > 0 && <> · {plan.includedModules} Module inkl.</>}
{(plan.maxDataVolumeMB != null || (plan.budgetAiPerUserCHF ?? 0) > 0) && (
<>
{plan.maxDataVolumeMB != null && (
<> · Speicher inkl. {formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}</>
<> · Speicher {formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}</>
)}
{(plan.budgetAiCHF ?? 0) > 0 && <> · AI-Budget { _formatCurrency(plan.budgetAiCHF ?? 0)}</>}
{(plan.budgetAiPerUserCHF ?? 0) > 0 && <> · AI-Budget {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User</>}
</>
)}
</div>
@ -252,15 +261,16 @@ const SubInfoCard: React.FC<SubInfoProps> = ({ sub, plan, label, onCancel, onRea
)}
{plan && (
<>
<span>AI-Budget (inkl.): {_formatCurrency(plan.budgetAiCHF ?? 0)} / Periode</span>
<span>AI-Budget: {_formatCurrency(plan.budgetAiPerUserCHF ?? 0)} / User / Monat</span>
<span>Module inkl.: {plan.includedModules ?? 0}</span>
<span>
Speicher (inkl.):{' '}
Speicher:{' '}
{plan.maxDataVolumeMB == null
? 'unbegrenzt'
: formatBinaryDataSizeFromMebibytes(plan.maxDataVolumeMB)}
</span>
<span style={{ gridColumn: '1 / -1' }}>
Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat (High-Watermark)
<span>
Speicher über Plan: {_formatCurrency(storageOverageChfPerGbMonth)} / GB / Monat
</span>
</>
)}

View file

@ -30,15 +30,13 @@ const _TABS: TabDef[] = [
{ id: 'year-end', templateTag: 'template:trustee-year-end-check', icon: '\u2705', color: '#795548' },
];
const _TAB_LABELS: Record<string, Record<string, string>> = {
'year-end': { de: 'Jahresabschluss prüfen', en: 'Year-End Review', fr: 'Contrôle de clôture' },
const _TAB_LABEL_KEYS: Record<string, string> = {
'year-end': 'Jahresabschluss prüfen',
};
const _TAB_DESCRIPTIONS: Record<string, Record<string, string>> = {
'year-end': {
de: 'Automatische Prüfungen für den Jahresabschluss: Saldovalidierung, Vorjahresvergleich, gesetzliche Checks.',
en: 'Automated year-end review: balance validation, prior-year comparison, legal compliance checks.',
},
const _TAB_DESCRIPTION_KEYS: Record<string, string> = {
'year-end':
'Automatische Prüfungen für den Jahresabschluss: Saldovalidierung, Vorjahresvergleich, gesetzliche Checks.',
};
// ---------------------------------------------------------------------------
@ -58,8 +56,7 @@ type RunState = 'idle' | 'starting' | 'running' | 'completed' | 'error';
// ---------------------------------------------------------------------------
export const TrusteeAbschlussView: React.FC = () => {
const { t, currentLanguage } = useLanguage();
const lang = currentLanguage || 'de';
const { t } = useLanguage();
const { instanceId } = useCurrentInstance();
const { showSuccess, showError } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
@ -228,7 +225,7 @@ export const TrusteeAbschlussView: React.FC = () => {
}}
>
<span style={{ marginRight: '0.375rem' }}>{tab.icon}</span>
{_TAB_LABELS[tab.id]?.[lang] || _TAB_LABELS[tab.id]?.de || tab.id}
{t(_TAB_LABEL_KEYS[tab.id] || tab.id)}
</button>
))}
</div>
@ -237,7 +234,7 @@ export const TrusteeAbschlussView: React.FC = () => {
{/* Tab content */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<p className={styles.sectionDescription}>
{_TAB_DESCRIPTIONS[activeTab]?.[lang] || _TAB_DESCRIPTIONS[activeTab]?.de || ''}
{_TAB_DESCRIPTION_KEYS[activeTab] ? t(_TAB_DESCRIPTION_KEYS[activeTab]) : ''}
</p>
{workflowsLoading ? (

View file

@ -33,18 +33,18 @@ const _TABS: TabDef[] = [
{ id: 'forecast', templateTag: 'template:trustee-forecast', icon: '\uD83D\uDCC8', color: '#E91E63' },
];
const _TAB_LABELS: Record<string, Record<string, string>> = {
budget: { de: 'Budget-Vergleich', en: 'Budget Comparison', fr: 'Comparaison budgétaire' },
kpi: { de: 'KPI-Dashboard', en: 'KPI Dashboard', fr: 'Tableau de bord KPI' },
cashflow: { de: 'Cashflow-Rechnung', en: 'Cash Flow Statement', fr: 'Flux de trésorerie' },
forecast: { de: 'Prognose', en: 'Forecast', fr: 'Prévision' },
const _TAB_LABEL_KEYS: Record<string, string> = {
budget: 'Budget-Vergleich',
kpi: 'KPI-Dashboard',
cashflow: 'Cashflow-Rechnung',
forecast: 'Prognose',
};
const _TAB_DESCRIPTIONS: Record<string, Record<string, string>> = {
budget: { de: 'Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel', en: 'Compare actuals vs. budget from Excel' },
kpi: { de: 'Kennzahlen berechnen und visualisieren', en: 'Calculate and visualize key metrics' },
cashflow: { de: 'Cashflow berechnen und analysieren', en: 'Calculate and analyze cash flow' },
forecast: { de: 'Trend-Analyse und Prognose der nächsten Monate', en: 'Trend analysis and forecast for coming months' },
const _TAB_DESCRIPTION_KEYS: Record<string, string> = {
budget: 'Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel',
kpi: 'Kennzahlen berechnen und visualisieren',
cashflow: 'Cashflow berechnen und analysieren',
forecast: 'Trend-Analyse und Prognose der nächsten Monate',
};
// ---------------------------------------------------------------------------
@ -64,8 +64,7 @@ type RunState = 'idle' | 'starting' | 'running' | 'completed' | 'error';
// ---------------------------------------------------------------------------
export const TrusteeAnalyseView: React.FC = () => {
const { t, currentLanguage } = useLanguage();
const lang = currentLanguage || 'de';
const { t } = useLanguage();
const { instanceId } = useCurrentInstance();
const { showSuccess, showError } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
@ -241,7 +240,7 @@ export const TrusteeAnalyseView: React.FC = () => {
}}
>
<span style={{ marginRight: '0.375rem' }}>{tab.icon}</span>
{_TAB_LABELS[tab.id]?.[lang] || _TAB_LABELS[tab.id]?.de || tab.id}
{t(_TAB_LABEL_KEYS[tab.id] || tab.id)}
</button>
))}
</div>
@ -249,7 +248,7 @@ export const TrusteeAnalyseView: React.FC = () => {
{/* Tab content */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<p className={styles.sectionDescription}>
{_TAB_DESCRIPTIONS[activeTab]?.[lang] || _TAB_DESCRIPTIONS[activeTab]?.de || ''}
{_TAB_DESCRIPTION_KEYS[activeTab] ? t(_TAB_DESCRIPTION_KEYS[activeTab]) : ''}
</p>
{workflowsLoading ? (

View file

@ -100,7 +100,7 @@ export const TrusteeDashboardView: React.FC = () => {
<div className={styles.statValue}>
{isLoading ? '...' : positions.length}
</div>
<div className={styles.statLabel}>Positionen</div>
<div className={styles.statLabel}>{t('Positionen')}</div>
</div>
</div>
@ -110,7 +110,7 @@ export const TrusteeDashboardView: React.FC = () => {
<div className={styles.statValue}>
{isLoading ? '...' : documents.length}
</div>
<div className={styles.statLabel}>Dokumente</div>
<div className={styles.statLabel}>{t('Dokumente')}</div>
</div>
</div>
@ -122,11 +122,11 @@ export const TrusteeDashboardView: React.FC = () => {
<div className={styles.statValueSmall}>
{isLoading ? '...' : (
accountingConfig?.configured
? <>{syncedCount} synced{syncErrorCount > 0 && <span style={{ color: 'var(--error-color, #dc2626)' }}> / {syncErrorCount} errors</span>}</>
: 'Not configured'
? <>{syncedCount} {t('synchronisiert')}{syncErrorCount > 0 && <span style={{ color: 'var(--error-color, #dc2626)' }}> / {syncErrorCount} {t('Fehler')}</span>}</>
: t('Nicht konfiguriert')
)}
</div>
<div className={styles.statLabel}>Buchhaltung</div>
<div className={styles.statLabel}>{t('Buchhaltung')}</div>
</div>
</div>
@ -155,10 +155,10 @@ export const TrusteeDashboardView: React.FC = () => {
/>
<div className={styles.infoSection}>
<h3>Instanz-Details</h3>
<h3>{t('Instanz-Details')}</h3>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Instanz:</span>
<span className={styles.infoLabel}>{t('Instanz:')}</span>
<span className={styles.infoValue}>{instance?.instanceLabel}</span>
</div>
<div className={styles.infoItem}>
@ -167,7 +167,7 @@ export const TrusteeDashboardView: React.FC = () => {
</div>
{accountingConfig?.configured && (
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Buchhaltungssystem:</span>
<span className={styles.infoLabel}>{t('Buchhaltungssystem:')}</span>
<span className={styles.infoValue}>
{accountingConfig.displayLabel || accountingConfig.connectorType}
{accountingConfig.lastSyncStatus && ` (${accountingConfig.lastSyncStatus})`}

View file

@ -140,7 +140,7 @@ export const TrusteeDocumentsView: React.FC = () => {
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Download error:', err);
showError('Fehler', 'Fehler beim Herunterladen des Dokuments.');
showError(t('Fehler'), t('Fehler beim Herunterladen des Dokuments.'));
} finally {
setDownloadingId(null);
}
@ -172,9 +172,9 @@ export const TrusteeDocumentsView: React.FC = () => {
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Dokumente: {error}</p>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Dokumente: {detail}', { detail: String(error) })}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
@ -193,14 +193,14 @@ export const TrusteeDocumentsView: React.FC = () => {
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ Neues Dokument
+ {t('Neues Dokument')}
</button>
)}
</div>

View file

@ -57,13 +57,13 @@ export const TrusteeInstanceRolesView: React.FC = () => {
const rolesList = response.data?.items || response.data || [];
setRoles(Array.isArray(rolesList) ? rolesList : []);
} catch (err: any) {
const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden der Rollen';
const errorMsg = err.response?.data?.detail || err.message || t('Fehler beim Laden der Rollen');
setError(errorMsg);
console.error('Error loading instance roles:', err);
} finally {
setLoading(false);
}
}, [instance?.id, instance?.mandateId]);
}, [instance?.id, instance?.mandateId, t]);
useEffect(() => {
fetchRoles();
@ -96,7 +96,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
<div className={styles.error}>
<p>{error}</p>
<button onClick={fetchRoles} className={styles.retryButton}>
<FaSync /> Erneut versuchen
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
@ -109,7 +109,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
<div className={styles.headerLeft}>
<p className={styles.viewSubtitle}>
<FaUserShield style={{ marginRight: '0.5rem', verticalAlign: 'middle' }} />
Verwalten Sie die Berechtigungen für die Rollen dieser Trustee-Instanz
{t('Verwalten Sie die Berechtigungen für die Rollen dieser Trustee-Instanz')}
</p>
</div>
<div className={styles.headerActions}>
@ -122,8 +122,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
<div className={styles.infoBox}>
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
<span>
Diese Rollen wurden von den Feature-Templates kopiert.
Änderungen hier gelten nur für diese Instanz.
{t('Diese Rollen wurden von den Feature-Templates kopiert. Änderungen hier gelten nur für diese Instanz.')}
</span>
</div>
@ -132,7 +131,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
<FaUserShield className={styles.emptyIcon} />
<p>{t('trusteeInstanceRoles.keineInstanzrollenGefunden')}</p>
<p className={styles.emptyHint}>
Instanz-Rollen werden automatisch erstellt, wenn Benutzer dieser Instanz zugewiesen werden.
{t('Instanz-Rollen werden automatisch erstellt, wenn Benutzer dieser Instanz zugewiesen werden.')}
</p>
</div>
) : (
@ -154,7 +153,7 @@ export const TrusteeInstanceRolesView: React.FC = () => {
</div>
<div className={styles.roleBadges}>
{role.isSystemRole && (
<span className={styles.systemBadge}>System</span>
<span className={styles.systemBadge}>{t('System')}</span>
)}
</div>
</div>

View file

@ -140,9 +140,9 @@ export const TrusteePositionDocumentsView: React.FC = () => {
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Verknüpfungen: {error}</p>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Verknüpfungen: {detail}', { detail: String(error) })}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
@ -161,14 +161,14 @@ export const TrusteePositionDocumentsView: React.FC = () => {
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ Neue Verknüpfung
+ {t('Neue Verknüpfung')}
</button>
)}
</div>

View file

@ -146,7 +146,7 @@ export const TrusteePositionsView: React.FC = () => {
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Download error:', err);
showError('Fehler', 'Fehler beim Herunterladen des Dokuments.');
showError(t('Fehler'), t('Fehler beim Herunterladen des Dokuments.'));
} finally {
setDownloadingDocIds(prev => {
const next = new Set(prev);
@ -394,9 +394,9 @@ export const TrusteePositionsView: React.FC = () => {
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Positionen: {error}</p>
<p className={styles.errorMessage}>{t('Fehler beim Laden der Positionen: {detail}', { detail: String(error) })}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
@ -415,14 +415,14 @@ export const TrusteePositionsView: React.FC = () => {
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={handleCreateClick}
>
+ Neue Position
+ {t('Neue Position')}
</button>
)}
</div>

View file

@ -101,7 +101,7 @@ export interface FeatureInstance {
*/
export interface MandateFeature {
code: string; // "trustee", "chatbot", "chatworkflow", etc.
label: I18nLabel; // { de: "Treuhand", en: "Trustee" }
label: string; // German plaintext i18n key
icon: string; // Material/React Icon Name
instances: FeatureInstance[];
}
@ -163,7 +163,7 @@ export interface User {
*/
export interface FeatureView {
code: string; // z.B. "dashboard", "contracts", "documents"
label: I18nLabel;
label: string; // German plaintext i18n key
icon?: string;
path: string; // Relativer Pfad innerhalb der Instanz
adminOnly?: boolean; // Nur für Admin-Rollen sichtbar
@ -175,7 +175,7 @@ export interface FeatureView {
*/
export interface FeatureConfig {
code: string;
label: I18nLabel;
label: string; // German plaintext i18n key
icon: string;
views: FeatureView[];
deprecated?: boolean;
@ -201,100 +201,100 @@ export interface FeatureConfig {
export const FEATURE_REGISTRY: Record<string, FeatureConfig> = {
trustee: {
code: 'trustee',
label: { de: 'Treuhand', en: 'Trustee' },
label: 'Treuhand',
icon: 'briefcase',
views: [
{ code: 'dashboard', label: { de: 'Übersicht', en: 'Dashboard' }, path: 'dashboard' },
{ code: 'positions', label: { de: 'Positionen', en: 'Positions' }, path: 'positions' },
{ code: 'documents', label: { de: 'Dokumente', en: 'Documents' }, path: 'documents' },
{ code: 'position-documents', label: { de: 'Zuordnungen', en: 'Assignments' }, path: 'position-documents' },
{ code: 'expense-import', label: { de: 'Spesen Import', en: 'Expense Import' }, path: 'expense-import' },
{ code: 'scan-upload', label: { de: 'Scannen / Hochladen', en: 'Scan / Upload' }, path: 'scan-upload' },
{ code: 'instance-roles', label: { de: 'Rollen & Rechte', en: 'Roles & Permissions' }, path: 'instance-roles', adminOnly: true },
{ code: 'settings', label: { de: 'Buchhaltungseinstellungen', en: 'Accounting Settings' }, path: 'settings' },
{ code: 'dashboard', label: 'Übersicht', path: 'dashboard' },
{ code: 'positions', label: 'Positionen', path: 'positions' },
{ code: 'documents', label: 'Dokumente', path: 'documents' },
{ code: 'position-documents', label: 'Zuordnungen', path: 'position-documents' },
{ code: 'expense-import', label: 'Spesen Import', path: 'expense-import' },
{ code: 'scan-upload', label: 'Scannen / Hochladen', path: 'scan-upload' },
{ code: 'instance-roles', label: 'Rollen & Rechte', path: 'instance-roles', adminOnly: true },
{ code: 'settings', label: 'Buchhaltungseinstellungen', path: 'settings' },
]
},
chatworkflow: {
code: 'chatworkflow',
label: { de: 'Workflow', en: 'Workflow' },
label: 'Workflow',
icon: 'play_circle',
views: [
{ code: 'dashboard', label: { de: 'Übersicht', en: 'Dashboard' }, path: 'dashboard' },
{ code: 'runs', label: { de: 'Runs', en: 'Runs' }, path: 'runs' },
{ code: 'files', label: { de: 'Dateien', en: 'Files' }, path: 'files' },
{ code: 'dashboard', label: 'Übersicht', path: 'dashboard' },
{ code: 'runs', label: 'Runs', path: 'runs' },
{ code: 'files', label: 'Dateien', path: 'files' },
]
},
chatbot: {
code: 'chatbot',
label: { de: 'Chatbot', en: 'Chatbot' },
label: 'Chatbot',
icon: 'chat',
views: [
{ code: 'conversations', label: { de: 'Konversationen', en: 'Conversations' }, path: 'conversations' },
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings' }, path: 'settings' },
{ code: 'conversations', label: 'Konversationen', path: 'conversations' },
{ code: 'settings', label: 'Einstellungen', path: 'settings' },
]
},
realestate: {
code: 'realestate',
label: { de: 'Immobilien', en: 'Real Estate' },
label: 'Immobilien',
icon: 'home',
views: [
{ code: 'dashboard', label: { de: 'Karte', en: 'Map' }, path: 'dashboard' },
{ code: 'instance-roles', label: { de: 'Rollen & Rechte', en: 'Roles & Permissions' }, path: 'instance-roles', adminOnly: true },
{ code: 'dashboard', label: 'Karte', path: 'dashboard' },
{ code: 'instance-roles', label: 'Rollen & Rechte', path: 'instance-roles', adminOnly: true },
]
},
teamsbot: {
code: 'teamsbot',
label: { de: 'Teams Bot', en: 'Teams Bot' },
label: 'Teams Bot',
icon: 'headset_mic',
views: [
{ code: 'dashboard', label: { de: 'Übersicht', en: 'Dashboard' }, path: 'dashboard' },
{ code: 'sessions', label: { de: 'Sitzungen', en: 'Sessions' }, path: 'sessions' },
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings' }, path: 'settings' },
{ code: 'dashboard', label: 'Übersicht', path: 'dashboard' },
{ code: 'sessions', label: 'Sitzungen', path: 'sessions' },
{ code: 'settings', label: 'Einstellungen', path: 'settings' },
]
},
graphicalEditor: {
code: 'graphicalEditor',
label: { de: 'Grafischer Editor', en: 'Graphical Editor' },
label: 'Grafischer Editor',
icon: 'sitemap',
views: [
{ code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' },
{ code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' },
{ code: 'templates', label: { de: 'Vorlagen', en: 'Templates' }, path: 'templates' },
{ code: 'workflows-tasks', label: { de: 'Tasks', en: 'Tasks' }, path: 'workflows-tasks' },
{ code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard' }, path: 'dashboard' },
{ code: 'editor', label: 'Editor', path: 'editor' },
{ code: 'workflows', label: 'Workflows', path: 'workflows' },
{ code: 'templates', label: 'Vorlagen', path: 'templates' },
{ code: 'workflows-tasks', label: 'Tasks', path: 'workflows-tasks' },
{ code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
]
},
neutralization: {
code: 'neutralization',
label: { de: 'Neutralisierung', en: 'Neutralization', fr: 'Neutralisation' },
label: 'Neutralisierung',
icon: 'shield_check',
views: [
{ code: 'dashboard', label: { de: 'Neutralisierung testen', en: 'Test Neutralization', fr: 'Tester neutralisation' }, path: 'playground' },
{ code: 'playground', label: { de: 'Neutralisierung testen', en: 'Test Neutralization', fr: 'Tester neutralisation' }, path: 'playground' },
{ code: 'config', label: { de: 'Einstellungen', en: 'Settings', fr: 'Paramètres' }, path: 'config' },
{ code: 'attributes', label: { de: 'Attribute', en: 'Attributes', fr: 'Attributs' }, path: 'attributes' },
{ code: 'dashboard', label: 'Neutralisierung testen', path: 'playground' },
{ code: 'playground', label: 'Neutralisierung testen', path: 'playground' },
{ code: 'config', label: 'Einstellungen', path: 'config' },
{ code: 'attributes', label: 'Attribute', path: 'attributes' },
]
},
commcoach: {
code: 'commcoach',
label: { de: 'Kommunikations-Coach', en: 'Communication Coach', fr: 'Coach Communication' },
label: 'Kommunikations-Coach',
icon: 'account_voice',
views: [
{ code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' },
{ code: 'coaching', label: { de: 'Coaching', en: 'Coaching', fr: 'Coaching' }, path: 'coaching' },
{ code: 'dossier', label: { de: 'Dossier', en: 'Dossier', fr: 'Dossier' }, path: 'dossier' },
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Paramètres' }, path: 'settings' },
{ code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
{ code: 'coaching', label: 'Coaching', path: 'coaching' },
{ code: 'dossier', label: 'Dossier', path: 'dossier' },
{ code: 'settings', label: 'Einstellungen', path: 'settings' },
]
},
workspace: {
code: 'workspace',
label: { de: 'AI Workspace', en: 'AI Workspace', fr: 'AI Workspace' },
label: 'AI Workspace',
icon: 'psychology',
views: [
{ code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' },
{ code: 'editor', label: { de: 'Editor', en: 'Editor', fr: 'Editeur' }, path: 'editor' },
{ code: 'rag-insights', label: { de: 'Wissens-Insights', en: 'Knowledge insights', fr: 'Aperçu des connaissances' }, path: 'rag-insights' },
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Parametres' }, path: 'settings' },
{ code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
{ code: 'editor', label: 'Editor', path: 'editor' },
{ code: 'rag-insights', label: 'Wissens-Insights', path: 'rag-insights' },
{ code: 'settings', label: 'Einstellungen', path: 'settings' },
]
},
};
@ -332,8 +332,20 @@ export function canAccessRecord(
}
/**
* Holt das Label für die aktuelle Sprache
* Holt Navigations-Label: i18n-Key (String) oder Legacy-I18nLabel.
*/
export function getLabel(label: I18nLabel, lang: 'de' | 'en' | 'fr' = 'de'): string {
export function getLabel(label: I18nLabel | string, lang: 'de' | 'en' | 'fr' = 'de'): string {
if (typeof label === 'string') return label;
return label[lang] || label.de || label.en || '';
}
/** German i18n key from API label (string or legacy multilingual object). */
export function labelAsI18nKey(
label: string | I18nLabel | { [key: string]: string } | undefined,
fallback: string
): string {
if (label === undefined || label === null) return fallback;
if (typeof label === 'string') return label || fallback;
const o = label as I18nLabel;
return o.de || o.en || o.fr || fallback;
}