195 lines
5.3 KiB
TypeScript
195 lines
5.3 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
import { errorResponse, getJsonBody, jsonResponse } from "@/lib/api-helpers";
|
|
|
|
async function recalcOrderTotal(orderId: number) {
|
|
const tasks = await prisma.task.findMany({
|
|
where: { orderId },
|
|
include: { parts: true },
|
|
});
|
|
|
|
const total = tasks.reduce((sum, task) => {
|
|
const partsCost = task.parts.reduce(
|
|
(p, tp) => p + (tp.price || 0) * tp.qty,
|
|
0,
|
|
);
|
|
const laborCost = task.laborHours * task.hourlyRate;
|
|
const price = task.price > 0 ? task.price : partsCost + laborCost;
|
|
return sum + price;
|
|
}, 0);
|
|
|
|
await prisma.order.update({ where: { id: orderId }, data: { total } });
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
const body = await getJsonBody(req);
|
|
if (!body) return errorResponse("Invalid JSON");
|
|
const {
|
|
orderId,
|
|
title,
|
|
notes,
|
|
laborHours,
|
|
hourlyRate,
|
|
price,
|
|
partIds = [],
|
|
templateParts = [],
|
|
} = body;
|
|
if (!orderId || !title)
|
|
return errorResponse("orderId and title are required");
|
|
|
|
const order = await prisma.order.findUnique({
|
|
where: { id: Number(orderId) },
|
|
include: { car: true },
|
|
});
|
|
if (!order) return errorResponse("Order not found", 404);
|
|
|
|
const last = await prisma.task.findFirst({
|
|
where: { orderId: Number(orderId) },
|
|
orderBy: { orderIndex: "desc" },
|
|
});
|
|
|
|
// If the title matches a quick task template, pull its defaults.
|
|
const quickTask = await prisma.quickTask.findFirst({
|
|
where: { title },
|
|
include: { parts: true },
|
|
});
|
|
|
|
if (quickTask) {
|
|
await prisma.quickTask.update({
|
|
where: { id: quickTask.id },
|
|
data: { useCount: { increment: 1 } },
|
|
});
|
|
}
|
|
|
|
const hasExplicitLabor =
|
|
laborHours !== undefined && String(laborHours).trim() !== "";
|
|
const hasExplicitRate =
|
|
hourlyRate !== undefined && String(hourlyRate).trim() !== "";
|
|
const hasExplicitPrice = price !== undefined && String(price).trim() !== "";
|
|
const taskLaborHours =
|
|
quickTask && !hasExplicitLabor
|
|
? quickTask.laborHours
|
|
: Number(laborHours) || 0;
|
|
const taskHourlyRate =
|
|
quickTask && !hasExplicitRate
|
|
? quickTask.hourlyRate
|
|
: Number(hourlyRate) || 50;
|
|
const taskPrice =
|
|
quickTask && !hasExplicitPrice
|
|
? quickTask.price
|
|
: hasExplicitPrice
|
|
? Number(price) || 0
|
|
: null;
|
|
const effectiveTemplateParts =
|
|
templateParts && templateParts.length > 0
|
|
? templateParts
|
|
: quickTask?.parts || [];
|
|
|
|
const task = await prisma.task.create({
|
|
data: {
|
|
orderId: Number(orderId),
|
|
title,
|
|
notes: notes || "",
|
|
laborHours: Number(taskLaborHours) || 0,
|
|
hourlyRate: Number(taskHourlyRate) || 50,
|
|
price: taskPrice ?? undefined,
|
|
orderIndex: (last?.orderIndex ?? -1) + 1,
|
|
},
|
|
include: { parts: { include: { part: true } } },
|
|
});
|
|
|
|
// Attach selected existing parts — only link to the same car or global parts.
|
|
for (const item of partIds) {
|
|
const { partId, qty = 1, price: partPrice } = item;
|
|
const part = await prisma.part.findFirst({
|
|
where: {
|
|
id: Number(partId),
|
|
OR: [{ carId: order.carId }, { isGlobal: true }],
|
|
},
|
|
});
|
|
if (part) {
|
|
await prisma.taskPart.create({
|
|
data: {
|
|
taskId: task.id,
|
|
partId: part.id,
|
|
qty: Number(qty) || 1,
|
|
price: partPrice != null ? Number(partPrice) : part.purchasePrice,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// Convert template parts into private car parts (or link globals) and attach them.
|
|
for (const tp of effectiveTemplateParts) {
|
|
const isGlobal = tp.isGlobal || false;
|
|
let partId: number | undefined;
|
|
if (isGlobal) {
|
|
// Try to find an existing global part with the same name.
|
|
const existing = await prisma.part.findFirst({
|
|
where: { name: tp.name, isGlobal: true },
|
|
});
|
|
if (existing) {
|
|
partId = existing.id;
|
|
} else {
|
|
const created = await prisma.part.create({
|
|
data: {
|
|
name: tp.name,
|
|
purchasePrice: Number(tp.price) || 0,
|
|
notes: "",
|
|
isGlobal: true,
|
|
},
|
|
});
|
|
partId = created.id;
|
|
}
|
|
} else {
|
|
const existing = await prisma.part.findFirst({
|
|
where: { name: tp.name, carId: order.carId, isGlobal: false },
|
|
});
|
|
if (existing) {
|
|
partId = existing.id;
|
|
} else {
|
|
const created = await prisma.part.create({
|
|
data: {
|
|
name: tp.name,
|
|
purchasePrice: Number(tp.price) || 0,
|
|
notes: "",
|
|
carId: order.carId,
|
|
isGlobal: false,
|
|
},
|
|
});
|
|
partId = created.id;
|
|
}
|
|
}
|
|
if (partId) {
|
|
await prisma.taskPart.create({
|
|
data: {
|
|
taskId: task.id,
|
|
partId,
|
|
qty: Number(tp.qty) || 1,
|
|
price: Number(tp.price) || 0,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
await recalcOrderTotal(Number(orderId));
|
|
if (quickTask) {
|
|
await prisma.quickTask.update({
|
|
where: { id: quickTask.id },
|
|
data: { useCount: { increment: 1 } },
|
|
});
|
|
} else {
|
|
await prisma.quickTask.upsert({
|
|
where: { title },
|
|
update: { useCount: { increment: 1 } },
|
|
create: { title, useCount: 1 },
|
|
});
|
|
}
|
|
|
|
const full = await prisma.task.findUnique({
|
|
where: { id: task.id },
|
|
include: { parts: { include: { part: true } } },
|
|
});
|
|
|
|
return jsonResponse(full, 201);
|
|
}
|