commit
9a59cdac86
10 changed files with 582 additions and 42 deletions
|
|
@ -36,6 +36,7 @@ import { FeatureLayout } from './layouts/FeatureLayout';
|
|||
import { DashboardPage } from './pages/Dashboard';
|
||||
import { SettingsPage } from './pages/Settings';
|
||||
import { GDPRPage } from './pages/GDPR';
|
||||
import StorePage from './pages/Store';
|
||||
import { FeatureViewPage } from './pages/FeatureView';
|
||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage, AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin';
|
||||
import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
|
||||
|
|
@ -94,6 +95,7 @@ function App() {
|
|||
<Route index element={<DashboardPage />} />
|
||||
|
||||
{/* System-Seiten (ohne Instanz-Kontext) */}
|
||||
<Route path="store" element={<StorePage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="gdpr" element={<GDPRPage />} />
|
||||
|
||||
|
|
|
|||
|
|
@ -92,25 +92,6 @@ export async function fetchRoleById(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch role options
|
||||
* Endpoint: GET /api/rbac/roles/options
|
||||
*/
|
||||
export async function fetchRoleOptions(
|
||||
request: ApiRequestFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/rbac/roles/options',
|
||||
method: 'get'
|
||||
});
|
||||
return data || null;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching role options:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new role
|
||||
* Endpoint: POST /api/rbac/roles
|
||||
|
|
|
|||
47
src/api/storeApi.ts
Normal file
47
src/api/storeApi.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Store API
|
||||
*
|
||||
* API layer for the Feature Store.
|
||||
* Manages feature activation/deactivation in the root mandate's shared instances.
|
||||
*/
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export interface StoreFeature {
|
||||
featureCode: string;
|
||||
label: Record<string, string>;
|
||||
icon: string;
|
||||
description: Record<string, string>;
|
||||
isActive: boolean;
|
||||
canActivate: boolean;
|
||||
instanceId: string | null;
|
||||
}
|
||||
|
||||
export interface StoreActivateResponse {
|
||||
featureCode: string;
|
||||
instanceId: string;
|
||||
featureAccessId: string;
|
||||
roleId: string | null;
|
||||
activated: boolean;
|
||||
}
|
||||
|
||||
export interface StoreDeactivateResponse {
|
||||
featureCode: string;
|
||||
instanceId: string;
|
||||
deactivated: boolean;
|
||||
}
|
||||
|
||||
export async function fetchStoreFeatures(): Promise<StoreFeature[]> {
|
||||
const response = await api.get<StoreFeature[]>('/api/store/features');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function activateStoreFeature(featureCode: string): Promise<StoreActivateResponse> {
|
||||
const response = await api.post<StoreActivateResponse>('/api/store/activate', { featureCode });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deactivateStoreFeature(featureCode: string): Promise<StoreDeactivateResponse> {
|
||||
const response = await api.post<StoreDeactivateResponse>('/api/store/deactivate', { featureCode });
|
||||
return response.data;
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
FaLightbulb, FaRegFileAlt, FaLink, FaComments,
|
||||
FaListAlt, FaCogs, FaChartLine, FaFileAlt, FaUserShield, FaDatabase,
|
||||
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock,
|
||||
FaHeadset, FaVideo, FaHatWizard,
|
||||
FaHeadset, FaVideo, FaHatWizard, FaStore,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -36,6 +36,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
// System pages
|
||||
'page.system.home': <FaHome />,
|
||||
'page.system.settings': <FaCog />,
|
||||
'page.system.store': <FaStore />,
|
||||
'page.system.gdpr': <FaShieldAlt />,
|
||||
|
||||
// Basedata pages (system-level)
|
||||
|
|
|
|||
|
|
@ -240,25 +240,6 @@ export function useMandateRoles() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get role options (for dropdowns)
|
||||
*/
|
||||
const fetchRoleOptions = useCallback(async (): Promise<Array<{ value: string; label: string }>> => {
|
||||
try {
|
||||
const response = await api.get('/api/rbac/roles/options');
|
||||
if (Array.isArray(response.data)) {
|
||||
return response.data.map((r: any) => ({
|
||||
value: r.id || r.value,
|
||||
label: r.roleLabel || r.label || r.id
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching role options:', err);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get users with a specific role
|
||||
*/
|
||||
|
|
@ -305,7 +286,6 @@ export function useMandateRoles() {
|
|||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
fetchRoleOptions,
|
||||
getUsersWithRole,
|
||||
getMandateRoles,
|
||||
getFeatureRoles,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,14 @@ export function useNavigation(language: string = 'de'): UseNavigationReturn {
|
|||
fetchNavigation();
|
||||
}, [fetchNavigation]);
|
||||
|
||||
useEffect(() => {
|
||||
const onFeaturesChanged = () => {
|
||||
fetchNavigation();
|
||||
};
|
||||
window.addEventListener('features-changed', onFeaturesChanged);
|
||||
return () => window.removeEventListener('features-changed', onFeaturesChanged);
|
||||
}, [fetchNavigation]);
|
||||
|
||||
// Derive static and dynamic blocks
|
||||
const staticBlocks = blocks.filter(isStaticBlock);
|
||||
const dynamicBlock = blocks.find(isDynamicBlock) || null;
|
||||
|
|
|
|||
90
src/hooks/useStore.ts
Normal file
90
src/hooks/useStore.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* useStore Hook
|
||||
*
|
||||
* Manages feature store interactions: loading catalog, activating/deactivating features.
|
||||
* After each mutation, refreshes featureStore and dispatches 'features-changed' event
|
||||
* so navigation and other components update in real-time.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
fetchStoreFeatures,
|
||||
activateStoreFeature,
|
||||
deactivateStoreFeature,
|
||||
type StoreFeature,
|
||||
} from '../api/storeApi';
|
||||
import { useFeatureStore } from '../stores/featureStore';
|
||||
|
||||
interface UseStoreReturn {
|
||||
features: StoreFeature[];
|
||||
loading: boolean;
|
||||
actionLoading: string | null;
|
||||
error: string | null;
|
||||
loadStore: () => Promise<void>;
|
||||
activate: (featureCode: string) => Promise<void>;
|
||||
deactivate: (featureCode: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useStore(): UseStoreReturn {
|
||||
const [features, setFeatures] = useState<StoreFeature[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const featureStore = useFeatureStore();
|
||||
|
||||
const loadStore = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await fetchStoreFeatures();
|
||||
setFeatures(data);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : 'Failed to load store';
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadStore();
|
||||
}, [loadStore]);
|
||||
|
||||
const _refreshAfterAction = useCallback(async () => {
|
||||
await featureStore.loadFeatures();
|
||||
window.dispatchEvent(new CustomEvent('features-changed'));
|
||||
await loadStore();
|
||||
}, [featureStore, loadStore]);
|
||||
|
||||
const activate = useCallback(async (featureCode: string) => {
|
||||
setActionLoading(featureCode);
|
||||
setError(null);
|
||||
try {
|
||||
await activateStoreFeature(featureCode);
|
||||
await _refreshAfterAction();
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : 'Activation failed';
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
}, [_refreshAfterAction]);
|
||||
|
||||
const deactivate = useCallback(async (featureCode: string) => {
|
||||
setActionLoading(featureCode);
|
||||
setError(null);
|
||||
try {
|
||||
await deactivateStoreFeature(featureCode);
|
||||
await _refreshAfterAction();
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : 'Deactivation failed';
|
||||
setError(msg);
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
}, [_refreshAfterAction]);
|
||||
|
||||
return { features, loading, actionLoading, error, loadStore, activate, deactivate };
|
||||
}
|
||||
|
||||
export default useStore;
|
||||
|
|
@ -236,8 +236,11 @@ export function useUserMandates() {
|
|||
*/
|
||||
const fetchRoles = useCallback(async (mandateId?: string): Promise<Role[]> => {
|
||||
try {
|
||||
// Fetch roles server-side filtered by mandate (or templates if no mandateId)
|
||||
const params = mandateId ? { mandateId } : {};
|
||||
const params: Record<string, string> = {};
|
||||
if (mandateId) {
|
||||
params.mandateId = mandateId;
|
||||
params.scopeFilter = 'mandate';
|
||||
}
|
||||
const response = await api.get('/api/rbac/roles', { params });
|
||||
let roles: Role[] = [];
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
|
|
|
|||
262
src/pages/Store.module.css
Normal file
262
src/pages/Store.module.css
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/**
|
||||
* Store Page Styles
|
||||
*/
|
||||
|
||||
.store {
|
||||
padding: 2rem;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.card {
|
||||
background: var(--surface-color, #ffffff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--border-color, #ccc);
|
||||
}
|
||||
|
||||
.cardActive {
|
||||
border-color: var(--primary-color, #2563eb);
|
||||
background: var(--primary-bg, rgba(37, 99, 235, 0.04));
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
font-size: 1.75rem;
|
||||
color: var(--primary-color, #2563eb);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.cardBody {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #666);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Status Badge */
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.statusActive {
|
||||
background: var(--success-bg, #ecfdf5);
|
||||
color: var(--success-color, #059669);
|
||||
}
|
||||
|
||||
.statusInactive {
|
||||
background: var(--surface-color, #f5f5f5);
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.cardActions {
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.activateButton {
|
||||
width: 100%;
|
||||
padding: 0.625rem 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--primary-color, #2563eb);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.activateButton:hover:not(:disabled) {
|
||||
background: var(--primary-hover, #1d4ed8);
|
||||
}
|
||||
|
||||
.deactivateButton {
|
||||
width: 100%;
|
||||
padding: 0.625rem 1rem;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.deactivateButton:hover:not(:disabled) {
|
||||
border-color: var(--error-color, #dc2626);
|
||||
color: var(--error-color, #dc2626);
|
||||
background: var(--error-bg, #fef2f2);
|
||||
}
|
||||
|
||||
.activateButton:disabled,
|
||||
.deactivateButton:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.error {
|
||||
background: var(--error-bg, #fef2f2);
|
||||
border: 1px solid var(--error-border, #fecaca);
|
||||
color: var(--error-color, #dc2626);
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Empty */
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Dark Theme */
|
||||
:global(.dark-theme) .header h1 {
|
||||
color: var(--text-primary-dark, #ffffff);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .subtitle {
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .card {
|
||||
background: var(--surface-dark, #1a1a1a);
|
||||
border-color: var(--border-dark, #333);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
border-color: var(--border-dark, #555);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .cardActive {
|
||||
border-color: var(--primary-color, #2563eb);
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .cardTitle {
|
||||
color: var(--text-primary-dark, #ffffff);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .cardDescription {
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .statusActive {
|
||||
background: rgba(5, 150, 105, 0.15);
|
||||
color: var(--success-color, #34d399);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .statusInactive {
|
||||
background: var(--surface-dark, #2a2a2a);
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .cardActions {
|
||||
border-top-color: var(--border-dark, #333);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .deactivateButton {
|
||||
border-color: var(--border-dark, #444);
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .deactivateButton:hover:not(:disabled) {
|
||||
border-color: var(--error-color-dark, #f87171);
|
||||
color: var(--error-color-dark, #f87171);
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .error {
|
||||
background: var(--error-bg-dark, #450a0a);
|
||||
border-color: var(--error-border-dark, #991b1b);
|
||||
color: var(--error-color-dark, #f87171);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .empty {
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .loading {
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
166
src/pages/Store.tsx
Normal file
166
src/pages/Store.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Store Page
|
||||
*
|
||||
* Feature Store where users can self-activate features in the root mandate.
|
||||
* Uses the Shared Instance Pattern -- each feature has one shared instance,
|
||||
* and users get their own FeatureAccess + user-role upon activation.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FaCogs, FaComments, FaHeadset } from 'react-icons/fa';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { StoreFeature } from '../api/storeApi';
|
||||
import styles from './Store.module.css';
|
||||
|
||||
const FEATURE_ICONS: Record<string, React.ReactNode> = {
|
||||
automation: <FaCogs />,
|
||||
chatplayground: <FaComments />,
|
||||
teamsbot: <FaHeadset />,
|
||||
};
|
||||
|
||||
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.',
|
||||
},
|
||||
chatplayground: {
|
||||
de: 'Teste und experimentiere mit AI-Chat-Modellen in einer interaktiven Umgebung.',
|
||||
en: 'Test and experiment with AI chat models in an interactive environment.',
|
||||
fr: 'Testez et experimentez avec des modeles de chat IA dans un environnement interactif.',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
actionLoading: string | null;
|
||||
onActivate: (code: string) => void;
|
||||
onDeactivate: (code: string) => void;
|
||||
}
|
||||
|
||||
const FeatureCard: React.FC<FeatureCardProps> = ({
|
||||
feature,
|
||||
language,
|
||||
actionLoading,
|
||||
onActivate,
|
||||
onDeactivate,
|
||||
}) => {
|
||||
const isProcessing = actionLoading === feature.featureCode;
|
||||
const icon = FEATURE_ICONS[feature.featureCode];
|
||||
|
||||
return (
|
||||
<div className={`${styles.card} ${feature.isActive ? 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>
|
||||
|
||||
<div>
|
||||
<span className={`${styles.statusBadge} ${feature.isActive ? styles.statusActive : styles.statusInactive}`}>
|
||||
<span className={styles.statusDot} />
|
||||
{feature.isActive
|
||||
? (language === 'de' ? 'Aktiv' : language === 'fr' ? 'Actif' : 'Active')
|
||||
: (language === 'de' ? 'Verfuegbar' : language === 'fr' ? 'Disponible' : 'Available')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.cardActions}>
|
||||
{feature.isActive ? (
|
||||
<button
|
||||
className={styles.deactivateButton}
|
||||
onClick={() => onDeactivate(feature.featureCode)}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing
|
||||
? (language === 'de' ? 'Wird deaktiviert...' : 'Deactivating...')
|
||||
: (language === 'de' ? 'Deaktivieren' : language === 'fr' ? 'Desactiver' : 'Deactivate')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className={styles.activateButton}
|
||||
onClick={() => onActivate(feature.featureCode)}
|
||||
disabled={isProcessing || !feature.canActivate}
|
||||
>
|
||||
{isProcessing
|
||||
? (language === 'de' ? 'Wird aktiviert...' : 'Activating...')
|
||||
: (language === 'de' ? 'Aktivieren' : language === 'fr' ? 'Activer' : 'Activate')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StorePage: React.FC = () => {
|
||||
const { currentLanguage } = useLanguage();
|
||||
const { features, loading, actionLoading, error, activate, deactivate } = useStore();
|
||||
|
||||
return (
|
||||
<div className={styles.store}>
|
||||
<div className={styles.header}>
|
||||
<h1>{currentLanguage === 'de' ? 'Feature Store' : currentLanguage === 'fr' ? 'Feature Store' : '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'
|
||||
? 'Activez des fonctionnalites pour votre compte. Vos donnees sont isolees et visibles uniquement par vous.'
|
||||
: 'Activate features for your account. Your data is isolated and only visible to you.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div className={styles.loading}>
|
||||
{currentLanguage === 'de' ? 'Lade Features...' : 'Loading features...'}
|
||||
</div>
|
||||
) : features.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
{currentLanguage === 'de'
|
||||
? 'Keine Features im Store verfuegbar.'
|
||||
: 'No features available in the store.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.grid}>
|
||||
{features.map((feature) => (
|
||||
<FeatureCard
|
||||
key={feature.featureCode}
|
||||
feature={feature}
|
||||
language={currentLanguage}
|
||||
actionLoading={actionLoading}
|
||||
onActivate={activate}
|
||||
onDeactivate={deactivate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StorePage;
|
||||
Loading…
Reference in a new issue