frontend_nyla/src/pages/Store.tsx
2026-04-09 00:11:35 +02:00

229 lines
8.8 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 />,
};
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.',
},
};
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'] || '';
}
interface FeatureCardProps {
feature: StoreFeature;
language: string;
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,
language,
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}>
{_getLabel(feature.label, language)}
</h3>
</div>
<div className={styles.cardBody}>
<p className={styles.cardDescription}>
{_getDescription(feature.featureCode, language)}
</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
? '...'
: (language === 'de' ? 'Deaktivieren' : language === 'fr' ? 'Desactiver' : 'Deactivate')}
</button>
</div>
))}
</div>
)}
{activeInstances.length === 0 && (
<div>
<span className={`${styles.statusBadge} ${styles.statusInactive}`}>
<span className={styles.statusDot} />
{language === 'de' ? 'Verfuegbar' : language === 'fr' ? 'Disponible' : 'Available'}
</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
? (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}`)}
</button>
))}
</div>
</div>
);
};
const StorePage: React.FC = () => {
const { t, currentLanguage } = 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>
<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')}
</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>
)}
{subscriptionInfo.maxDataVolumeMB != null && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? 'Speicher' : 'Storage'}:{' '}
{formatBinaryDataSizeFromMebibytes(subscriptionInfo.maxDataVolumeMB)}
</span>
)}
{subscriptionInfo.budgetAiCHF != null && subscriptionInfo.budgetAiCHF > 0 && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? 'AI-Budget' : 'AI budget'}: {subscriptionInfo.budgetAiCHF} CHF
</span>
)}
{subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && (
<span className={styles.bannerSeparator}>
{currentLanguage === 'de' ? t('store.trialEndet') : t('store.trialEnds')}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
</span>
)}
</div>
)}
{error && <div className={styles.error}>{error}</div>}
{loading ? (
<div className={styles.loading}>
{currentLanguage === 'de' ? t('store.ladeFeatures') : t('store.loadingFeatures')}
</div>
) : features.length === 0 ? (
<div className={styles.empty}>
{currentLanguage === 'de'
? t('store.keineFeaturesImStoreVerfuegbar')
: t('store.noFeaturesAvailableInThe')}
</div>
) : (
<div className={styles.grid}>
{features.map((feature) => (
<FeatureCard
key={feature.featureCode}
feature={feature}
language={currentLanguage}
mandates={mandates}
actionLoading={actionLoading}
onActivate={activate}
onDeactivate={deactivate}
/>
))}
</div>
)}
</div>
);
};
export default StorePage;