Phase 1 Sprint C+D: Admin UI + Frontend Sidebar
This commit is contained in:
@@ -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 })
|
||||
|
||||
83
src/app/api/tenant/symbols/[id]/image/route.ts
Normal file
83
src/app/api/tenant/symbols/[id]/image/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user