fix(journal): Modul-Button folgt dem Mandanten des offenen Einsatzes (v1.8.6)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 45m49s

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) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 20:36:04 +02:00
parent aaeeee1717
commit f9ad00a7e7
5 changed files with 47 additions and 19 deletions

View File

@@ -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": {

View File

@@ -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 },
})

View File

@@ -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}

View File

@@ -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<JournalEntry[]>([])
const [checkItems, setCheckItems] = useState<JournalCheckItem[]>([])
@@ -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}
/>

View File

@@ -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<CockpitModule[]>(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')