'use client'; import { useCallback, useEffect, useState } from 'react'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { useAuthStore } from '@/lib/stores/auth-store'; import { useMarketplaceStore } from '@/lib/stores/marketplace-store'; import { ActivateModuleDialog } from './components/ActivateModuleDialog'; const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; interface Module { id: string; slug: string; name: string; version: string; category: string; description: Record | string; icon: string | null; isSystem: boolean; } interface TenantActivation { id: string; moduleId: string; isActive: boolean; } export default function AdminModulesPage() { const t = useTranslations('adminModules'); const tCommon = useTranslations('common'); const currentUser = useAuthStore((s) => s.user); // Shared signal that makes the sidebar refetch its active-module list // (same mechanism the marketplace pages use). Without bumping this after a // toggle here, an activation from the admin page only updates local state — // the sidebar link would not appear until a full page reload. const bumpSidebarRefresh = useMarketplaceStore((s) => s.bumpSidebarRefresh); const [modules, setModules] = useState([]); const [activations, setActivations] = useState>(new Map()); const [loading, setLoading] = useState(true); const [toggling, setToggling] = useState(null); const [error, setError] = useState(null); // Deactivation stays on the direct toggleModule path below. Activation // instead opens this dialog (D-10) -- the module named here is the one // whose "Aktivieren"-switch was just clicked. const [activateDialogModule, setActivateDialogModule] = useState(null); const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN'; const fetchModules = useCallback(async () => { try { const [allRes, activeRes] = await Promise.all([ fetch(`${API_URL}/modules`, { credentials: 'include' }), fetch(`${API_URL}/modules/active`, { credentials: 'include' }), ]); if (allRes.ok) { const allModules: Module[] = await allRes.json(); setModules(allModules); } if (activeRes.ok) { const activeModules: (Module & { activations?: TenantActivation[] })[] = await activeRes.json(); const map = new Map(); for (const mod of activeModules) { map.set(mod.id, true); } setActivations(map); } } catch { // silently fail } finally { setLoading(false); } }, []); useEffect(() => { if (isAdmin) { fetchModules(); } else { setLoading(false); } }, [isAdmin, fetchModules]); const toggleModule = async (moduleId: string, currentlyActive: boolean) => { setToggling(moduleId); setError(null); try { const action = currentlyActive ? 'deactivate' : 'activate'; const res = await fetch(`${API_URL}/modules/${moduleId}/${action}`, { method: 'POST', credentials: 'include', }); if (res.ok) { setActivations((prev) => { const next = new Map(prev); if (currentlyActive) { next.delete(moduleId); } else { next.set(moduleId, true); } return next; }); // Refresh the sidebar so the (de)activated module's link appears/ // disappears immediately, without a manual full page reload. bumpSidebarRefresh(); } else { const body = await res.text().catch(() => ''); setError(`${res.status}: ${body}`); } } catch (err) { setError(String(err)); } finally { setToggling(null); } }; const getDescription = (desc: Record | string): string => { if (typeof desc === 'string') return desc; return desc.de || desc.en || Object.values(desc)[0] || ''; }; if (!isAdmin) { return (

{tCommon('accessDenied')}

); } return (

{t('title')}

{t('description')}

{t('grantsLink')}
{error && (
{error}
)} {loading ? (

{tCommon('loading')}

) : modules.length === 0 ? (

{t('noModules')}

) : (
{modules.map((mod) => { const isActive = activations.has(mod.id); const isToggling = toggling === mod.id; return (

{mod.name}

v{mod.version} {mod.isSystem && ( {t('system')} )} {mod.category}

{getDescription(mod.description)}

{isActive ? t('activated') : t('deactivated')}
); })}
)} {activateDialogModule && ( setActivateDialogModule(null)} onSuccess={(moduleId) => { setActivations((prev) => { const next = new Map(prev); next.set(moduleId, true); return next; }); bumpSidebarRefresh(); setActivateDialogModule(null); }} onError={(message) => setError(message)} /> )}
); }