138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
import { errorResponse, getJsonBody, jsonResponse } from "@/lib/api-helpers";
|
|
import { scrapePartInfo } from "@/lib/scrape";
|
|
|
|
export async function GET(req: Request) {
|
|
const { searchParams } = new URL(req.url);
|
|
const q = searchParams.get("q")?.trim() ?? "";
|
|
const carId = searchParams.get("carId");
|
|
const includeAll = searchParams.get("all") === "true";
|
|
const includeGlobal = searchParams.get("includeGlobal") === "true";
|
|
|
|
const term = q.toLowerCase();
|
|
const cid = carId ? Number(carId) : undefined;
|
|
const parts = await prisma.part.findMany({
|
|
where: {
|
|
AND: [
|
|
q
|
|
? {
|
|
OR: [
|
|
{ name: { contains: term } },
|
|
{ manufacturer: { contains: term } },
|
|
],
|
|
}
|
|
: {},
|
|
carId
|
|
? includeGlobal
|
|
? { OR: [{ carId: cid }, { isGlobal: true }] }
|
|
: { carId: cid }
|
|
: includeAll
|
|
? {}
|
|
: { isGlobal: true },
|
|
],
|
|
},
|
|
include: { car: true },
|
|
orderBy: { updatedAt: "desc" },
|
|
take: 100,
|
|
});
|
|
|
|
return jsonResponse(parts);
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
const body = await getJsonBody(req);
|
|
if (!body) return errorResponse("Invalid JSON");
|
|
const {
|
|
name,
|
|
manufacturer,
|
|
supplierLink,
|
|
url,
|
|
purchasePrice,
|
|
notes,
|
|
carId,
|
|
orderId,
|
|
isGlobal,
|
|
} = body;
|
|
if (!name) return errorResponse("name is required");
|
|
|
|
const normalizedName = name.trim();
|
|
const carIdNum = carId ? Number(carId) : null;
|
|
|
|
// Prevent duplicate part names per car and avoid global/private collisions.
|
|
if (carIdNum) {
|
|
const duplicate = await prisma.part.findFirst({
|
|
where: {
|
|
name: { equals: normalizedName },
|
|
OR: [{ carId: carIdNum }, { isGlobal: true }],
|
|
},
|
|
});
|
|
if (duplicate && duplicate.name.toLowerCase() === normalizedName.toLowerCase()) {
|
|
return errorResponse(
|
|
`A part named "${duplicate.name}" already exists for this car or globally.`,
|
|
409,
|
|
);
|
|
}
|
|
}
|
|
|
|
const part = await prisma.part.create({
|
|
data: {
|
|
name: normalizedName,
|
|
manufacturer,
|
|
supplierLink,
|
|
url,
|
|
purchasePrice: Number(purchasePrice) || 0,
|
|
notes: notes || "",
|
|
carId: carIdNum,
|
|
orderId: orderId ? Number(orderId) : null,
|
|
// Private by default. User must explicitly mark global.
|
|
isGlobal: isGlobal === true || isGlobal === "true",
|
|
},
|
|
include: { car: true },
|
|
});
|
|
|
|
return jsonResponse(part, 201);
|
|
}
|
|
|
|
export async function PATCH(req: Request) {
|
|
const body = await getJsonBody(req);
|
|
if (!body) return errorResponse("Invalid JSON");
|
|
const { id, ...data } = body;
|
|
if (!id) return errorResponse("id is required");
|
|
|
|
if (data.purchasePrice !== undefined) data.purchasePrice = Number(data.purchasePrice) || 0;
|
|
if (data.sellPrice !== undefined) data.sellPrice = Number(data.sellPrice) || 0;
|
|
if (data.qty !== undefined) data.qty = Number(data.qty) || 0;
|
|
if (data.isGlobal !== undefined) data.isGlobal = data.isGlobal === true || data.isGlobal === "true";
|
|
|
|
if (data.name) {
|
|
const target = await prisma.part.findUnique({ where: { id: Number(id) } });
|
|
if (target) {
|
|
const carIdNum = data.carId !== undefined ? Number(data.carId) || null : target.carId;
|
|
const duplicate = await prisma.part.findFirst({
|
|
where: {
|
|
id: { not: target.id },
|
|
name: { equals: data.name.trim() },
|
|
OR: [{ carId: carIdNum }, { isGlobal: true }],
|
|
},
|
|
});
|
|
if (
|
|
duplicate &&
|
|
duplicate.name.toLowerCase() === data.name.trim().toLowerCase()
|
|
) {
|
|
return errorResponse(
|
|
`A part named "${duplicate.name}" already exists for this car or globally.`,
|
|
409,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const updated = await prisma.part.update({
|
|
where: { id: Number(id) },
|
|
data,
|
|
include: { car: true },
|
|
});
|
|
|
|
return jsonResponse(updated);
|
|
}
|