import { prisma } from "@/lib/prisma"; import { ApiError } from "@/lib/api"; import { generateQrToken } from "@/lib/qr"; /** * Clone a part's operations under a new partId. Each op gets a fresh qrToken * and is reset to `pending` with a zero unit count — the copy has never been * worked on even if the source had partial/completed steps. Claims, time logs, * and QC records are intentionally NOT carried over: they belong to the * original's history. */ async function cloneOperationsForPart( sourcePartId: string, destPartId: string, ): Promise { const sourceOps = await prisma.operation.findMany({ where: { partId: sourcePartId }, orderBy: { sequence: "asc" }, }); for (const op of sourceOps) { // Collision-retry for qrToken; 192 bits of entropy, same pattern as // the create-op route. let qrToken = generateQrToken(); for (let attempt = 0; attempt < 5; attempt++) { const existing = await prisma.operation.findUnique({ where: { qrToken }, select: { id: true }, }); if (!existing) break; qrToken = generateQrToken(); if (attempt === 4) throw new ApiError(500, "qr_collision", "Unable to allocate QR token"); } await prisma.operation.create({ data: { partId: destPartId, sequence: op.sequence, templateId: op.templateId, name: op.name, kind: op.kind, machineId: op.machineId, settings: op.settings, materialNotes: op.materialNotes, instructions: op.instructions, qcRequired: op.qcRequired, plannedMinutes: op.plannedMinutes, plannedUnits: op.plannedUnits, qrToken, // fresh copy — no prior work, no claim, no status status: "pending", unitsCompleted: 0, }, }); } return sourceOps.length; } /** * Clone a Part into its existing assembly. Returns the new part's id. * Operations are copied unless `includeOperations` is false. File * attachments are re-referenced by id — FileAsset rows are content-addressed * and shared. * * The caller must check that `code` is unique within the destination assembly * before calling; we re-check inside for safety and turn Prisma's unique- * constraint violation into a 409. */ export async function duplicatePart(opts: { sourcePartId: string; code: string; name?: string; includeOperations: boolean; }): Promise<{ id: string; operationsCopied: number }> { const source = await prisma.part.findUnique({ where: { id: opts.sourcePartId }, }); if (!source) throw new ApiError(404, "not_found", "Part not found"); const conflict = await prisma.part.findUnique({ where: { assemblyId_code: { assemblyId: source.assemblyId, code: opts.code } }, select: { id: true }, }); if (conflict) throw new ApiError(409, "code_taken", `Part code ${opts.code} already in use in this assembly`); const created = await prisma.part.create({ data: { assemblyId: source.assemblyId, code: opts.code, name: opts.name ?? source.name, material: source.material, qty: source.qty, notes: source.notes, // Re-attach the same FileAssets — storage is content-addressed, no copy needed. stepFileId: source.stepFileId, drawingFileId: source.drawingFileId, cutFileId: source.cutFileId, thumbnailFileId: source.thumbnailFileId, }, }); const operationsCopied = opts.includeOperations ? await cloneOperationsForPart(source.id, created.id) : 0; return { id: created.id, operationsCopied }; } /** * Clone an Assembly (plus every child Part, plus — optionally — every * Operation) into the same project. Returns the new assembly's id and a * summary count. Part codes and operation sequences are preserved since * they were unique within their parent in the source and will remain unique * within the new parent. Only the assembly's own `code` might clash with a * sibling in the project, so we check that up front. */ export async function duplicateAssembly(opts: { sourceAssemblyId: string; code: string; name?: string; includeOperations: boolean; }): Promise<{ id: string; partsCopied: number; operationsCopied: number }> { const source = await prisma.assembly.findUnique({ where: { id: opts.sourceAssemblyId }, include: { parts: { select: { id: true } } }, }); if (!source) throw new ApiError(404, "not_found", "Assembly not found"); const conflict = await prisma.assembly.findUnique({ where: { projectId_code: { projectId: source.projectId, code: opts.code } }, select: { id: true }, }); if (conflict) throw new ApiError(409, "code_taken", `Assembly code ${opts.code} already in use in this project`); const newAssembly = await prisma.assembly.create({ data: { projectId: source.projectId, code: opts.code, name: opts.name ?? source.name, qty: source.qty, notes: source.notes, stepFileId: source.stepFileId, drawingFileId: source.drawingFileId, cutFileId: source.cutFileId, }, }); let operationsCopied = 0; let partsCopied = 0; for (const sourcePart of source.parts) { const full = await prisma.part.findUnique({ where: { id: sourcePart.id } }); if (!full) continue; const clonedPart = await prisma.part.create({ data: { assemblyId: newAssembly.id, code: full.code, name: full.name, material: full.material, qty: full.qty, notes: full.notes, stepFileId: full.stepFileId, drawingFileId: full.drawingFileId, cutFileId: full.cutFileId, thumbnailFileId: full.thumbnailFileId, }, }); partsCopied++; if (opts.includeOperations) { operationsCopied += await cloneOperationsForPart(full.id, clonedPart.id); } } return { id: newAssembly.id, partsCopied, operationsCopied }; }