From ca26f1e733451e9477a8d909d7c7d6cfe9712917 Mon Sep 17 00:00:00 2001 From: Pepe Ziberi Date: Wed, 20 May 2026 21:19:17 +0200 Subject: [PATCH] Phase 1 Sprint B: Neue Tenant-Symbol APIs --- src/app/api/icons/[id]/image/route.ts | 47 ++++- src/app/api/templates/import/route.ts | 116 +++++++++++ src/app/api/templates/route.ts | 50 +++++ src/app/api/tenant/categories/route.ts | 185 ++++++++++++++++++ src/app/api/tenant/symbols/route.ts | 256 ++++++++++++++++++++----- 5 files changed, 602 insertions(+), 52 deletions(-) create mode 100644 src/app/api/templates/import/route.ts create mode 100644 src/app/api/templates/route.ts create mode 100644 src/app/api/tenant/categories/route.ts diff --git a/src/app/api/icons/[id]/image/route.ts b/src/app/api/icons/[id]/image/route.ts index 2b81337..737e0c6 100644 --- a/src/app/api/icons/[id]/image/route.ts +++ b/src/app/api/icons/[id]/image/route.ts @@ -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 }) } diff --git a/src/app/api/templates/import/route.ts b/src/app/api/templates/import/route.ts new file mode 100644 index 0000000..c85bd46 --- /dev/null +++ b/src/app/api/templates/import/route.ts @@ -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() // categoryName -> TenantCategory.id + const uniqueCategoryNames: string[] = [...new Set(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 }) + } +} diff --git a/src/app/api/templates/route.ts b/src/app/api/templates/route.ts new file mode 100644 index 0000000..41ab93e --- /dev/null +++ b/src/app/api/templates/route.ts @@ -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 }) + } +} diff --git a/src/app/api/tenant/categories/route.ts b/src/app/api/tenant/categories/route.ts new file mode 100644 index 0000000..885c8c6 --- /dev/null +++ b/src/app/api/tenant/categories/route.ts @@ -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 }) + } +} diff --git a/src/app/api/tenant/symbols/route.ts b/src/app/api/tenant/symbols/route.ts index 54a543a..3aae328 100644 --- a/src/app/api/tenant/symbols/route.ts +++ b/src/app/api/tenant/symbols/route.ts @@ -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 = {} + 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)