Phase 1 Sprint B: Neue Tenant-Symbol APIs
This commit is contained in:
@@ -9,10 +9,53 @@ export async function GET(
|
|||||||
{ params }: { params: { id: string } }
|
{ params }: { params: { id: string } }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const icon = await prisma.iconAsset.findUnique({
|
const id = params.id
|
||||||
where: { id: params.id },
|
|
||||||
|
// ─── 1. Try TenantSymbol first (Phase 1 architecture) ───
|
||||||
|
const tenantSymbol = await (prisma as any).tenantSymbol.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { svgPath: true, iconId: true, name: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (tenantSymbol?.svgPath) {
|
||||||
|
// Serve from public/signaturen/ or MinIO
|
||||||
|
const contentType = tenantSymbol.svgPath.endsWith('.svg') ? 'image/svg+xml' : 'image/png'
|
||||||
|
if (tenantSymbol.svgPath.startsWith('signaturen/')) {
|
||||||
|
try {
|
||||||
|
const filePath = join(process.cwd(), 'public', tenantSymbol.svgPath)
|
||||||
|
const buffer = await readFile(filePath)
|
||||||
|
return new NextResponse(buffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Cache-Control': 'public, max-age=31536000',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
console.error('TenantSymbol file not found:', tenantSymbol.svgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not in public/, try MinIO
|
||||||
|
try {
|
||||||
|
const { stream, contentType: ct } = await getFileStream(tenantSymbol.svgPath)
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(Buffer.from(chunk))
|
||||||
|
}
|
||||||
|
const buffer = Buffer.concat(chunks)
|
||||||
|
return new NextResponse(buffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': ct || contentType,
|
||||||
|
'Cache-Control': 'public, max-age=86400',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
console.error('Error streaming TenantSymbol from MinIO:', tenantSymbol.svgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. Fallback: IconAsset (legacy architecture) ───
|
||||||
|
const icon = await prisma.iconAsset.findUnique({ where: { id } })
|
||||||
|
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 })
|
return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|||||||
116
src/app/api/templates/import/route.ts
Normal file
116
src/app/api/templates/import/route.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { prisma } from '@/lib/db'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
|
||||||
|
async function getTenantId() {
|
||||||
|
const user = await getSession()
|
||||||
|
if (!user) return { error: 'Nicht autorisiert', status: 401 }
|
||||||
|
if (user.role !== 'TENANT_ADMIN' && user.role !== 'SERVER_ADMIN') {
|
||||||
|
return { error: 'Keine Berechtigung', status: 403 }
|
||||||
|
}
|
||||||
|
if (!user.tenantId) return { error: 'Kein Mandant zugeordnet', status: 400 }
|
||||||
|
return { tenantId: user.tenantId }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/templates/import
|
||||||
|
* Importiert Symbole aus einem Template-Paket als TenantSymbols.
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* {
|
||||||
|
* packageId: "feuerwehr-ch",
|
||||||
|
* symbolIds?: ["id1", "id2"], // optional: nur ausgewählte, sonst alle
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* { imported: [{ tenantSymbolId, name, categoryId }] }
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await getTenantId()
|
||||||
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
|
const { tenantId } = auth
|
||||||
|
|
||||||
|
const { packageId, symbolIds } = await req.json()
|
||||||
|
if (!packageId) return NextResponse.json({ error: 'packageId erforderlich' }, { status: 400 })
|
||||||
|
|
||||||
|
// 1. Fetch templates to import
|
||||||
|
const where: any = { packageId }
|
||||||
|
if (symbolIds && Array.isArray(symbolIds) && symbolIds.length > 0) {
|
||||||
|
where.id = { in: symbolIds }
|
||||||
|
}
|
||||||
|
|
||||||
|
const templates = await (prisma as any).symbolTemplate.findMany({ where })
|
||||||
|
if (templates.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'Keine Symbole im Paket gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ensure default category exists for each template category
|
||||||
|
const categoryMap = new Map<string, string>() // categoryName -> TenantCategory.id
|
||||||
|
const uniqueCategoryNames: string[] = [...new Set<string>(templates.map((t: any) => t.categoryName as string))]
|
||||||
|
|
||||||
|
for (const catName of uniqueCategoryNames) {
|
||||||
|
let tenantCat = await (prisma as any).tenantCategory.findFirst({
|
||||||
|
where: { tenantId, name: catName },
|
||||||
|
})
|
||||||
|
if (!tenantCat) {
|
||||||
|
tenantCat = await (prisma as any).tenantCategory.create({
|
||||||
|
data: { tenantId, name: catName, sortOrder: 0 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
categoryMap.set(catName, tenantCat.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get current max sortOrder for tenant
|
||||||
|
const maxSortAgg = await (prisma as any).tenantSymbol.aggregate({
|
||||||
|
where: { tenantId },
|
||||||
|
_max: { sortOrder: true },
|
||||||
|
})
|
||||||
|
let currentSort = (maxSortAgg._max.sortOrder ?? -1) + 1
|
||||||
|
|
||||||
|
// 4. Import each template as TenantSymbol
|
||||||
|
const imported: Array<{
|
||||||
|
tenantSymbolId: string
|
||||||
|
name: string
|
||||||
|
categoryId: string
|
||||||
|
svgPath: string
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
for (const tpl of templates) {
|
||||||
|
const categoryId = categoryMap.get(tpl.categoryName)
|
||||||
|
if (!categoryId) continue
|
||||||
|
|
||||||
|
// Check if already exists (same name + svgPath in this tenant)
|
||||||
|
const existing = await (prisma as any).tenantSymbol.findFirst({
|
||||||
|
where: { tenantId, name: tpl.name, svgPath: tpl.svgPath },
|
||||||
|
})
|
||||||
|
if (existing) {
|
||||||
|
// Skip already imported symbols
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantSymbol = await (prisma as any).tenantSymbol.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
categoryId,
|
||||||
|
name: tpl.name,
|
||||||
|
svgPath: tpl.svgPath,
|
||||||
|
sortOrder: currentSort++,
|
||||||
|
isUploaded: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
imported.push({
|
||||||
|
tenantSymbolId: tenantSymbol.id,
|
||||||
|
name: tenantSymbol.name,
|
||||||
|
categoryId: tenantSymbol.categoryId,
|
||||||
|
svgPath: tenantSymbol.svgPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ imported, count: imported.length })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing templates:', error)
|
||||||
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/app/api/templates/route.ts
Normal file
50
src/app/api/templates/route.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { prisma } from '@/lib/db'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/templates
|
||||||
|
* Listet alle verfügbaren Vorlagen-Pakete mit Vorschau.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const packages = await (prisma as any).symbolTemplate.groupBy({
|
||||||
|
by: ['packageId', 'packageName'],
|
||||||
|
_count: { id: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await Promise.all(
|
||||||
|
packages.map(async (pkg: any) => {
|
||||||
|
// Get category breakdown for this package
|
||||||
|
const categories = await (prisma as any).symbolTemplate.groupBy({
|
||||||
|
by: ['categoryName'],
|
||||||
|
where: { packageId: pkg.packageId },
|
||||||
|
_count: { id: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get a few preview symbols
|
||||||
|
const previews = await (prisma as any).symbolTemplate.findMany({
|
||||||
|
where: { packageId: pkg.packageId },
|
||||||
|
take: 5,
|
||||||
|
select: { id: true, name: true, svgPath: true, categoryName: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
packageId: pkg.packageId,
|
||||||
|
packageName: pkg.packageName,
|
||||||
|
symbolCount: pkg._count.id,
|
||||||
|
categoryCount: categories.length,
|
||||||
|
categories: categories.map((c: any) => ({
|
||||||
|
categoryName: c.categoryName,
|
||||||
|
symbolCount: c._count.id,
|
||||||
|
})),
|
||||||
|
previews,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({ packages: result })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching templates:', error)
|
||||||
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/app/api/tenant/categories/route.ts
Normal file
185
src/app/api/tenant/categories/route.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { prisma } from '@/lib/db'
|
||||||
|
import { getSession } from '@/lib/auth'
|
||||||
|
|
||||||
|
async function getTenantId() {
|
||||||
|
const user = await getSession()
|
||||||
|
if (!user) return { error: 'Nicht autorisiert', status: 401 }
|
||||||
|
if (user.role !== 'TENANT_ADMIN' && user.role !== 'SERVER_ADMIN') {
|
||||||
|
return { error: 'Keine Berechtigung', status: 403 }
|
||||||
|
}
|
||||||
|
if (!user.tenantId) return { error: 'Kein Mandant zugeordnet', status: 400 }
|
||||||
|
return { tenantId: user.tenantId }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── GET ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/tenant/categories
|
||||||
|
* Liefert alle Tenant-Kategorien des aktuellen Mandanten mit Symbol-Zählung.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const auth = await getTenantId()
|
||||||
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
|
const { tenantId } = auth
|
||||||
|
|
||||||
|
const categories = await (prisma as any).tenantCategory.findMany({
|
||||||
|
where: { tenantId },
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: { symbols: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { sortOrder: 'asc' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = categories.map((cat: any) => ({
|
||||||
|
id: cat.id,
|
||||||
|
name: cat.name,
|
||||||
|
sortOrder: cat.sortOrder,
|
||||||
|
icon: cat.icon,
|
||||||
|
symbolCount: cat._count.symbols,
|
||||||
|
createdAt: cat.createdAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({ categories: result })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tenant categories:', error)
|
||||||
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── POST ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/tenant/categories
|
||||||
|
* Legt eine neue Kategorie an.
|
||||||
|
*
|
||||||
|
* Body: { name: string, sortOrder?: number, icon?: string }
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await getTenantId()
|
||||||
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
|
const { tenantId } = auth
|
||||||
|
|
||||||
|
const { name, sortOrder, icon } = await req.json()
|
||||||
|
if (!name || typeof name !== 'string') {
|
||||||
|
return NextResponse.json({ error: 'Name erforderlich' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate name within tenant
|
||||||
|
const existing = await (prisma as any).tenantCategory.findFirst({
|
||||||
|
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
|
||||||
|
})
|
||||||
|
if (existing) {
|
||||||
|
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await (prisma as any).tenantCategory.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
name: name.trim(),
|
||||||
|
sortOrder: sortOrder ?? 0,
|
||||||
|
icon: icon || null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ category }, { status: 201 })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating tenant category:', error)
|
||||||
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PATCH ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/tenant/categories
|
||||||
|
* Aktualisiert eine Kategorie.
|
||||||
|
*
|
||||||
|
* Body: { id: string, name?: string, sortOrder?: number, icon?: string }
|
||||||
|
*/
|
||||||
|
export async function PATCH(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await getTenantId()
|
||||||
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
|
const { tenantId } = auth
|
||||||
|
|
||||||
|
const { id, name, sortOrder, icon } = await req.json()
|
||||||
|
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||||
|
|
||||||
|
const data: any = {}
|
||||||
|
if (name !== undefined) data.name = name.trim()
|
||||||
|
if (sortOrder !== undefined) data.sortOrder = sortOrder
|
||||||
|
if (icon !== undefined) data.icon = icon || null
|
||||||
|
|
||||||
|
// If renaming, check for duplicates
|
||||||
|
if (name) {
|
||||||
|
const existing = await (prisma as any).tenantCategory.findFirst({
|
||||||
|
where: {
|
||||||
|
tenantId,
|
||||||
|
name: { equals: name.trim(), mode: 'insensitive' },
|
||||||
|
id: { not: id },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (existing) {
|
||||||
|
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = await (prisma as any).tenantCategory.updateMany({
|
||||||
|
where: { id, tenantId },
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (category.count === 0) {
|
||||||
|
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating tenant category:', error)
|
||||||
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── DELETE ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/tenant/categories
|
||||||
|
* Löscht eine Kategorie (nur wenn leer).
|
||||||
|
*
|
||||||
|
* Body: { id: string }
|
||||||
|
*/
|
||||||
|
export async function DELETE(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const auth = await getTenantId()
|
||||||
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
|
const { tenantId } = auth
|
||||||
|
|
||||||
|
const { id } = await req.json()
|
||||||
|
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||||
|
|
||||||
|
// Check if category has symbols
|
||||||
|
const symbolCount = await (prisma as any).tenantSymbol.count({
|
||||||
|
where: { categoryId: id, tenantId },
|
||||||
|
})
|
||||||
|
if (symbolCount > 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Kategorie enthält ${symbolCount} Symbol(e) — bitte zuerst verschieben oder löschen` },
|
||||||
|
{ status: 409 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await (prisma as any).tenantCategory.deleteMany({
|
||||||
|
where: { id, tenantId },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting tenant category:', error)
|
||||||
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/db'
|
import { prisma } from '@/lib/db'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
|
import { uploadFile } from '@/lib/minio'
|
||||||
|
|
||||||
async function getTenantId() {
|
async function getTenantId() {
|
||||||
const user = await getSession()
|
const user = await getSession()
|
||||||
@@ -12,117 +13,261 @@ async function getTenantId() {
|
|||||||
return { tenantId: user.tenantId }
|
return { tenantId: user.tenantId }
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: Returns library (all system icons) + tenant's own symbol collection
|
// ─── GET ────────────────────────────────────────────────────────────────────
|
||||||
export async function GET() {
|
|
||||||
|
/**
|
||||||
|
* GET /api/tenant/symbols
|
||||||
|
* Liefert die Symbole des Mandanten gruppiert nach TenantCategory.
|
||||||
|
* Optional: ?grouped=true (default) oder ?grouped=false (flache Liste)
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await getTenantId()
|
const auth = await getTenantId()
|
||||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
const { tenantId } = auth
|
const { tenantId } = auth
|
||||||
|
|
||||||
// All system icons grouped by category (the library)
|
const { searchParams } = new URL(req.url)
|
||||||
const icons = await (prisma as any).iconAsset.findMany({
|
const grouped = searchParams.get('grouped') !== 'false'
|
||||||
where: { isActive: true },
|
|
||||||
include: { category: { select: { id: true, name: true } } },
|
|
||||||
orderBy: [{ category: { sortOrder: 'asc' } }, { name: 'asc' }],
|
|
||||||
})
|
|
||||||
|
|
||||||
const library = icons.map((icon: any) => ({
|
|
||||||
id: icon.id,
|
|
||||||
name: icon.name,
|
|
||||||
mimeType: icon.mimeType,
|
|
||||||
iconType: icon.iconType,
|
|
||||||
categoryId: icon.categoryId,
|
|
||||||
categoryName: icon.category?.name || 'Ohne Kategorie',
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Tenant's own symbol collection
|
|
||||||
const tenantSymbols = await (prisma as any).tenantSymbol.findMany({
|
const tenantSymbols = await (prisma as any).tenantSymbol.findMany({
|
||||||
where: { tenantId },
|
where: { tenantId },
|
||||||
include: { icon: { select: { id: true, name: true, mimeType: true, iconType: true, category: { select: { name: true } } } } },
|
include: {
|
||||||
|
category: { select: { id: true, name: true, sortOrder: true } },
|
||||||
|
},
|
||||||
|
orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }],
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapped = tenantSymbols.map((ts: any) => ({
|
||||||
|
id: ts.id,
|
||||||
|
name: ts.name || ts.customName || 'Unbenannt',
|
||||||
|
customName: ts.customName,
|
||||||
|
svgPath: ts.svgPath,
|
||||||
|
categoryId: ts.categoryId,
|
||||||
|
categoryName: ts.category?.name || 'Ohne Kategorie',
|
||||||
|
sortOrder: ts.sortOrder,
|
||||||
|
isUploaded: ts.isUploaded,
|
||||||
|
createdAt: ts.createdAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!grouped) {
|
||||||
|
return NextResponse.json({ symbols: mapped })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by category
|
||||||
|
const groupedResult: Record<string, any[]> = {}
|
||||||
|
for (const sym of mapped) {
|
||||||
|
const catName = sym.categoryName
|
||||||
|
if (!groupedResult[catName]) groupedResult[catName] = []
|
||||||
|
groupedResult[catName].push(sym)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort categories by sortOrder from category
|
||||||
|
const categories = await (prisma as any).tenantCategory.findMany({
|
||||||
|
where: { tenantId },
|
||||||
orderBy: { sortOrder: 'asc' },
|
orderBy: { sortOrder: 'asc' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const mySymbols = tenantSymbols.map((ts: any) => ({
|
const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = []
|
||||||
id: ts.id,
|
for (const cat of categories) {
|
||||||
iconId: ts.iconId,
|
if (groupedResult[cat.name]) {
|
||||||
name: ts.customName || ts.icon.name,
|
ordered.push({ categoryId: cat.id, categoryName: cat.name, symbols: groupedResult[cat.name] })
|
||||||
customName: ts.customName,
|
delete groupedResult[cat.name]
|
||||||
baseName: ts.icon.name,
|
}
|
||||||
mimeType: ts.icon.mimeType,
|
}
|
||||||
iconType: ts.icon.iconType,
|
// Append any remaining uncategorized
|
||||||
categoryName: ts.icon.category?.name || 'Ohne Kategorie',
|
for (const [catName, symbols] of Object.entries(groupedResult)) {
|
||||||
sortOrder: ts.sortOrder,
|
ordered.push({ categoryId: null, categoryName: catName, symbols })
|
||||||
}))
|
}
|
||||||
|
|
||||||
return NextResponse.json({ library, mySymbols })
|
return NextResponse.json({ categories: ordered })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching tenant symbols:', error)
|
console.error('Error fetching tenant symbols:', error)
|
||||||
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: Add a symbol from the library to "my symbols"
|
// ─── POST ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/tenant/symbols
|
||||||
|
* Erstellt ein neues TenantSymbol (manuell oder aus Template/IconAsset).
|
||||||
|
*
|
||||||
|
* Body (manuell aus IconAsset):
|
||||||
|
* { iconId: string, customName?: string, categoryId?: string }
|
||||||
|
*
|
||||||
|
* Body (aus Template-Import):
|
||||||
|
* { templateId: string, categoryId?: string }
|
||||||
|
*
|
||||||
|
* Body (manueller Upload):
|
||||||
|
* FormData mit: file (SVG/PNG), name, categoryId
|
||||||
|
*/
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await getTenantId()
|
const auth = await getTenantId()
|
||||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
const { tenantId } = auth
|
const { tenantId } = auth
|
||||||
|
|
||||||
const { iconId, customName } = await req.json()
|
const contentType = req.headers.get('content-type') || ''
|
||||||
if (!iconId) return NextResponse.json({ error: 'iconId erforderlich' }, { status: 400 })
|
|
||||||
|
|
||||||
// Get max sortOrder for this tenant
|
// ─── File Upload ─────────────────────────────────────────────────────────
|
||||||
const maxSort = await (prisma as any).tenantSymbol.aggregate({
|
if (contentType.includes('multipart/form-data')) {
|
||||||
|
const formData = await req.formData()
|
||||||
|
const file = formData.get('file') as File | null
|
||||||
|
const name = formData.get('name') as string | null
|
||||||
|
const categoryId = formData.get('categoryId') as string | null
|
||||||
|
|
||||||
|
if (!file || !name) {
|
||||||
|
return NextResponse.json({ error: 'Datei und Name erforderlich' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSortAgg = await (prisma as any).tenantSymbol.aggregate({
|
||||||
|
where: { tenantId },
|
||||||
|
_max: { sortOrder: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const bytes = await file.arrayBuffer()
|
||||||
|
const buffer = Buffer.from(bytes)
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase() || 'svg'
|
||||||
|
const mimeType = ext === 'svg' ? 'image/svg+xml' : `image/${ext}`
|
||||||
|
const fileKey = `tenant-${tenantId}/symbols/${Date.now()}-${name.replace(/\s+/g, '_').toLowerCase()}.${ext}`
|
||||||
|
|
||||||
|
await uploadFile(fileKey, buffer, mimeType)
|
||||||
|
|
||||||
|
const symbol = await (prisma as any).tenantSymbol.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
name: name.trim(),
|
||||||
|
svgPath: fileKey,
|
||||||
|
categoryId: categoryId || null,
|
||||||
|
sortOrder: (maxSortAgg._max.sortOrder ?? -1) + 1,
|
||||||
|
isUploaded: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: symbol.id,
|
||||||
|
name: symbol.name,
|
||||||
|
svgPath: symbol.svgPath,
|
||||||
|
categoryId: symbol.categoryId,
|
||||||
|
sortOrder: symbol.sortOrder,
|
||||||
|
isUploaded: true,
|
||||||
|
}, { status: 201 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── JSON (from IconAsset or Template) ──────────────────────────────────
|
||||||
|
const body = await req.json()
|
||||||
|
const { iconId, customName, templateId, categoryId } = body
|
||||||
|
|
||||||
|
const maxSortAgg = await (prisma as any).tenantSymbol.aggregate({
|
||||||
where: { tenantId },
|
where: { tenantId },
|
||||||
_max: { sortOrder: true },
|
_max: { sortOrder: true },
|
||||||
})
|
})
|
||||||
|
const nextSort = (maxSortAgg._max.sortOrder ?? -1) + 1
|
||||||
|
|
||||||
|
// From SymbolTemplate
|
||||||
|
if (templateId) {
|
||||||
|
const tpl = await (prisma as any).symbolTemplate.findUnique({
|
||||||
|
where: { id: templateId },
|
||||||
|
})
|
||||||
|
if (!tpl) {
|
||||||
|
return NextResponse.json({ error: 'Template nicht gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = await (prisma as any).tenantSymbol.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
name: tpl.name,
|
||||||
|
svgPath: tpl.svgPath,
|
||||||
|
categoryId: categoryId || null,
|
||||||
|
sortOrder: nextSort,
|
||||||
|
isUploaded: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: symbol.id,
|
||||||
|
name: symbol.name,
|
||||||
|
svgPath: symbol.svgPath,
|
||||||
|
categoryId: symbol.categoryId,
|
||||||
|
sortOrder: symbol.sortOrder,
|
||||||
|
isUploaded: false,
|
||||||
|
}, { status: 201 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// From IconAsset (legacy path)
|
||||||
|
if (!iconId) {
|
||||||
|
return NextResponse.json({ error: 'iconId oder templateId erforderlich' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = await prisma.iconAsset.findUnique({
|
||||||
|
where: { id: iconId },
|
||||||
|
include: { category: true },
|
||||||
|
})
|
||||||
|
if (!icon) {
|
||||||
|
return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
const symbol = await (prisma as any).tenantSymbol.create({
|
const symbol = await (prisma as any).tenantSymbol.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId,
|
tenantId,
|
||||||
iconId,
|
iconId: icon.id,
|
||||||
customName: customName || null,
|
customName: customName || null,
|
||||||
sortOrder: (maxSort._max.sortOrder ?? -1) + 1,
|
name: customName || icon.name,
|
||||||
|
svgPath: icon.fileKey,
|
||||||
|
categoryId: categoryId || null,
|
||||||
|
sortOrder: nextSort,
|
||||||
|
isUploaded: false,
|
||||||
},
|
},
|
||||||
include: { icon: { select: { name: true, mimeType: true, iconType: true, category: { select: { name: true } } } } },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: symbol.id,
|
id: symbol.id,
|
||||||
iconId: symbol.iconId,
|
iconId: symbol.iconId,
|
||||||
name: symbol.customName || symbol.icon.name,
|
name: symbol.name,
|
||||||
customName: symbol.customName,
|
customName: symbol.customName,
|
||||||
baseName: symbol.icon.name,
|
svgPath: symbol.svgPath,
|
||||||
mimeType: symbol.icon.mimeType,
|
categoryId: symbol.categoryId,
|
||||||
iconType: symbol.icon.iconType,
|
|
||||||
categoryName: symbol.icon.category?.name || 'Ohne Kategorie',
|
|
||||||
sortOrder: symbol.sortOrder,
|
sortOrder: symbol.sortOrder,
|
||||||
})
|
isUploaded: false,
|
||||||
|
}, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding tenant symbol:', error)
|
console.error('Error adding tenant symbol:', error)
|
||||||
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: Rename a symbol or update sortOrder
|
// ─── PATCH ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/tenant/symbols
|
||||||
|
* Aktualisiert ein TenantSymbol.
|
||||||
|
*
|
||||||
|
* Body: { id: string, name?: string, customName?: string, categoryId?: string, sortOrder?: number }
|
||||||
|
*/
|
||||||
export async function PATCH(req: NextRequest) {
|
export async function PATCH(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await getTenantId()
|
const auth = await getTenantId()
|
||||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||||
const { tenantId } = auth
|
const { tenantId } = auth
|
||||||
|
|
||||||
const { id, customName, sortOrder } = await req.json()
|
const { id, name, customName, categoryId, sortOrder } = await req.json()
|
||||||
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||||
|
|
||||||
const data: any = {}
|
const data: any = {}
|
||||||
|
if (name !== undefined) data.name = name || null
|
||||||
if (customName !== undefined) data.customName = customName || null
|
if (customName !== undefined) data.customName = customName || null
|
||||||
|
if (categoryId !== undefined) data.categoryId = categoryId || null
|
||||||
if (sortOrder !== undefined) data.sortOrder = sortOrder
|
if (sortOrder !== undefined) data.sortOrder = sortOrder
|
||||||
|
|
||||||
await (prisma as any).tenantSymbol.updateMany({
|
const result = await (prisma as any).tenantSymbol.updateMany({
|
||||||
where: { id, tenantId },
|
where: { id, tenantId },
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (result.count === 0) {
|
||||||
|
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating tenant symbol:', error)
|
console.error('Error updating tenant symbol:', error)
|
||||||
@@ -130,7 +275,14 @@ export async function PATCH(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: Remove a symbol from "my symbols"
|
// ─── DELETE ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/tenant/symbols
|
||||||
|
* Löscht ein TenantSymbol.
|
||||||
|
*
|
||||||
|
* Body: { id: string }
|
||||||
|
*/
|
||||||
export async function DELETE(req: NextRequest) {
|
export async function DELETE(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await getTenantId()
|
const auth = await getTenantId()
|
||||||
@@ -140,10 +292,14 @@ export async function DELETE(req: NextRequest) {
|
|||||||
const { id } = await req.json()
|
const { id } = await req.json()
|
||||||
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||||
|
|
||||||
await (prisma as any).tenantSymbol.deleteMany({
|
const result = await (prisma as any).tenantSymbol.deleteMany({
|
||||||
where: { id, tenantId },
|
where: { id, tenantId },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (result.count === 0) {
|
||||||
|
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting tenant symbol:', error)
|
console.error('Error deleting tenant symbol:', error)
|
||||||
|
|||||||
Reference in New Issue
Block a user