197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
/**
|
|
* Feature Store -- Users activate feature instances in their own mandates.
|
|
* Uses the Own Instance Pattern -- each activation creates a dedicated FeatureInstance
|
|
* in the selected mandate. Explicit mandate selection required.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { FaCogs, FaComments, FaHeadset, FaProjectDiagram } from 'react-icons/fa';
|
|
import { useLanguage } from '../providers/language/LanguageContext';
|
|
import { useStore } from '../hooks/useStore';
|
|
import type { StoreFeature, UserMandate } from '../api/storeApi';
|
|
import { formatBinaryDataSizeFromMebibytes } from '../utils/formatDataSize';
|
|
import styles from './Store.module.css';
|
|
|
|
const FEATURE_ICONS: Record<string, React.ReactNode> = {
|
|
automation: <FaCogs />,
|
|
graphicalEditor: <FaProjectDiagram />,
|
|
teamsbot: <FaHeadset />,
|
|
workspace: <FaComments />,
|
|
commcoach: <FaComments />,
|
|
};
|
|
|
|
/** 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 _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;
|
|
mandates: UserMandate[];
|
|
actionLoading: string | null;
|
|
onActivate: (code: string, mandateId?: string) => void;
|
|
onDeactivate: (code: string, mandateId: string, instanceId: string) => void;
|
|
}
|
|
|
|
const FeatureCard: React.FC<FeatureCardProps> = ({
|
|
feature,
|
|
mandates,
|
|
actionLoading,
|
|
onActivate,
|
|
onDeactivate,
|
|
}) => {
|
|
const { t } = useLanguage();
|
|
const isProcessing = actionLoading === feature.featureCode;
|
|
const icon = FEATURE_ICONS[feature.featureCode];
|
|
const activeInstances = feature.instances.filter(inst => inst.isActive);
|
|
const hasActive = activeInstances.length > 0;
|
|
|
|
return (
|
|
<div className={`${styles.card} ${hasActive ? styles.cardActive : ''}`}>
|
|
<div className={styles.cardHeader}>
|
|
{icon && <span className={styles.cardIcon}>{icon}</span>}
|
|
<h3 className={styles.cardTitle}>
|
|
{t(feature.label)}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className={styles.cardBody}>
|
|
<p className={styles.cardDescription}>
|
|
{_storeCardDescription(feature, t)}
|
|
</p>
|
|
</div>
|
|
|
|
{activeInstances.length > 0 && (
|
|
<div className={styles.instanceList}>
|
|
{activeInstances.map((inst) => (
|
|
<div key={inst.instanceId} className={styles.instanceRow}>
|
|
<div className={styles.instanceInfo}>
|
|
<span className={`${styles.statusBadge} ${styles.statusActive}`}>
|
|
<span className={styles.statusDot} />
|
|
{inst.mandateName || inst.label}
|
|
</span>
|
|
</div>
|
|
<button
|
|
className={styles.deactivateButtonSmall}
|
|
onClick={() => onDeactivate(feature.featureCode, inst.mandateId, inst.instanceId)}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? '...' : t('Deaktivieren')}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeInstances.length === 0 && (
|
|
<div>
|
|
<span className={`${styles.statusBadge} ${styles.statusInactive}`}>
|
|
<span className={styles.statusDot} />
|
|
{t('Verfügbar')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.cardActions}>
|
|
{feature.canActivate && mandates.map((m) => (
|
|
<button
|
|
key={m.id}
|
|
className={styles.activateButton}
|
|
onClick={() => onActivate(feature.featureCode, m.id)}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing
|
|
? t('Wird aktiviert…')
|
|
: t('Aktivieren für {name}', { name: String(m.label || m.name) })}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const StorePage: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
const { features, mandates, subscriptionInfo, loading, actionLoading, error, activate, deactivate } = useStore();
|
|
|
|
return (
|
|
<div className={styles.store}>
|
|
<div className={styles.header}>
|
|
<h1>{t('Feature Store')}</h1>
|
|
<p className={styles.subtitle}>
|
|
{t('Aktiviere Features für dein Konto. Deine Daten sind isoliert und nur für dich sichtbar.')}
|
|
</p>
|
|
</div>
|
|
|
|
{subscriptionInfo && subscriptionInfo.plan && (
|
|
<div className={styles.subscriptionBanner}>
|
|
<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}>
|
|
{t('Speicher')}:{' '}
|
|
{formatBinaryDataSizeFromMebibytes(subscriptionInfo.maxDataVolumeMB)}
|
|
</span>
|
|
)}
|
|
{subscriptionInfo.budgetAiPerUserCHF != null && subscriptionInfo.budgetAiPerUserCHF > 0 && (
|
|
<span className={styles.bannerSeparator}>
|
|
{t('AI-Budget')}: {subscriptionInfo.budgetAiPerUserCHF} CHF / User
|
|
</span>
|
|
)}
|
|
{subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && (
|
|
<span className={styles.bannerSeparator}>
|
|
{t('Testphase endet am')}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{error && <div className={styles.error}>{error}</div>}
|
|
|
|
{loading ? (
|
|
<div className={styles.loading}>
|
|
{t('Lade Features…')}
|
|
</div>
|
|
) : features.length === 0 ? (
|
|
<div className={styles.empty}>
|
|
{t('Keine Features im Store verfügbar.')}
|
|
</div>
|
|
) : (
|
|
<div className={styles.grid}>
|
|
{features.map((feature) => (
|
|
<FeatureCard
|
|
key={feature.featureCode}
|
|
feature={feature}
|
|
mandates={mandates}
|
|
actionLoading={actionLoading}
|
|
onActivate={activate}
|
|
onDeactivate={deactivate}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StorePage;
|