diff --git a/package-lock.json b/package-lock.json index a66f475..689e120 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lageplan", - "version": "1.2.2", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lageplan", - "version": "1.2.2", + "version": "1.3.1", "hasInstallScript": true, "dependencies": { "@dnd-kit/core": "^6.1.0", diff --git a/package.json b/package.json index a6facb1..429053b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lageplan", - "version": "1.2.2", + "version": "1.3.1", "description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation", "private": true, "scripts": { diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index f143388..f5a6b4a 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -53,6 +53,7 @@ import { BookOpen, AlertTriangle, LayoutGrid, + Building2, } from 'lucide-react' import Link from 'next/link' import { TenantDetailDialog } from '@/components/admin/tenant-detail-dialog' @@ -62,6 +63,7 @@ import { SomaTab } from '@/components/admin/soma-tab' import { SuggestionsTab } from '@/components/admin/suggestions-tab' import { DictionaryTab } from '@/components/admin/dictionary-tab' import { SymbolManager } from '@/components/admin/symbol-manager' +import { OrgTab } from '@/components/admin/org-tab' // --- Types --- interface IconCategory { @@ -133,7 +135,7 @@ export default function AdminPage() { const [tenants, setTenants] = useState([]) const [selectedCategory, setSelectedCategory] = useState('all') const [isLoading, setIsLoading] = useState(true) - const [activeTab, setActiveTab] = useState(user?.role === 'SERVER_ADMIN' ? 'tenants' : 'users') + const [activeTab, setActiveTab] = useState(user?.role === 'SERVER_ADMIN' ? 'tenants' : 'org') // Category Dialog const [isCategoryDialogOpen, setIsCategoryDialogOpen] = useState(false) @@ -571,10 +573,22 @@ export default function AdminPage() { ) : user?.role === 'TENANT_ADMIN' ? ( + + + Organisation + Benutzer + + + Symbole + + + + SOMA + Wörterliste @@ -587,21 +601,16 @@ export default function AdminPage() { Spenden - - - Symbole - - - - Kategorien - - - - SOMA - ) : null} + {/* ===== ORGANISATION TAB (TENANT_ADMIN) ===== */} + {user?.role === 'TENANT_ADMIN' && ( + + + + )} + {/* ===== ICONS TAB ===== */} {user?.role === 'TENANT_ADMIN' ? ( diff --git a/src/app/api/tenant/info/route.ts b/src/app/api/tenant/info/route.ts index fcb92ba..7923338 100644 --- a/src/app/api/tenant/info/route.ts +++ b/src/app/api/tenant/info/route.ts @@ -17,9 +17,13 @@ export async function GET(req: NextRequest) { id: true, name: true, slug: true, + description: true, + contactEmail: true, + contactPhone: true, + address: true, + logoUrl: true, plan: true, subscriptionStatus: true, - contactEmail: true, privacyAccepted: true, privacyAcceptedAt: true, adminAccessAccepted: true, @@ -39,3 +43,35 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }) } } + +export async function PATCH(req: NextRequest) { + try { + const user = await getSession() + if (!user) return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 }) + if (user.role !== 'TENANT_ADMIN') return NextResponse.json({ error: 'Nur Admin' }, { status: 403 }) + if (!user.tenantId) return NextResponse.json({ error: 'Kein Mandant' }, { status: 400 }) + + const body = await req.json() + const { name, description, contactEmail, contactPhone, address } = body + + if (!name || !name.trim()) { + return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 }) + } + + const updated = await (prisma as any).tenant.update({ + where: { id: user.tenantId }, + data: { + name: name.trim(), + description: description || null, + contactEmail: contactEmail || null, + contactPhone: contactPhone || null, + address: address || null, + }, + }) + + return NextResponse.json({ tenant: updated }) + } catch (error: any) { + console.error('[Tenant Info PATCH] Error:', error?.message) + return NextResponse.json({ error: 'Serverfehler' }, { status: 500 }) + } +} diff --git a/src/app/api/tenant/logo/route.ts b/src/app/api/tenant/logo/route.ts new file mode 100644 index 0000000..eff660f --- /dev/null +++ b/src/app/api/tenant/logo/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/db' +import { getSession } from '@/lib/auth' +import { uploadFile, deleteFile } from '@/lib/minio' + +export async function POST(req: NextRequest) { + try { + const user = await getSession() + if (!user || user.role !== 'TENANT_ADMIN') { + return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 403 }) + } + if (!user.tenantId) { + return NextResponse.json({ error: 'Kein Mandant' }, { status: 400 }) + } + + const formData = await req.formData() + const file = formData.get('logo') as File + if (!file) { + return NextResponse.json({ error: 'Keine Datei hochgeladen' }, { status: 400 }) + } + + const validTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'] + if (!validTypes.includes(file.type)) { + return NextResponse.json({ error: 'Ungültiges Dateiformat. Erlaubt: PNG, JPEG, SVG, WebP' }, { status: 400 }) + } + + if (file.size > 2 * 1024 * 1024) { + return NextResponse.json({ error: 'Datei zu gross (max. 2 MB)' }, { status: 400 }) + } + + const buffer = Buffer.from(await file.arrayBuffer()) + const ext = file.name.split('.').pop() || 'png' + const fileKey = `logos/tenant-${user.tenantId}.${ext}` + + await uploadFile(fileKey, buffer, file.type) + + const logoServeUrl = `/api/admin/tenants/${user.tenantId}/logo/serve` + await (prisma as any).tenant.update({ + where: { id: user.tenantId }, + data: { logoFileKey: fileKey, logoUrl: logoServeUrl }, + }) + + return NextResponse.json({ logoUrl: logoServeUrl }) + } catch (error) { + console.error('Tenant logo upload error:', error) + return NextResponse.json({ error: 'Upload fehlgeschlagen' }, { status: 500 }) + } +} + +export async function DELETE(req: NextRequest) { + try { + const user = await getSession() + if (!user || user.role !== 'TENANT_ADMIN') { + return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 403 }) + } + if (!user.tenantId) { + return NextResponse.json({ error: 'Kein Mandant' }, { status: 400 }) + } + + const tenant = await (prisma as any).tenant.findUnique({ where: { id: user.tenantId } }) + if (tenant?.logoFileKey) { + try { + await deleteFile(tenant.logoFileKey) + } catch {} + } + + await (prisma as any).tenant.update({ + where: { id: user.tenantId }, + data: { logoUrl: null, logoFileKey: null }, + }) + + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Tenant logo delete error:', error) + return NextResponse.json({ error: 'Löschen fehlgeschlagen' }, { status: 500 }) + } +} diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index 6d8320b..22c73d6 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -862,7 +862,7 @@ export default function AppPage() { Niemand bearbeitet gerade )} -
+
{roleCanEdit && !isEditingByMe && !isReadOnly && (