feat(15-07): Aktivierungsdialog mit drei Aktionen in /admin/modules

- Neue ActivateModuleDialog.tsx: Abbrechen / "Spaeter konfigurieren" (nur
  POST /modules/:id/activate) / "Sofort freigeben" (POST .../activate
  gefolgt von POST /module-grants fuer die als Standard markierte Gruppe) --
  zwei getrennte Aufrufe, kein neuer kombinierter Endpoint (D-10)
- Laedt GET /groups beim Oeffnen; ohne markierte Standardgruppe ist "Sofort
  freigeben" disabled mit Hinweistext (D-13)
- admin/modules/page.tsx: toggleModule-Klick verzweigt -- Deaktivierung
  bleibt direkt, Aktivierung oeffnet den Dialog statt sofort zu aktivieren;
  Erfolg aktualisiert Aktivierungs-Map + Sidebar-Refresh wie bisher
- grants-matrix.test.tsx erweitert um 3 Tests fuer den Dialog (alle drei
  Buttons vorhanden, Sofort-freigeben deaktiviert ohne Standardgruppe,
  Aufrufreihenfolge activate->grant)
This commit is contained in:
2026-08-04 19:21:25 +02:00
parent 6b2a6f1ce4
commit 3cd6d997cf
3 changed files with 323 additions and 1 deletions
@@ -5,6 +5,7 @@ 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';
@@ -40,6 +41,10 @@ export default function AdminModulesPage() {
const [loading, setLoading] = useState(true);
const [toggling, setToggling] = useState<string | null>(null);
const [error, setError] = useState<string | null>(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<Module | null>(null);
const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN';
@@ -192,7 +197,16 @@ export default function AdminModulesPage() {
{isActive ? t('activated') : t('deactivated')}
</span>
<button
onClick={() => toggleModule(mod.id, isActive)}
onClick={() => {
if (isActive) {
// Deactivation is unchanged -- direct toggle, no dialog.
toggleModule(mod.id, true);
} else {
// Activation opens the confirmation dialog instead of
// activating directly (D-10).
setActivateDialogModule(mod);
}
}}
disabled={isToggling}
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-wait disabled:opacity-50 ${
isActive ? 'bg-primary' : 'bg-input'
@@ -212,6 +226,25 @@ export default function AdminModulesPage() {
})}
</div>
)}
{activateDialogModule && (
<ActivateModuleDialog
open
moduleId={activateDialogModule.id}
moduleName={activateDialogModule.name}
onCancel={() => setActivateDialogModule(null)}
onSuccess={(moduleId) => {
setActivations((prev) => {
const next = new Map(prev);
next.set(moduleId, true);
return next;
});
bumpSidebarRefresh();
setActivateDialogModule(null);
}}
onError={(message) => setError(message)}
/>
)}
</div>
);
}