fix(tenant-symbols): kompletter Endpoint auf Raw-SQL umgestellt
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
@@ -13,13 +13,40 @@ async function getTenantId() {
|
||||
return { tenantId: user.tenantId }
|
||||
}
|
||||
|
||||
async function ensureTables() {
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE TABLE IF NOT EXISTS tenant_symbols (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
"iconId" TEXT REFERENCES icon_assets(id) ON DELETE SET NULL,
|
||||
"name" TEXT,
|
||||
"svgPath" TEXT,
|
||||
"isUploaded" BOOLEAN NOT NULL DEFAULT false,
|
||||
"categoryId" TEXT,
|
||||
"migratedFromIconId" TEXT,
|
||||
"customName" TEXT,
|
||||
"sortOrder" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE TABLE IF NOT EXISTS tenant_categories (
|
||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"tenantId" TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
// ─── 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()
|
||||
@@ -29,22 +56,16 @@ export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const grouped = searchParams.get('grouped') !== 'false'
|
||||
|
||||
let tenantSymbols: any[] = []
|
||||
try {
|
||||
tenantSymbols = await (prisma as any).tenantSymbol.findMany({
|
||||
where: { tenantId },
|
||||
include: {
|
||||
category: { select: { id: true, name: true, sortOrder: true } },
|
||||
},
|
||||
orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }],
|
||||
})
|
||||
} catch (dbErr: any) {
|
||||
console.error('tenantSymbol.findMany failed:', dbErr)
|
||||
if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') {
|
||||
return NextResponse.json({ categories: [], symbols: [] })
|
||||
}
|
||||
throw dbErr
|
||||
}
|
||||
await ensureTables()
|
||||
|
||||
const tenantSymbols: any[] = await prisma.$queryRawUnsafe(
|
||||
`SELECT ts.*, tc.id as "catId", tc.name as "catName", tc."sortOrder" as "catSort"
|
||||
FROM tenant_symbols ts
|
||||
LEFT JOIN tenant_categories tc ON ts."categoryId" = tc.id
|
||||
WHERE ts."tenantId" = $1 AND ts."isActive" = true
|
||||
ORDER BY COALESCE(tc."sortOrder", 9999) ASC, COALESCE(ts."sortOrder", 9999) ASC, COALESCE(ts.name, ts."customName", 'Unbenannt') ASC`,
|
||||
tenantId
|
||||
)
|
||||
|
||||
const mapped = tenantSymbols.map((ts: any) => ({
|
||||
id: ts.id,
|
||||
@@ -52,7 +73,7 @@ export async function GET(req: NextRequest) {
|
||||
customName: ts.customName,
|
||||
svgPath: ts.svgPath,
|
||||
categoryId: ts.categoryId,
|
||||
categoryName: ts.category?.name || 'Ohne Kategorie',
|
||||
categoryName: ts.catName || 'Ohne Kategorie',
|
||||
sortOrder: ts.sortOrder,
|
||||
isUploaded: ts.isUploaded,
|
||||
createdAt: ts.createdAt,
|
||||
@@ -70,21 +91,11 @@ export async function GET(req: NextRequest) {
|
||||
groupedResult[catName].push(sym)
|
||||
}
|
||||
|
||||
// Sort categories by sortOrder from category
|
||||
let categories: any[] = []
|
||||
try {
|
||||
categories = await (prisma as any).tenantCategory.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
})
|
||||
} catch (catErr: any) {
|
||||
console.error('tenantCategory.findMany failed:', catErr)
|
||||
if (catErr?.message?.includes('tenant_categories') || catErr?.code === 'P2021') {
|
||||
categories = []
|
||||
} else {
|
||||
throw catErr
|
||||
}
|
||||
}
|
||||
// Get categories for ordering
|
||||
const categories: any[] = await prisma.$queryRawUnsafe(
|
||||
`SELECT * FROM tenant_categories WHERE "tenantId" = $1 AND "isActive" = true ORDER BY "sortOrder" ASC`,
|
||||
tenantId
|
||||
)
|
||||
|
||||
const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = []
|
||||
for (const cat of categories) {
|
||||
@@ -107,27 +118,23 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// ─── 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
|
||||
|
||||
await ensureTables()
|
||||
|
||||
const contentType = req.headers.get('content-type') || ''
|
||||
|
||||
// Get next sort order
|
||||
const maxSort = await prisma.$queryRawUnsafe(
|
||||
`SELECT MAX("sortOrder") as maxsort FROM tenant_symbols WHERE "tenantId" = $1`,
|
||||
tenantId
|
||||
) as any[]
|
||||
const nextSort = (maxSort[0]?.maxsort ?? -1) + 1
|
||||
|
||||
// ─── File Upload ─────────────────────────────────────────────────────────
|
||||
if (contentType.includes('multipart/form-data')) {
|
||||
const formData = await req.formData()
|
||||
@@ -139,11 +146,6 @@ export async function POST(req: NextRequest) {
|
||||
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'
|
||||
@@ -152,16 +154,13 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`INSERT INTO tenant_symbols (id, "tenantId", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt")
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, true, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
tenantId, name.trim(), fileKey, categoryId || null, nextSort
|
||||
) as any[]
|
||||
const symbol = result[0]
|
||||
|
||||
return NextResponse.json({
|
||||
id: symbol.id,
|
||||
@@ -177,31 +176,24 @@ export async function POST(req: NextRequest) {
|
||||
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) {
|
||||
const tpl = await prisma.$queryRawUnsafe(
|
||||
`SELECT * FROM symbol_templates WHERE id = $1 LIMIT 1`,
|
||||
templateId
|
||||
) as any[]
|
||||
if (!tpl || tpl.length === 0) {
|
||||
return NextResponse.json({ error: 'Template nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
const t = tpl[0]
|
||||
|
||||
const symbol = await (prisma as any).tenantSymbol.create({
|
||||
data: {
|
||||
tenantId,
|
||||
name: tpl.name,
|
||||
svgPath: tpl.svgPath,
|
||||
categoryId: categoryId || null,
|
||||
sortOrder: nextSort,
|
||||
isUploaded: false,
|
||||
},
|
||||
})
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`INSERT INTO tenant_symbols (id, "tenantId", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt")
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, false, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
tenantId, t.name, t.svgPath, categoryId || null, nextSort
|
||||
) as any[]
|
||||
const symbol = result[0]
|
||||
|
||||
return NextResponse.json({
|
||||
id: symbol.id,
|
||||
@@ -226,18 +218,13 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
const symbol = await (prisma as any).tenantSymbol.create({
|
||||
data: {
|
||||
tenantId,
|
||||
iconId: icon.id,
|
||||
customName: customName || null,
|
||||
name: customName || icon.name,
|
||||
svgPath: icon.fileKey,
|
||||
categoryId: categoryId || null,
|
||||
sortOrder: nextSort,
|
||||
isUploaded: false,
|
||||
},
|
||||
})
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`INSERT INTO tenant_symbols (id, "tenantId", "iconId", "customName", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt")
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, false, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
tenantId, icon.id, customName || null, customName || icon.name, icon.fileKey, categoryId || null, nextSort
|
||||
) as any[]
|
||||
const symbol = result[0]
|
||||
|
||||
return NextResponse.json({
|
||||
id: symbol.id,
|
||||
@@ -257,12 +244,6 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// ─── 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()
|
||||
@@ -272,18 +253,22 @@ export async function PATCH(req: NextRequest) {
|
||||
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 ensureTables()
|
||||
|
||||
const result = await (prisma as any).tenantSymbol.updateMany({
|
||||
where: { id, tenantId },
|
||||
data,
|
||||
})
|
||||
const updates: string[] = []
|
||||
const params: any[] = [id, tenantId]
|
||||
if (name !== undefined) { updates.push(`name = $${params.length + 1}`); params.push(name || null); }
|
||||
if (customName !== undefined) { updates.push(`"customName" = $${params.length + 1}`); params.push(customName || null); }
|
||||
if (categoryId !== undefined) { updates.push(`"categoryId" = $${params.length + 1}`); params.push(categoryId || null); }
|
||||
if (sortOrder !== undefined) { updates.push(`"sortOrder" = $${params.length + 1}`); params.push(sortOrder); }
|
||||
updates.push(`"updatedAt" = NOW()`)
|
||||
|
||||
if (result.count === 0) {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`UPDATE tenant_symbols SET ${updates.join(', ')} WHERE id = $1 AND "tenantId" = $2 RETURNING *`,
|
||||
...params
|
||||
) as any[]
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
@@ -296,12 +281,6 @@ export async function PATCH(req: NextRequest) {
|
||||
|
||||
// ─── DELETE ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* DELETE /api/tenant/symbols
|
||||
* Löscht ein TenantSymbol.
|
||||
*
|
||||
* Body: { id: string }
|
||||
*/
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const auth = await getTenantId()
|
||||
@@ -311,11 +290,14 @@ export async function DELETE(req: NextRequest) {
|
||||
const { id } = await req.json()
|
||||
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||
|
||||
const result = await (prisma as any).tenantSymbol.deleteMany({
|
||||
where: { id, tenantId },
|
||||
})
|
||||
await ensureTables()
|
||||
|
||||
if (result.count === 0) {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`DELETE FROM tenant_symbols WHERE id = $1 AND "tenantId" = $2 RETURNING *`,
|
||||
id, tenantId
|
||||
) as any[]
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user