Phase 1 Sprint C+D: Admin UI + Frontend Sidebar

This commit is contained in:
Pepe Ziberi
2026-05-20 21:29:45 +02:00
parent ca26f1e733
commit 4602de7a38
4 changed files with 963 additions and 332 deletions

View File

@@ -5,10 +5,11 @@ import { getSession } from '@/lib/auth'
export async function GET() {
try {
const user = await getSession()
const tenantId = user?.tenantId
// Filter categories: global (tenantId=null) + tenant-specific
const categoryWhere: any = user?.tenantId
? { OR: [{ tenantId: null }, { tenantId: user.tenantId }] }
/* ─── 1. Global library (legacy IconAsset) ─── */
const categoryWhere: any = tenantId
? { OR: [{ tenantId: null }, { tenantId }] }
: {}
const categories = await (prisma as any).iconCategory.findMany({
@@ -16,19 +17,18 @@ export async function GET() {
orderBy: { sortOrder: 'asc' },
include: {
icons: {
where: user?.tenantId
? { isActive: true, OR: [{ tenantId: null }, { tenantId: user.tenantId }] }
where: tenantId
? { isActive: true, OR: [{ tenantId: null }, { tenantId }] }
: { isActive: true, tenantId: null },
orderBy: { name: 'asc' },
},
},
})
// Get tenant's hidden icon IDs (legacy)
let hiddenIconIds: string[] = []
if (user?.tenantId) {
if (tenantId) {
const tenant = await (prisma as any).tenant.findUnique({
where: { id: user.tenantId },
where: { id: tenantId },
select: { hiddenIconIds: true },
})
hiddenIconIds = tenant?.hiddenIconIds || []
@@ -44,15 +44,72 @@ export async function GET() {
})),
}))
// Get tenant's custom symbol collection (with custom names)
let mySymbols: any[] = []
if (user?.tenantId) {
/* ─── 2. Tenant symbols (Phase 1 architecture) ─── */
let tenantSymbolGroups: any[] = []
let flatTenantSymbols: any[] = []
if (tenantId) {
const tenantSymbols = await (prisma as any).tenantSymbol.findMany({
where: { tenantId: user.tenantId },
where: { tenantId },
include: { category: true },
orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }],
})
flatTenantSymbols = tenantSymbols.map((ts: any) => ({
id: ts.id,
name: ts.customName || ts.name,
customName: ts.customName,
categoryId: ts.categoryId,
categoryName: ts.category?.name || null,
isUploaded: ts.isUploaded,
migratedFromIconId: ts.migratedFromIconId,
imageUrl: ts.isUploaded
? `/api/tenant/symbols/${ts.id}/image`
: ts.migratedFromIconId
? `/api/icons/${ts.migratedFromIconId}/image`
: `/api/icons/${ts.id}/image`,
}))
// Group by category for sidebar display
const groups = new Map<string | null, any[]>()
for (const sym of flatTenantSymbols) {
const key = sym.categoryId || null
if (!groups.has(key)) groups.set(key, [])
groups.get(key)!.push(sym)
}
// Fetch categories that have symbols but may not be in the symbol list (empty ones are omitted)
const catIds = Array.from(groups.keys()).filter(Boolean) as string[]
const tenantCategories = catIds.length
? await (prisma as any).tenantCategory.findMany({
where: { id: { in: catIds }, tenantId },
orderBy: { sortOrder: 'asc' },
})
: []
const catMap = new Map(tenantCategories.map((c: any) => [c.id, c]))
tenantSymbolGroups = Array.from(groups.entries()).map(([catId, symbols]) => {
const cat = catId ? catMap.get(catId) : null
return {
categoryId: catId,
categoryName: cat ? (cat as any).name || 'Kategorie' : 'Ohne Kategorie',
sortOrder: cat ? (cat as any).sortOrder ?? 999 : 999,
symbols,
}
})
tenantSymbolGroups.sort((a, b) => a.sortOrder - b.sortOrder)
}
/* ─── 3. Legacy mySymbols (keep for old clients during transition) ─── */
let mySymbolsLegacy: any[] = []
if (tenantId) {
const legacy = await (prisma as any).tenantSymbol.findMany({
where: { tenantId, iconId: { not: null } },
include: { icon: { select: { id: true, name: true, mimeType: true, iconType: true } } },
orderBy: { sortOrder: 'asc' },
})
mySymbols = tenantSymbols.map((ts: any) => ({
mySymbolsLegacy = legacy.map((ts: any) => ({
id: ts.icon.id,
tenantSymbolId: ts.id,
name: ts.customName || ts.icon.name,
@@ -63,7 +120,12 @@ export async function GET() {
}))
}
return NextResponse.json({ categories: categoriesWithUrls, mySymbols })
return NextResponse.json({
categories: categoriesWithUrls,
mySymbols: mySymbolsLegacy, // legacy shape for old clients
tenantSymbols: flatTenantSymbols, // new flat list
tenantSymbolGroups, // grouped by TenantCategory
})
} catch (error) {
console.error('Error fetching icons:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })

View File

@@ -0,0 +1,83 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { getFileStream } from '@/lib/minio'
import { readFile } from 'fs/promises'
import { join } from 'path'
async function getTenantId() {
const { headers } = await import('next/headers')
const h = await headers()
return h.get('x-tenant-id') || 'default'
}
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const tenantId = await getTenantId()
const symbol = await (prisma as any).tenantSymbol.findFirst({
where: { id, tenantId },
include: { category: true },
})
if (!symbol) {
return new NextResponse('Not found', { status: 404 })
}
// 1. Uploaded file → MinIO
if (symbol.isUploaded && symbol.svgPath) {
try {
const { stream, contentType } = await getFileStream(symbol.svgPath)
const chunks: Buffer[] = []
for await (const chunk of stream as any) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
const buffer = Buffer.concat(chunks)
return new NextResponse(buffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
},
})
} catch (err) {
console.error('MinIO stream error:', err)
return new NextResponse('File not available', { status: 502 })
}
}
// 2. Migrated from IconAsset → serve from public/signaturen
if (symbol.migratedFromIconId) {
const icon = await (prisma as any).iconAsset.findUnique({
where: { id: symbol.migratedFromIconId },
})
if (icon?.fileKey) {
const filePath = join(process.cwd(), 'public', icon.fileKey)
try {
const buffer = await readFile(filePath)
const ext = icon.fileKey.split('.').pop()?.toLowerCase()
const mimeType =
ext === 'svg' ? 'image/svg+xml' :
ext === 'png' ? 'image/png' :
ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' :
'application/octet-stream'
return new NextResponse(buffer, {
headers: {
'Content-Type': mimeType,
'Cache-Control': 'public, max-age=86400',
},
})
} catch {
// fall through to 404
}
}
}
return new NextResponse('Not found', { status: 404 })
} catch (err) {
console.error('Tenant symbol image error:', err)
return new NextResponse('Internal error', { status: 500 })
}
}