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 { NextRequest, NextResponse } from 'next/server'
|
||||||
import { prisma } from '@/lib/db'
|
import { prisma } from '@/lib/db'
|
||||||
import { getSession } from '@/lib/auth'
|
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() {
|
async function getTenantId() {
|
||||||
const user = await getSession()
|
const user = await getSession()
|
||||||
@@ -17,62 +12,37 @@ async function getTenantId() {
|
|||||||
return { tenantId: user.tenantId }
|
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 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/tenant/categories
|
|
||||||
* Liefert alle Tenant-Kategorien des aktuellen Mandanten mit Symbol-Zählung.
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
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
|
||||||
|
|
||||||
let categories: any[] = []
|
await ensureTable()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = categories.map((cat: any) => ({
|
const categories: any[] = await prisma.$queryRawUnsafe(
|
||||||
id: cat.id,
|
`SELECT * FROM tenant_categories WHERE "tenantId" = $1 AND "isActive" = true ORDER BY "sortOrder" ASC`,
|
||||||
name: cat.name,
|
tenantId
|
||||||
sortOrder: cat.sortOrder,
|
)
|
||||||
icon: cat.icon,
|
|
||||||
symbolCount: cat._count?.symbols ?? 0,
|
|
||||||
createdAt: cat.createdAt,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return NextResponse.json({ categories: result })
|
return NextResponse.json({ categories })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching tenant categories:', error)
|
console.error('Error fetching tenant categories:', error)
|
||||||
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
@@ -81,68 +51,36 @@ export async function GET() {
|
|||||||
|
|
||||||
// ─── POST ───────────────────────────────────────────────────────────────────
|
// ─── POST ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/tenant/categories
|
|
||||||
* Legt eine neue Kategorie an.
|
|
||||||
*
|
|
||||||
* Body: { name: string, sortOrder?: number, icon?: string }
|
|
||||||
*/
|
|
||||||
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
|
||||||
|
|
||||||
const { name, sortOrder, icon } = await req.json()
|
const { name, sortOrder } = await req.json()
|
||||||
if (!name || typeof name !== 'string') {
|
if (!name || typeof name !== 'string') {
|
||||||
return NextResponse.json({ error: 'Name erforderlich' }, { status: 400 })
|
return NextResponse.json({ error: 'Name erforderlich' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate name within tenant
|
await ensureTable()
|
||||||
let existing: any = null
|
|
||||||
try {
|
// Check for duplicate
|
||||||
existing = await (prisma as any).tenantCategory.findFirst({
|
const existing = await prisma.$queryRawUnsafe(
|
||||||
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
|
`SELECT id FROM tenant_categories WHERE "tenantId" = $1 AND LOWER(name) = LOWER($2) LIMIT 1`,
|
||||||
})
|
tenantId, name.trim()
|
||||||
} catch (dbErr: any) {
|
) as any[]
|
||||||
if (isMissingTable(dbErr)) {
|
if (existing && existing.length > 0) {
|
||||||
await ensureTenantCategoriesTable()
|
|
||||||
existing = await (prisma as any).tenantCategory.findFirst({
|
|
||||||
where: { tenantId, name: { equals: name, mode: 'insensitive' } },
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw dbErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (existing) {
|
|
||||||
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const result = await prisma.$queryRawUnsafe(
|
||||||
const category = await (prisma as any).tenantCategory.create({
|
`INSERT INTO tenant_categories (id, "tenantId", name, "sortOrder", "createdAt", "updatedAt")
|
||||||
data: {
|
VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
|
||||||
tenantId,
|
RETURNING *`,
|
||||||
name: name.trim(),
|
tenantId, name.trim(), sortOrder ?? 0
|
||||||
sortOrder: sortOrder ?? 0,
|
) as any[]
|
||||||
icon: icon || null,
|
|
||||||
},
|
return NextResponse.json({ category: result[0] }, { status: 201 })
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating tenant category:', error)
|
console.error('Error creating tenant category:', error)
|
||||||
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
return NextResponse.json({ error: 'Interner Fehler' }, { status: 500 })
|
||||||
@@ -151,78 +89,41 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// ─── PATCH ──────────────────────────────────────────────────────────────────
|
// ─── PATCH ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* PATCH /api/tenant/categories
|
|
||||||
* Aktualisiert eine Kategorie.
|
|
||||||
*
|
|
||||||
* Body: { id: string, name?: string, sortOrder?: number, icon?: string }
|
|
||||||
*/
|
|
||||||
export async function PATCH(req: NextRequest) {
|
export async function PATCH(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
|
||||||
|
|
||||||
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 })
|
if (!id) return NextResponse.json({ error: 'id erforderlich' }, { status: 400 })
|
||||||
|
|
||||||
const data: any = {}
|
await ensureTable()
|
||||||
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 renaming, check for duplicates
|
||||||
if (name) {
|
if (name) {
|
||||||
let existing: any = null
|
const existing = await prisma.$queryRawUnsafe(
|
||||||
try {
|
`SELECT id FROM tenant_categories WHERE "tenantId" = $1 AND LOWER(name) = LOWER($2) AND id != $3 LIMIT 1`,
|
||||||
existing = await (prisma as any).tenantCategory.findFirst({
|
tenantId, name.trim(), id
|
||||||
where: {
|
) as any[]
|
||||||
tenantId,
|
if (existing && existing.length > 0) {
|
||||||
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) {
|
|
||||||
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
return NextResponse.json({ error: 'Kategorie existiert bereits' }, { status: 409 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const updates: string[] = []
|
||||||
const category = await (prisma as any).tenantCategory.updateMany({
|
const params: any[] = [id, tenantId]
|
||||||
where: { id, tenantId },
|
if (name !== undefined) { updates.push(`name = $${params.length + 1}`); params.push(name.trim()); }
|
||||||
data,
|
if (sortOrder !== undefined) { updates.push(`"sortOrder" = $${params.length + 1}`); params.push(sortOrder); }
|
||||||
})
|
updates.push(`"updatedAt" = NOW()`)
|
||||||
|
|
||||||
if (category.count === 0) {
|
const result = await prisma.$queryRawUnsafe(
|
||||||
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
|
`UPDATE tenant_categories SET ${updates.join(', ')} WHERE id = $1 AND "tenantId" = $2 RETURNING *`,
|
||||||
}
|
...params
|
||||||
} catch (dbErr: any) {
|
) as any[]
|
||||||
if (isMissingTable(dbErr)) {
|
|
||||||
await ensureTenantCategoriesTable()
|
if (!result || result.length === 0) {
|
||||||
const category = await (prisma as any).tenantCategory.updateMany({
|
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
|
||||||
where: { id, tenantId },
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
if (category.count === 0) {
|
|
||||||
return NextResponse.json({ error: 'Kategorie nicht gefunden' }, { status: 404 })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw dbErr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
@@ -234,12 +135,6 @@ export async function PATCH(req: NextRequest) {
|
|||||||
|
|
||||||
// ─── DELETE ─────────────────────────────────────────────────────────────────
|
// ─── DELETE ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/tenant/categories
|
|
||||||
* Löscht eine Kategorie (nur wenn leer).
|
|
||||||
*
|
|
||||||
* 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()
|
||||||
@@ -249,39 +144,24 @@ 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 })
|
||||||
|
|
||||||
|
await ensureTable()
|
||||||
|
|
||||||
// Check if category has symbols
|
// Check if category has symbols
|
||||||
try {
|
const symbolCount = await prisma.$queryRawUnsafe(
|
||||||
const symbolCount = await (prisma as any).tenantSymbol.count({
|
`SELECT COUNT(*)::int as cnt FROM tenant_symbols WHERE "categoryId" = $1 AND "tenantId" = $2`,
|
||||||
where: { categoryId: id, tenantId },
|
id, tenantId
|
||||||
})
|
) as any[]
|
||||||
if (symbolCount > 0) {
|
if (symbolCount && symbolCount[0]?.cnt > 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `Kategorie enthält ${symbolCount} Symbol(e) — bitte zuerst verschieben oder löschen` },
|
{ error: `Kategorie enthält ${symbolCount[0].cnt} Symbol(e) — bitte zuerst verschieben oder löschen` },
|
||||||
{ status: 409 }
|
{ status: 409 }
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} catch (dbErr: any) {
|
|
||||||
if (dbErr?.message?.includes('tenant_symbols') || dbErr?.code === 'P2021') {
|
|
||||||
// Skip check if table doesn't exist
|
|
||||||
} else {
|
|
||||||
throw dbErr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await prisma.$queryRawUnsafe(
|
||||||
await (prisma as any).tenantCategory.deleteMany({
|
`DELETE FROM tenant_categories WHERE id = $1 AND "tenantId" = $2`,
|
||||||
where: { id, tenantId },
|
id, tenantId
|
||||||
})
|
)
|
||||||
} catch (dbErr: any) {
|
|
||||||
if (isMissingTable(dbErr)) {
|
|
||||||
await ensureTenantCategoriesTable()
|
|
||||||
await (prisma as any).tenantCategory.deleteMany({
|
|
||||||
where: { id, tenantId },
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw dbErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user