decision subscription
This commit is contained in:
parent
abe6ba60d4
commit
9ac2d5a6c1
21 changed files with 221 additions and 238 deletions
|
|
@ -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: [] },
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) })),
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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})`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue