From f9ad00a7e72b2c02db9bb98202f5bc53de91536c Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Thu, 23 Jul 2026 20:36:04 +0200 Subject: [PATCH] fix(journal): Modul-Button folgt dem Mandanten des offenen Einsatzes (v1.8.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ursache: der "Module"-Button hing am Mandanten des Nutzers, nicht am Mandanten des geöffneten Einsatzes. SERVER_ADMIN (ohne eigenen Mandant) oder mandantenfremde Einsätze zeigten daher keinen Button. - /api/tenant/modules: GET/PUT akzeptieren tenantId; SERVER_ADMIN darf den Mandanten des Einsatzes verwalten, andere nur den eigenen (canManage sauber berechnet) - journal-view lädt/speichert Module für den Einsatz-Mandanten (projectTenantId) - ModuleManagerDialog reicht tenantId beim Speichern durch Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- src/app/api/tenant/modules/route.ts | 44 ++++++++++++++----- src/app/app/page.tsx | 1 + src/components/journal/journal-view.tsx | 13 ++++-- .../journal/module-manager-dialog.tsx | 6 ++- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index a3abc72..8b1cfc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lageplan", - "version": "1.8.5", + "version": "1.8.6", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "private": true, "scripts": { diff --git a/src/app/api/tenant/modules/route.ts b/src/app/api/tenant/modules/route.ts index 326470b..3545571 100644 --- a/src/app/api/tenant/modules/route.ts +++ b/src/app/api/tenant/modules/route.ts @@ -3,26 +3,45 @@ import { prisma } from '@/lib/db' import { getSession } from '@/lib/auth' import { normalizeModules, DEFAULT_MODULES } from '@/lib/modules' -/** Cockpit-Modul-Konfiguration des eigenen Mandanten lesen */ -export async function GET() { +/** + * Ermittelt den massgeblichen Mandanten für die Modul-Verwaltung. + * SERVER_ADMIN darf einen beliebigen Mandanten (z.B. den des offenen Einsatzes) verwalten; + * andere nur ihren eigenen. `canManage` sagt, ob dieser Nutzer speichern darf. + */ +function resolveTenant(user: any, requestedTenantId: string | null): { tenantId: string | null; canManage: boolean } { + const isServerAdmin = user.role === 'SERVER_ADMIN' + const isTenantAdmin = user.role === 'TENANT_ADMIN' + if (isServerAdmin) { + // SERVER_ADMIN: nimmt den angefragten Mandanten (Einsatz), sonst den eigenen (falls vorhanden) + return { tenantId: requestedTenantId || user.tenantId || null, canManage: true } + } + // Alle anderen: immer nur der eigene Mandant. Admin dieses Mandanten darf verwalten. + return { tenantId: user.tenantId || null, canManage: isTenantAdmin && !!user.tenantId } +} + +/** Cockpit-Modul-Konfiguration lesen (Mandant = eigener oder, für SERVER_ADMIN, der des Einsatzes) */ +export async function GET(request: NextRequest) { try { const user = await getSession() if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 }) - if (!user.tenantId) { - // SERVER_ADMIN ohne Mandant: Standard-Cockpit + const requested = request.nextUrl.searchParams.get('tenantId') + const { tenantId, canManage } = resolveTenant(user, requested) + + if (!tenantId) { + // Kein Mandant auflösbar: Standard-Cockpit, nicht verwaltbar return NextResponse.json({ modules: DEFAULT_MODULES, canManage: false }) } const tenant = await (prisma as any).tenant.findUnique({ - where: { id: user.tenantId }, + where: { id: tenantId }, select: { modulesConfig: true }, }) - const canManage = user.role === 'SERVER_ADMIN' || user.role === 'TENANT_ADMIN' return NextResponse.json({ modules: normalizeModules(tenant?.modulesConfig), canManage, + tenantId, }) } catch (error) { console.error('Error fetching module config:', error) @@ -30,22 +49,23 @@ export async function GET() { } } -/** Cockpit-Modul-Konfiguration speichern (nur Admins) */ +/** Cockpit-Modul-Konfiguration speichern (nur Admins des betroffenen Mandanten) */ export async function PUT(request: NextRequest) { try { const user = await getSession() if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 }) - if (!user.tenantId) return NextResponse.json({ error: 'Kein Mandant zugeordnet' }, { status: 400 }) - if (user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN') { - return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }) - } const body = await request.json() + const { tenantId, canManage } = resolveTenant(user, body?.tenantId || null) + + if (!tenantId) return NextResponse.json({ error: 'Kein Mandant zugeordnet' }, { status: 400 }) + if (!canManage) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }) + // normalizeModules verwirft Unbekanntes und stellt eingebaute Module sicher const modules = normalizeModules(body?.modules) await (prisma as any).tenant.update({ - where: { id: user.tenantId }, + where: { id: tenantId }, data: { modulesConfig: modules }, }) diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index 68ca656..ad76f84 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -1165,6 +1165,7 @@ export default function AppPage() { einsatzNr={(currentProject as any)?.einsatzNr || ''} canEdit={canEditJournal} tenantId={tenant?.id || null} + projectTenantId={(currentProject as any)?.tenantId ?? null} tenantName={tenant?.name || ''} tenantLogoUrl={tenant?.id ? `/api/admin/tenants/${tenant.id}/logo/serve` : null} mapRef={mapRef} diff --git a/src/components/journal/journal-view.tsx b/src/components/journal/journal-view.tsx index 749140b..227be7d 100644 --- a/src/components/journal/journal-view.tsx +++ b/src/components/journal/journal-view.tsx @@ -26,6 +26,8 @@ interface JournalViewProps { journalfuehrer: string canEdit: boolean tenantId?: string | null + /** Mandant des OFFENEN Einsatzes (kann von dem des Nutzers abweichen — z.B. SERVER_ADMIN). */ + projectTenantId?: string | null einsatzNr?: string tenantName?: string tenantLogoUrl?: string | null @@ -67,7 +69,7 @@ function ModuleCard({ icon, name, count, children }: { ) } -export function JournalView({ projectId, projectTitle, projectLocation, mode, einsatzleiter, journalfuehrer, canEdit, tenantId, einsatzNr, tenantName, tenantLogoUrl, mapRef, mapScreenshot: preCapuredScreenshot }: JournalViewProps) { +export function JournalView({ projectId, projectTitle, projectLocation, mode, einsatzleiter, journalfuehrer, canEdit, tenantId, projectTenantId, einsatzNr, tenantName, tenantLogoUrl, mapRef, mapScreenshot: preCapuredScreenshot }: JournalViewProps) { const isUebung = mode === 'UEBUNG' const [entries, setEntries] = useState([]) const [checkItems, setCheckItems] = useState([]) @@ -145,16 +147,18 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei } }, [projectId]) - // Cockpit-Modul-Konfiguration laden + // Cockpit-Modul-Konfiguration laden — Mandant des OFFENEN Einsatzes (Fallback: eigener) + const modulesTenantId = projectTenantId || tenantId || null useEffect(() => { - fetch('/api/tenant/modules') + const url = modulesTenantId ? `/api/tenant/modules?tenantId=${encodeURIComponent(modulesTenantId)}` : '/api/tenant/modules' + fetch(url) .then(r => r.ok ? r.json() : null) .then(data => { if (data?.modules) setModules(data.modules) if (data) setCanManageModules(!!data.canManage) }) .catch(() => {}) - }, [tenantId]) + }, [modulesTenantId]) // Init check items from templates if none exist (guarded against double-call) const initCheckItems = useCallback(async () => { @@ -847,6 +851,7 @@ export function JournalView({ projectId, projectTitle, projectLocation, mode, ei open={showModuleManager} onOpenChange={setShowModuleManager} modules={modules} + tenantId={modulesTenantId} onSaved={setModules} /> diff --git a/src/components/journal/module-manager-dialog.tsx b/src/components/journal/module-manager-dialog.tsx index 111ecc0..d4153c3 100644 --- a/src/components/journal/module-manager-dialog.tsx +++ b/src/components/journal/module-manager-dialog.tsx @@ -16,6 +16,8 @@ interface ModuleManagerDialogProps { open: boolean onOpenChange: (open: boolean) => void modules: CockpitModule[] + /** Mandant, dessen Module gespeichert werden (Einsatz-Mandant; für SERVER_ADMIN relevant). */ + tenantId?: string | null onSaved: (modules: CockpitModule[]) => void } @@ -25,7 +27,7 @@ interface ModuleManagerDialogProps { * aus Vorlagen erstellen (Checkliste, Atemschutz, Kräfte, Lagemeldungen, leer). * Gespeichert wird pro Mandant (gilt für alle Einsätze). */ -export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: ModuleManagerDialogProps) { +export function ModuleManagerDialog({ open, onOpenChange, modules, tenantId, onSaved }: ModuleManagerDialogProps) { const [working, setWorking] = useState(modules) const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState('') @@ -133,7 +135,7 @@ export function ModuleManagerDialog({ open, onOpenChange, modules, onSaved }: Mo const res = await fetch('/api/tenant/modules', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modules: working }), + body: JSON.stringify({ modules: working, tenantId: tenantId || undefined }), }) const data = await res.json() if (!res.ok) throw new Error(data.error || 'Speichern fehlgeschlagen')