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 }
|
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 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await getTenantId()
|
const auth = await getTenantId()
|
||||||
@@ -29,22 +56,16 @@ export async function GET(req: NextRequest) {
|
|||||||
const { searchParams } = new URL(req.url)
|
const { searchParams } = new URL(req.url)
|
||||||
const grouped = searchParams.get('grouped') !== 'false'
|
const grouped = searchParams.get('grouped') !== 'false'
|
||||||
|
|
||||||
let tenantSymbols: any[] = []
|
await ensureTables()
|
||||||
try {
|
|
||||||
tenantSymbols = await (prisma as any).tenantSymbol.findMany({
|
const tenantSymbols: any[] = await prisma.$queryRawUnsafe(
|
||||||
where: { tenantId },
|
`SELECT ts.*, tc.id as "catId", tc.name as "catName", tc."sortOrder" as "catSort"
|
||||||
include: {
|
FROM tenant_symbols ts
|
||||||
category: { select: { id: true, name: true, sortOrder: true } },
|
LEFT JOIN tenant_categories tc ON ts."categoryId" = tc.id
|
||||||
},
|
WHERE ts."tenantId" = $1 AND ts."isActive" = true
|
||||||
orderBy: [{ category: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { name: 'asc' }],
|
ORDER BY COALESCE(tc."sortOrder", 9999) ASC, COALESCE(ts."sortOrder", 9999) ASC, COALESCE(ts.name, ts."customName", 'Unbenannt') ASC`,
|
||||||
})
|
tenantId
|
||||||
} 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
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapped = tenantSymbols.map((ts: any) => ({
|
const mapped = tenantSymbols.map((ts: any) => ({
|
||||||
id: ts.id,
|
id: ts.id,
|
||||||
@@ -52,7 +73,7 @@ export async function GET(req: NextRequest) {
|
|||||||
customName: ts.customName,
|
customName: ts.customName,
|
||||||
svgPath: ts.svgPath,
|
svgPath: ts.svgPath,
|
||||||
categoryId: ts.categoryId,
|
categoryId: ts.categoryId,
|
||||||
categoryName: ts.category?.name || 'Ohne Kategorie',
|
categoryName: ts.catName || 'Ohne Kategorie',
|
||||||
sortOrder: ts.sortOrder,
|
sortOrder: ts.sortOrder,
|
||||||
isUploaded: ts.isUploaded,
|
isUploaded: ts.isUploaded,
|
||||||
createdAt: ts.createdAt,
|
createdAt: ts.createdAt,
|
||||||
@@ -70,21 +91,11 @@ export async function GET(req: NextRequest) {
|
|||||||
groupedResult[catName].push(sym)
|
groupedResult[catName].push(sym)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort categories by sortOrder from category
|
// Get categories for ordering
|
||||||
let categories: any[] = []
|
const categories: any[] = await prisma.$queryRawUnsafe(
|
||||||
try {
|
`SELECT * FROM tenant_categories WHERE "tenantId" = $1 AND "isActive" = true ORDER BY "sortOrder" ASC`,
|
||||||
categories = await (prisma as any).tenantCategory.findMany({
|
tenantId
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = []
|
const ordered: Array<{ categoryId: string | null; categoryName: string; symbols: any[] }> = []
|
||||||
for (const cat of categories) {
|
for (const cat of categories) {
|
||||||
@@ -107,27 +118,23 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
// ─── POST ───────────────────────────────────────────────────────────────────
|
// ─── 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
|
||||||
|
|
||||||
|
await ensureTables()
|
||||||
|
|
||||||
const contentType = req.headers.get('content-type') || ''
|
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 ─────────────────────────────────────────────────────────
|
// ─── File Upload ─────────────────────────────────────────────────────────
|
||||||
if (contentType.includes('multipart/form-data')) {
|
if (contentType.includes('multipart/form-data')) {
|
||||||
const formData = await req.formData()
|
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 })
|
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 bytes = await file.arrayBuffer()
|
||||||
const buffer = Buffer.from(bytes)
|
const buffer = Buffer.from(bytes)
|
||||||
const ext = file.name.split('.').pop()?.toLowerCase() || 'svg'
|
const ext = file.name.split('.').pop()?.toLowerCase() || 'svg'
|
||||||
@@ -152,16 +154,13 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
await uploadFile(fileKey, buffer, mimeType)
|
await uploadFile(fileKey, buffer, mimeType)
|
||||||
|
|
||||||
const symbol = await (prisma as any).tenantSymbol.create({
|
const result = await prisma.$queryRawUnsafe(
|
||||||
data: {
|
`INSERT INTO tenant_symbols (id, "tenantId", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt")
|
||||||
tenantId,
|
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, true, NOW(), NOW())
|
||||||
name: name.trim(),
|
RETURNING *`,
|
||||||
svgPath: fileKey,
|
tenantId, name.trim(), fileKey, categoryId || null, nextSort
|
||||||
categoryId: categoryId || null,
|
) as any[]
|
||||||
sortOrder: (maxSortAgg._max.sortOrder ?? -1) + 1,
|
const symbol = result[0]
|
||||||
isUploaded: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: symbol.id,
|
id: symbol.id,
|
||||||
@@ -177,31 +176,24 @@ export async function POST(req: NextRequest) {
|
|||||||
const body = await req.json()
|
const body = await req.json()
|
||||||
const { iconId, customName, templateId, categoryId } = body
|
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
|
// From SymbolTemplate
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
const tpl = await (prisma as any).symbolTemplate.findUnique({
|
const tpl = await prisma.$queryRawUnsafe(
|
||||||
where: { id: templateId },
|
`SELECT * FROM symbol_templates WHERE id = $1 LIMIT 1`,
|
||||||
})
|
templateId
|
||||||
if (!tpl) {
|
) as any[]
|
||||||
|
if (!tpl || tpl.length === 0) {
|
||||||
return NextResponse.json({ error: 'Template nicht gefunden' }, { status: 404 })
|
return NextResponse.json({ error: 'Template nicht gefunden' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
const t = tpl[0]
|
||||||
|
|
||||||
const symbol = await (prisma as any).tenantSymbol.create({
|
const result = await prisma.$queryRawUnsafe(
|
||||||
data: {
|
`INSERT INTO tenant_symbols (id, "tenantId", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt")
|
||||||
tenantId,
|
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, false, NOW(), NOW())
|
||||||
name: tpl.name,
|
RETURNING *`,
|
||||||
svgPath: tpl.svgPath,
|
tenantId, t.name, t.svgPath, categoryId || null, nextSort
|
||||||
categoryId: categoryId || null,
|
) as any[]
|
||||||
sortOrder: nextSort,
|
const symbol = result[0]
|
||||||
isUploaded: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: symbol.id,
|
id: symbol.id,
|
||||||
@@ -226,18 +218,13 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 })
|
return NextResponse.json({ error: 'Icon nicht gefunden' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const symbol = await (prisma as any).tenantSymbol.create({
|
const result = await prisma.$queryRawUnsafe(
|
||||||
data: {
|
`INSERT INTO tenant_symbols (id, "tenantId", "iconId", "customName", name, "svgPath", "categoryId", "sortOrder", "isUploaded", "createdAt", "updatedAt")
|
||||||
tenantId,
|
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, false, NOW(), NOW())
|
||||||
iconId: icon.id,
|
RETURNING *`,
|
||||||
customName: customName || null,
|
tenantId, icon.id, customName || null, customName || icon.name, icon.fileKey, categoryId || null, nextSort
|
||||||
name: customName || icon.name,
|
) as any[]
|
||||||
svgPath: icon.fileKey,
|
const symbol = result[0]
|
||||||
categoryId: categoryId || null,
|
|
||||||
sortOrder: nextSort,
|
|
||||||
isUploaded: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: symbol.id,
|
id: symbol.id,
|
||||||
@@ -257,12 +244,6 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// ─── PATCH ──────────────────────────────────────────────────────────────────
|
// ─── 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()
|
||||||
@@ -272,18 +253,22 @@ export async function PATCH(req: NextRequest) {
|
|||||||
const { id, name, customName, categoryId, 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 = {}
|
await ensureTables()
|
||||||
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
|
|
||||||
|
|
||||||
const result = await (prisma as any).tenantSymbol.updateMany({
|
const updates: string[] = []
|
||||||
where: { id, tenantId },
|
const params: any[] = [id, tenantId]
|
||||||
data,
|
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 })
|
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,12 +281,6 @@ export async function PATCH(req: NextRequest) {
|
|||||||
|
|
||||||
// ─── DELETE ─────────────────────────────────────────────────────────────────
|
// ─── 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()
|
||||||
@@ -311,11 +290,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 })
|
||||||
|
|
||||||
const result = await (prisma as any).tenantSymbol.deleteMany({
|
await ensureTables()
|
||||||
where: { id, tenantId },
|
|
||||||
})
|
|
||||||
|
|
||||||
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 })
|
return NextResponse.json({ error: 'Symbol nicht gefunden' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user