import { NextResponse } from "next/server"; import { prisma } from "@/lib/db"; import { getActiveAccountId } from "@/lib/account-context"; import { authErrorResponse } from "@/lib/auth/api"; import { isAccountInUserScope, requireUserAccountScope } from "@/lib/auth/user-scope"; import { migrateLegacyProductCatalog } from "@/lib/services/product-catalog"; import { parseMatchTags, serializeMatchTags } from "@/lib/types/product-match"; import { hasProductContext } from "@/lib/types/product-context"; function mapBrand(row: { id: string; name: string; notes: string | null; products: Array<{ id: string; label: string; context: string; matchTags: string | null; }>; _count?: { products: number }; }) { return { id: row.id, name: row.name, notes: row.notes, products: row.products.map((p) => ({ id: p.id, label: p.label, context: p.context, matchTags: parseMatchTags(p.matchTags), })), productCount: row._count?.products ?? row.products.length, }; } function parsePositiveInt(value: string | null, fallback: number, max: number) { const parsed = Number.parseInt(value ?? "", 10); if (!Number.isFinite(parsed) || parsed < 1) return fallback; return Math.min(parsed, max); } export async function GET(request: Request) { try { const accountId = await getActiveAccountId(); const { where: accountWhere, accountIds } = await requireUserAccountScope(accountId); try { await migrateLegacyProductCatalog(accountIds); } catch (migrateErr) { console.error("[brands] migrateLegacyProductCatalog failed:", migrateErr); } const { searchParams } = new URL(request.url); const brandId = searchParams.get("brandId"); const fetchAll = searchParams.get("all") === "1"; const q = searchParams.get("q")?.trim() ?? ""; const page = parsePositiveInt(searchParams.get("page"), 1, 10_000); const limit = fetchAll ? 500 : parsePositiveInt(searchParams.get("limit"), 8, 50); const productLimit = parsePositiveInt(searchParams.get("productLimit"), 12, 200); const searchWhere = q ? { OR: [ { name: { contains: q } }, { notes: { contains: q } }, { products: { some: { OR: [{ label: { contains: q } }, { matchTags: { contains: q } }], }, }, }, ], } : {}; const where = { ...accountWhere, ...searchWhere }; if (brandId) { const brand = await prisma.brandProfile.findFirst({ where: { ...where, id: brandId }, include: { products: { orderBy: { updatedAt: "desc" }, take: productLimit }, _count: { select: { products: true } }, }, }); if (!brand) { return NextResponse.json({ error: "找不到品牌" }, { status: 404 }); } return NextResponse.json({ brand: mapBrand(brand), brands: [mapBrand(brand)], total: 1, page: 1, limit: 1, totalPages: 1, }); } const total = await prisma.brandProfile.count({ where }); const brands = await prisma.brandProfile.findMany({ where, include: { products: { orderBy: { updatedAt: "desc" }, take: productLimit }, _count: { select: { products: true } }, }, orderBy: { updatedAt: "desc" }, ...(fetchAll ? {} : { skip: (page - 1) * limit, take: limit }), }); const totalPages = fetchAll ? 1 : Math.max(1, Math.ceil(total / limit)); return NextResponse.json({ brands: brands.map(mapBrand), total, page: fetchAll ? 1 : page, limit: fetchAll ? total : limit, totalPages, }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "讀取品牌失敗"; return NextResponse.json({ error: message, brands: [] }, { status: 500 }); } } export async function POST(request: Request) { try { const { name, notes, product } = (await request.json()) as { name?: string; notes?: string; product?: { label?: string; context?: string; matchTags?: string[]; }; }; if (!name?.trim()) { return NextResponse.json({ error: "請填寫品牌名稱" }, { status: 400 }); } const accountId = await getActiveAccountId(); if (!accountId) { return NextResponse.json({ error: "請先建立並選定經營帳號" }, { status: 400 }); } const brand = await prisma.brandProfile.create({ data: { accountId, name: name.trim(), notes: notes?.trim() || null, ...(product?.label && product.context && hasProductContext(product.context) ? { products: { create: { accountId, label: product.label.trim(), context: product.context.trim(), matchTags: serializeMatchTags(product.matchTags ?? []), }, }, } : {}), }, include: { products: true }, }); return NextResponse.json({ brand: mapBrand(brand) }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "建立品牌失敗"; return NextResponse.json({ error: message }, { status: 500 }); } } export async function PATCH(request: Request) { try { const { id, name, notes } = (await request.json()) as { id?: string; name?: string; notes?: string | null; }; if (!id) return NextResponse.json({ error: "缺少 id" }, { status: 400 }); const { accountIds } = await requireUserAccountScope(await getActiveAccountId()); const existing = await prisma.brandProfile.findUnique({ where: { id } }); if (!existing || !isAccountInUserScope(accountIds, existing.accountId)) { return NextResponse.json({ error: "找不到品牌" }, { status: 404 }); } const brand = await prisma.brandProfile.update({ where: { id }, data: { ...(name !== undefined && { name: name.trim() }), ...(notes !== undefined && { notes: notes?.trim() || null }), }, include: { products: { orderBy: { updatedAt: "desc" } } }, }); return NextResponse.json({ brand: mapBrand(brand) }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "更新品牌失敗"; return NextResponse.json({ error: message }, { status: 500 }); } } export async function DELETE(request: Request) { try { const id = new URL(request.url).searchParams.get("id"); if (!id) return NextResponse.json({ error: "缺少 id" }, { status: 400 }); const { accountIds } = await requireUserAccountScope(await getActiveAccountId()); const existing = await prisma.brandProfile.findUnique({ where: { id } }); if (!existing || !isAccountInUserScope(accountIds, existing.accountId)) { return NextResponse.json({ error: "找不到品牌" }, { status: 404 }); } const linkedTopics = await prisma.topic.count({ where: { brandProfileId: id } }); if (linkedTopics > 0) { return NextResponse.json( { error: `仍有 ${linkedTopics} 個主題使用此品牌,請先更換` }, { status: 400 } ); } await prisma.brandProfile.delete({ where: { id } }); return NextResponse.json({ success: true }); } catch (error) { const authRes = authErrorResponse(error); if (authRes) return authRes; const message = error instanceof Error ? error.message : "刪除品牌失敗"; return NextResponse.json({ error: message }, { status: 500 }); } }