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 } }
|
||||
) {
|
||||
try {
|
||||
const icon = await prisma.iconAsset.findUnique({
|
||||
where: { id: params.id },
|
||||
const 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) {
|
||||
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 { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { uploadFile } from '@/lib/minio'
|
||||
|
||||
async function getTenantId() {
|
||||
const user = await getSession()
|
||||
@@ -12,117 +13,261 @@ async function getTenantId() {
|
||||
return { tenantId: user.tenantId }
|
||||
}
|
||||
|
||||
// GET: Returns library (all system icons) + tenant's own symbol collection
|
||||
export async function GET() {
|
||||
// ─── 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 {
|
||||
const auth = await getTenantId()
|
||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||
const { tenantId } = auth
|
||||
|
||||
// All system icons grouped by category (the library)
|
||||
const icons = await (prisma as any).iconAsset.findMany({
|
||||
where: { isActive: true },
|
||||
include: { category: { select: { id: true, name: true } } },
|
||||
orderBy: [{ category: { sortOrder: 'asc' } }, { name: 'asc' }],
|
||||
})
|
||||
const { searchParams } = new URL(req.url)
|
||||
const grouped = searchParams.get('grouped') !== 'false'
|
||||
|
||||
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({
|
||||
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' },
|
||||
})
|
||||
|
||||
const mySymbols = tenantSymbols.map((ts: any) => ({
|
||||
id: ts.id,
|
||||
iconId: ts.iconId,
|
||||
name: ts.customName || ts.icon.name,
|
||||
customName: ts.customName,
|
||||
baseName: ts.icon.name,
|
||||
mimeType: ts.icon.mimeType,
|
||||
iconType: ts.icon.iconType,
|
||||
categoryName: ts.icon.category?.name || 'Ohne Kategorie',
|
||||
sortOrder: ts.sortOrder,
|
||||
}))
|
||||
const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = []
|
||||
for (const cat of categories) {
|
||||
if (groupedResult[cat.name]) {
|
||||
ordered.push({ categoryId: cat.id, categoryName: cat.name, symbols: groupedResult[cat.name] })
|
||||
delete groupedResult[cat.name]
|
||||
}
|
||||
}
|
||||
// Append any remaining uncategorized
|
||||
for (const [catName, symbols] of Object.entries(groupedResult)) {
|
||||
ordered.push({ categoryId: null, categoryName: catName, symbols })
|
||||
}
|
||||
|
||||
return NextResponse.json({ library, mySymbols })
|
||||
return NextResponse.json({ categories: ordered })
|
||||
} catch (error) {
|
||||
console.error('Error fetching tenant symbols:', error)
|
||||
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) {
|
||||
try {
|
||||
const auth = await getTenantId()
|
||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||
const { tenantId } = auth
|
||||
|
||||
const { iconId, customName } = await req.json()
|
||||
if (!iconId) return NextResponse.json({ error: 'iconId erforderlich' }, { status: 400 })
|
||||
const contentType = req.headers.get('content-type') || ''
|
||||
|
||||
// Get max sortOrder for this tenant
|
||||
const maxSort = await (prisma as any).tenantSymbol.aggregate({
|
||||
// ─── File Upload ─────────────────────────────────────────────────────────
|
||||
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 },
|
||||
_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({
|
||||
data: {
|
||||
tenantId,
|
||||
iconId,
|
||||
iconId: icon.id,
|
||||
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({
|
||||
id: symbol.id,
|
||||
iconId: symbol.iconId,
|
||||
name: symbol.customName || symbol.icon.name,
|
||||
name: symbol.name,
|
||||
customName: symbol.customName,
|
||||
baseName: symbol.icon.name,
|
||||
mimeType: symbol.icon.mimeType,
|
||||
iconType: symbol.icon.iconType,
|
||||
categoryName: symbol.icon.category?.name || 'Ohne Kategorie',
|
||||
svgPath: symbol.svgPath,
|
||||
categoryId: symbol.categoryId,
|
||||
sortOrder: symbol.sortOrder,
|
||||
})
|
||||
isUploaded: false,
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error adding tenant symbol:', error)
|
||||
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) {
|
||||
try {
|
||||
const auth = await getTenantId()
|
||||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||
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 })
|
||||
|
||||
const data: any = {}
|
||||
if (name !== undefined) data.name = name || null
|
||||
if (customName !== undefined) data.customName = customName || null
|
||||
if (categoryId !== undefined) data.categoryId = categoryId || null
|
||||
if (sortOrder !== undefined) data.sortOrder = sortOrder
|
||||
|
||||
await (prisma as any).tenantSymbol.updateMany({
|
||||
const result = await (prisma as any).tenantSymbol.updateMany({
|
||||
where: { id, tenantId },
|
||||
data,
|
||||
})
|
||||
|
||||
if (result.count === 0) {
|
||||
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (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) {
|
||||
try {
|
||||
const auth = await getTenantId()
|
||||
@@ -140,10 +292,14 @@ export async function DELETE(req: NextRequest) {
|
||||
const { id } = await req.json()
|
||||
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 },
|
||||
})
|
||||
|
||||
if (result.count === 0) {
|
||||
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting tenant symbol:', error)
|
||||
|
||||
Reference in New Issue
Block a user