carservice: initial commit
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user