diff --git a/src/app/api/tenant/categories/route.ts b/src/app/api/tenant/categories/route.ts index c28bc8e..bbac513 100644 --- a/src/app/api/tenant/categories/route.ts +++ b/src/app/api/tenant/categories/route.ts @@ -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) {