fix(categories): kompletter Endpoint auf Raw-SQL umgestellt – unabhängig von Prisma-Client-Modellen
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m51s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 10m51s
This commit is contained in:
@@ -1,11 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { ensureTenantCategoriesTable } from '@/lib/auto-migrate'
|
||||
|
||||
function isMissingTable(err: any) {
|
||||
return err?.message?.includes('tenant_categories') || err?.code === 'P2021'
|
||||
}
|
||||
|
||||
async function getTenantId() {
|
||||
const user = await getSession()
|
||||
@@ -17,62 +12,37 @@ async function getTenantId() {
|
||||
return { tenantId: user.tenantId }
|
||||
}
|
||||
|
||||
async function ensureTable() {
|
||||
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/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
|
||||
|
||||
let categories: any[] = []
|
||||
try {
|
||||
categories = await (prisma as any).tenantCategory.findMany({
|
||||
where: { tenantId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { symbols: true },
|
||||
},
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
})
|
||||
} catch (dbErr: any) {
|
||||
console.error('tenantCategory.findMany failed:', dbErr)
|
||||
if (isMissingTable(dbErr)) {
|
||||
try {
|
||||
await ensureTenantCategoriesTable()
|
||||
categories = await (prisma as any).tenantCategory.findMany({
|
||||
where: { tenantId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { symbols: true },
|
||||
},
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
})
|
||||
} catch (retryErr) {
|
||||
console.error('Retry after auto-migrate failed:', retryErr)
|
||||
return NextResponse.json({ categories: [] })
|
||||
}
|
||||
} else {
|
||||
throw dbErr
|
||||
}
|
||||
}
|
||||
await ensureTable()
|
||||
|
||||
const result = categories.map((cat: any) => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
sortOrder: cat.sortOrder,
|
||||
icon: cat.icon,
|
||||
symbolCount: cat._count?.symbols ?? 0,
|
||||
createdAt: cat.createdAt,
|
||||
}))
|
||||
const categories: any[] = await prisma.$queryRawUnsafe(
|
||||
`SELECT * FROM tenant_categories WHERE "tenantId" = $1 AND "isActive" = true ORDER BY "sortOrder" ASC`,
|
||||
tenantId
|
||||
)
|
||||
|
||||
return NextResponse.json({ categories: result })
|
||||
return NextResponse.json({ categories })
|
||||
} catch (error) {
|
||||
console.error('Error fetching tenant categories:', error)
|
||||
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||
@@ -81,68 +51,36 @@ export async function GET() {
|
||||
|
||||
// ─── 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()
|
||||
const { name, sortOrder } = await req.json()
|
||||
if (!name || typeof name !== 'string') {
|
||||
return NextResponse.json({ error: 'Name erforderlich' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check for duplicate name within tenant
|
||||
let existing: any = null
|
||||
try {
|
||||
existing = await (prisma as any).tenantCategory.findFirst({
|
||||
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
|
||||
})
|
||||
} catch (dbErr: any) {
|
||||
if (isMissingTable(dbErr)) {
|
||||
await ensureTenantCategoriesTable()
|
||||
existing = await (prisma as any).tenantCategory.findFirst({
|
||||
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
|
||||
})
|
||||
} else {
|
||||
throw dbErr
|
||||
}
|
||||
}
|
||||
if (existing) {
|
||||
await ensureTable()
|
||||
|
||||
// Check for duplicate
|
||||
const existing = await prisma.$queryRawUnsafe(
|
||||
`SELECT id FROM tenant_categories WHERE "tenantId" = $1 AND LOWER(name) = LOWER($2) LIMIT 1`,
|
||||
tenantId, name.trim()
|
||||
) as any[]
|
||||
if (existing && existing.length > 0) {
|
||||
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
||||
}
|
||||
|
||||
try {
|
||||
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 (dbErr: any) {
|
||||
if (isMissingTable(dbErr)) {
|
||||
await ensureTenantCategoriesTable()
|
||||
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 })
|
||||
}
|
||||
throw dbErr
|
||||
}
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`INSERT INTO tenant_categories (id, "tenantId", name, "sortOrder", "createdAt", "updatedAt")
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
tenantId, name.trim(), sortOrder ?? 0
|
||||
) as any[]
|
||||
|
||||
return NextResponse.json({ category: result[0] }, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error creating tenant category:', error)
|
||||
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||
@@ -151,78 +89,41 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// ─── 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()
|
||||
const { id, name, sortOrder } = 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
|
||||
await ensureTable()
|
||||
|
||||
// If renaming, check for duplicates
|
||||
if (name) {
|
||||
let existing: any = null
|
||||
try {
|
||||
existing = await (prisma as any).tenantCategory.findFirst({
|
||||
where: {
|
||||
tenantId,
|
||||
name: { equals: name.trim(), mode: 'insensitive' },
|
||||
id: { not: id },
|
||||
},
|
||||
})
|
||||
} catch (dbErr: any) {
|
||||
if (isMissingTable(dbErr)) {
|
||||
await ensureTenantCategoriesTable()
|
||||
existing = await (prisma as any).tenantCategory.findFirst({
|
||||
where: {
|
||||
tenantId,
|
||||
name: { equals: name.trim(), mode: 'insensitive' },
|
||||
id: { not: id },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
throw dbErr
|
||||
}
|
||||
}
|
||||
if (existing) {
|
||||
const existing = await prisma.$queryRawUnsafe(
|
||||
`SELECT id FROM tenant_categories WHERE "tenantId" = $1 AND LOWER(name) = LOWER($2) AND id != $3 LIMIT 1`,
|
||||
tenantId, name.trim(), id
|
||||
) as any[]
|
||||
if (existing && existing.length > 0) {
|
||||
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const category = await (prisma as any).tenantCategory.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.trim()); }
|
||||
if (sortOrder !== undefined) { updates.push(`"sortOrder" = $${params.length + 1}`); params.push(sortOrder); }
|
||||
updates.push(`"updatedAt" = NOW()`)
|
||||
|
||||
if (category.count === 0) {
|
||||
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
} catch (dbErr: any) {
|
||||
if (isMissingTable(dbErr)) {
|
||||
await ensureTenantCategoriesTable()
|
||||
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 })
|
||||
}
|
||||
} else {
|
||||
throw dbErr
|
||||
}
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`UPDATE tenant_categories SET ${updates.join(', ')} WHERE id = $1 AND "tenantId" = $2 RETURNING *`,
|
||||
...params
|
||||
) as any[]
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
@@ -234,12 +135,6 @@ export async function PATCH(req: NextRequest) {
|
||||
|
||||
// ─── 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()
|
||||
@@ -249,39 +144,24 @@ export async function DELETE(req: NextRequest) {
|
||||
const { id } = await req.json()
|
||||
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||
|
||||
await ensureTable()
|
||||
|
||||
// Check if category has symbols
|
||||
try {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
} catch (dbErr: any) {
|
||||
if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') {
|
||||
// Skip check if table doesn't exist
|
||||
} else {
|
||||
throw dbErr
|
||||
}
|
||||
const symbolCount = await prisma.$queryRawUnsafe(
|
||||
`SELECT COUNT(*)::int as cnt FROM tenant_symbols WHERE "categoryId" = $1 AND "tenantId" = $2`,
|
||||
id, tenantId
|
||||
) as any[]
|
||||
if (symbolCount && symbolCount[0]?.cnt > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: `Kategorie enthält ${symbolCount[0].cnt} Symbol(e) — bitte zuerst verschieben oder löschen` },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await (prisma as any).tenantCategory.deleteMany({
|
||||
where: { id, tenantId },
|
||||
})
|
||||
} catch (dbErr: any) {
|
||||
if (isMissingTable(dbErr)) {
|
||||
await ensureTenantCategoriesTable()
|
||||
await (prisma as any).tenantCategory.deleteMany({
|
||||
where: { id, tenantId },
|
||||
})
|
||||
} else {
|
||||
throw dbErr
|
||||
}
|
||||
}
|
||||
await prisma.$queryRawUnsafe(
|
||||
`DELETE FROM tenant_categories WHERE id = $1 AND "tenantId" = $2`,
|
||||
id, tenantId
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user