import type { ProjectCockpitDto, ProjectCockpitPurchaseOrderDto, ProjectCockpitReceiptDto, ProjectCockpitRiskLevel, ProjectCockpitVendorDto, ProjectCustomerOptionDto, ProjectDetailDto, ProjectDocumentOptionDto, ProjectInput, ProjectMilestoneDto, ProjectMilestoneInput, ProjectOwnerOptionDto, ProjectPriority, ProjectRollupDto, ProjectShipmentOptionDto, ProjectStatus, ProjectSummaryDto, WorkOrderStatus, } from "@mrp/shared"; import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js"; import { logAuditEvent } from "../../lib/audit.js"; import { prisma } from "../../lib/prisma.js"; import { getSalesOrderPlanningById } from "../sales/planning.js"; const projectModel = (prisma as any).project; type ProjectMilestoneRecord = { id: string; title: string; status: string; dueDate: Date | null; completedAt: Date | null; notes: string; sortOrder: number; }; type ProjectRecord = { id: string; projectNumber: string; name: string; status: string; priority: string; dueDate: Date | null; notes: string; createdAt: Date; updatedAt: Date; customer: { id: string; name: string; email: string; phone: string; }; owner: { id: string; firstName: string; lastName: string; } | null; salesQuote: { id: string; documentNumber: string; } | null; salesOrder: { id: string; documentNumber: string; } | null; shipment: { id: string; shipmentNumber: string; } | null; milestones: ProjectMilestoneRecord[]; workOrders: Array<{ id: string; status: string; dueDate: Date | null; }>; }; type ProjectSalesDocumentRecord = { documentNumber: string; status: string; discountPercent: number; taxPercent: number; freightAmount: number; lines: Array<{ quantity: number; unitPrice: number; }>; }; type ProjectShipmentRecord = { shipmentNumber: string; status: string; shipDate: Date | null; packageCount: number; trackingNumber: string; carrier: string; serviceLevel: string; }; type ProjectPurchaseOrderRecord = { id: string; documentNumber: string; status: string; issueDate: Date; vendor: { id: string; name: string; }; lines: Array<{ id: string; quantity: number; unitCost: number; receiptLines: Array<{ quantity: number; }>; }>; }; type ProjectReceiptLineRecord = { quantity: number; purchaseReceipt: { id: string; receiptNumber: string; receivedAt: Date; purchaseOrder: { id: string; documentNumber: string; vendor: { name: string; }; }; }; }; function roundMoney(value: number) { return Math.round(value * 100) / 100; } function calculateSalesDocumentTotal(record: ProjectSalesDocumentRecord) { const subtotal = roundMoney(record.lines.reduce((sum, line) => sum + (line.quantity * line.unitPrice), 0)); const discountAmount = roundMoney(subtotal * (record.discountPercent / 100)); const taxableSubtotal = roundMoney(subtotal - discountAmount); const taxAmount = roundMoney(taxableSubtotal * (record.taxPercent / 100)); return roundMoney(taxableSubtotal + taxAmount + roundMoney(record.freightAmount)); } function getOwnerName(owner: ProjectRecord["owner"]) { return owner ? `${owner.firstName} ${owner.lastName}`.trim() : null; } function mapProjectMilestone(record: ProjectMilestoneRecord): ProjectMilestoneDto { return { id: record.id, title: record.title, status: record.status as ProjectMilestoneDto["status"], dueDate: record.dueDate ? record.dueDate.toISOString() : null, completedAt: record.completedAt ? record.completedAt.toISOString() : null, notes: record.notes, sortOrder: record.sortOrder, }; } function buildProjectRollups(record: ProjectRecord): ProjectRollupDto { const now = Date.now(); const milestoneCount = record.milestones.length; const completedMilestoneCount = record.milestones.filter((milestone) => milestone.status === "COMPLETE").length; const overdueMilestoneCount = record.milestones.filter( (milestone) => milestone.status !== "COMPLETE" && milestone.dueDate && milestone.dueDate.getTime() < now ).length; const workOrderCount = record.workOrders.length; const completedWorkOrderCount = record.workOrders.filter((workOrder) => workOrder.status === "COMPLETE").length; const activeStatuses = new Set(["RELEASED", "IN_PROGRESS", "ON_HOLD"]); const closedStatuses = new Set(["COMPLETE", "CANCELLED"]); const activeWorkOrderCount = record.workOrders.filter((workOrder) => activeStatuses.has(workOrder.status as WorkOrderStatus)).length; const overdueWorkOrderCount = record.workOrders.filter( (workOrder) => !closedStatuses.has(workOrder.status as WorkOrderStatus) && workOrder.dueDate && workOrder.dueDate.getTime() < now ).length; return { milestoneCount, completedMilestoneCount, openMilestoneCount: milestoneCount - completedMilestoneCount, overdueMilestoneCount, workOrderCount, activeWorkOrderCount, completedWorkOrderCount, overdueWorkOrderCount, }; } function deriveProjectRiskLevel(score: number): ProjectCockpitRiskLevel { if (score >= 85) { return "LOW"; } if (score >= 65) { return "MEDIUM"; } return "HIGH"; } async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollupDto): Promise { const blockedMilestoneCount = record.milestones.filter((milestone) => milestone.status === "BLOCKED").length; const [quote, salesOrder, shipment, purchaseOrders, receiptLines, planning] = await Promise.all([ record.salesQuote ? prisma.salesQuote.findUnique({ where: { id: record.salesQuote.id }, select: { documentNumber: true, status: true, discountPercent: true, taxPercent: true, freightAmount: true, lines: { select: { quantity: true, unitPrice: true, }, }, }, }) : Promise.resolve(null), record.salesOrder ? prisma.salesOrder.findUnique({ where: { id: record.salesOrder.id }, select: { documentNumber: true, status: true, discountPercent: true, taxPercent: true, freightAmount: true, lines: { select: { quantity: true, unitPrice: true, }, }, }, }) : Promise.resolve(null), record.shipment ? prisma.shipment.findUnique({ where: { id: record.shipment.id }, select: { shipmentNumber: true, status: true, shipDate: true, packageCount: true, trackingNumber: true, carrier: true, serviceLevel: true, }, }) : Promise.resolve(null), record.salesOrder ? prisma.purchaseOrder.findMany({ where: { lines: { some: { salesOrderId: record.salesOrder.id, }, }, }, select: { id: true, documentNumber: true, status: true, issueDate: true, vendor: { select: { id: true, name: true, }, }, lines: { where: { salesOrderId: record.salesOrder.id, }, select: { id: true, quantity: true, unitCost: true, receiptLines: { select: { quantity: true, }, }, }, }, }, orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }], }) : Promise.resolve([]), record.salesOrder ? prisma.purchaseReceiptLine.findMany({ where: { purchaseOrderLine: { salesOrderId: record.salesOrder.id, }, }, select: { quantity: true, purchaseReceipt: { select: { id: true, receiptNumber: true, receivedAt: true, purchaseOrder: { select: { id: true, documentNumber: true, vendor: { select: { name: true, }, }, }, }, }, }, }, orderBy: [{ purchaseReceipt: { receivedAt: "desc" } }, { createdAt: "desc" }], }) : Promise.resolve([]), record.salesOrder ? getSalesOrderPlanningById(record.salesOrder.id) : Promise.resolve(null), ]); const typedPurchaseOrders = purchaseOrders as ProjectPurchaseOrderRecord[]; const typedReceiptLines = receiptLines as ProjectReceiptLineRecord[]; const typedQuote = quote as ProjectSalesDocumentRecord | null; const typedSalesOrder = salesOrder as ProjectSalesDocumentRecord | null; const typedShipment = shipment as ProjectShipmentRecord | null; const typedPlanning = planning as SalesOrderPlanningDto | null; const purchaseOrdersSummary: ProjectCockpitPurchaseOrderDto[] = typedPurchaseOrders.map((purchaseOrder) => { const linkedLineCount = purchaseOrder.lines.length; const linkedLineValue = roundMoney( purchaseOrder.lines.reduce((sum, line) => sum + (line.quantity * line.unitCost), 0) ); const totalOrderedQuantity = purchaseOrder.lines.reduce((sum, line) => sum + line.quantity, 0); const totalReceivedQuantity = purchaseOrder.lines.reduce( (sum, line) => sum + line.receiptLines.reduce((lineSum, receiptLine) => lineSum + receiptLine.quantity, 0), 0 ); const totalOutstandingQuantity = Math.max(0, totalOrderedQuantity - totalReceivedQuantity); return { id: purchaseOrder.id, documentNumber: purchaseOrder.documentNumber, vendorId: purchaseOrder.vendor.id, vendorName: purchaseOrder.vendor.name, status: purchaseOrder.status, issueDate: purchaseOrder.issueDate.toISOString(), linkedLineCount, linkedLineValue, totalOrderedQuantity, totalReceivedQuantity, totalOutstandingQuantity, receiptCount: purchaseOrder.lines.reduce((sum, line) => sum + line.receiptLines.length, 0), }; }); const vendorMap = new Map(); for (const purchaseOrder of purchaseOrdersSummary) { const existing = vendorMap.get(purchaseOrder.vendorId); if (existing) { existing.orderCount += 1; existing.linkedLineValue = roundMoney(existing.linkedLineValue + purchaseOrder.linkedLineValue); existing.outstandingQuantity += purchaseOrder.totalOutstandingQuantity; continue; } vendorMap.set(purchaseOrder.vendorId, { vendorId: purchaseOrder.vendorId, vendorName: purchaseOrder.vendorName, orderCount: 1, linkedLineValue: purchaseOrder.linkedLineValue, outstandingQuantity: purchaseOrder.totalOutstandingQuantity, }); } const receiptMap = new Map(); for (const receiptLine of typedReceiptLines) { const receipt = receiptLine.purchaseReceipt; const existing = receiptMap.get(receipt.id); if (existing) { existing.totalQuantity += receiptLine.quantity; continue; } receiptMap.set(receipt.id, { receiptId: receipt.id, receiptNumber: receipt.receiptNumber, purchaseOrderId: receipt.purchaseOrder.id, purchaseOrderNumber: receipt.purchaseOrder.documentNumber, vendorName: receipt.purchaseOrder.vendor.name, receivedAt: receipt.receivedAt.toISOString(), totalQuantity: receiptLine.quantity, }); } const totalOrderedQuantity = purchaseOrdersSummary.reduce((sum, purchaseOrder) => sum + purchaseOrder.totalOrderedQuantity, 0); const totalReceivedQuantity = purchaseOrdersSummary.reduce((sum, purchaseOrder) => sum + purchaseOrder.totalReceivedQuantity, 0); const totalOutstandingQuantity = purchaseOrdersSummary.reduce((sum, purchaseOrder) => sum + purchaseOrder.totalOutstandingQuantity, 0); const linkedLineCount = purchaseOrdersSummary.reduce((sum, purchaseOrder) => sum + purchaseOrder.linkedLineCount, 0); const linkedLineValue = roundMoney( purchaseOrdersSummary.reduce((sum, purchaseOrder) => sum + purchaseOrder.linkedLineValue, 0) ); const outstandingPurchaseOrderCount = purchaseOrdersSummary.filter((purchaseOrder) => purchaseOrder.totalOutstandingQuantity > 0).length; const shortageItemCount = typedPlanning?.summary.uncoveredItemCount ?? 0; const totalUncoveredQuantity = typedPlanning?.summary.totalUncoveredQuantity ?? 0; const purchaseOutstandingPenalty = totalOrderedQuantity > 0 ? Math.min(12, Math.round((totalOutstandingQuantity / totalOrderedQuantity) * 12)) : 0; const readinessScore = Math.max( 0, 100 - Math.min(20, blockedMilestoneCount * 10) - Math.min(18, rollups.overdueMilestoneCount * 9) - Math.min(18, rollups.overdueWorkOrderCount * 9) - Math.min(24, shortageItemCount * 8) - purchaseOutstandingPenalty ); const commercialQuoteTotal = typedQuote ? calculateSalesDocumentTotal(typedQuote) : null; const commercialOrderTotal = typedSalesOrder ? calculateSalesDocumentTotal(typedSalesOrder) : null; return { commercial: { quoteTotal: commercialQuoteTotal, quoteStatus: typedQuote?.status ?? null, orderTotal: commercialOrderTotal, orderStatus: typedSalesOrder?.status ?? null, activeDocumentType: typedSalesOrder ? "ORDER" : typedQuote ? "QUOTE" : null, activeDocumentNumber: typedSalesOrder?.documentNumber ?? typedQuote?.documentNumber ?? null, activeDocumentStatus: typedSalesOrder?.status ?? typedQuote?.status ?? null, activeDocumentTotal: commercialOrderTotal ?? commercialQuoteTotal, }, purchasing: { linkedPurchaseOrderCount: purchaseOrdersSummary.length, openPurchaseOrderCount: purchaseOrdersSummary.filter((purchaseOrder) => purchaseOrder.status !== "CLOSED").length, vendorCount: vendorMap.size, linkedLineCount, linkedLineValue, totalOrderedQuantity, totalReceivedQuantity, totalOutstandingQuantity, purchaseOrders: purchaseOrdersSummary, vendors: [...vendorMap.values()].sort((left, right) => { if (right.outstandingQuantity !== left.outstandingQuantity) { return right.outstandingQuantity - left.outstandingQuantity; } return right.linkedLineValue - left.linkedLineValue; }), recentReceipts: [...receiptMap.values()] .sort((left, right) => new Date(right.receivedAt).getTime() - new Date(left.receivedAt).getTime()) .slice(0, 5), }, delivery: { shipmentNumber: typedShipment?.shipmentNumber ?? null, shipmentStatus: typedShipment?.status ?? null, shipDate: typedShipment?.shipDate ? typedShipment.shipDate.toISOString() : null, packageCount: typedShipment?.packageCount ?? 0, trackingNumber: typedShipment?.trackingNumber ?? null, carrier: typedShipment?.carrier ?? null, serviceLevel: typedShipment?.serviceLevel ?? null, }, risk: { readinessScore, riskLevel: deriveProjectRiskLevel(readinessScore), blockedMilestoneCount, overdueMilestoneCount: rollups.overdueMilestoneCount, overdueWorkOrderCount: rollups.overdueWorkOrderCount, shortageItemCount, totalUncoveredQuantity, outstandingPurchaseOrderCount, outstandingReceiptQuantity: totalOutstandingQuantity, }, }; } function mapProjectSummary(record: ProjectRecord): ProjectSummaryDto { return { id: record.id, projectNumber: record.projectNumber, name: record.name, status: record.status as ProjectStatus, priority: record.priority as ProjectPriority, customerId: record.customer.id, customerName: record.customer.name, ownerId: record.owner?.id ?? null, ownerName: getOwnerName(record.owner), dueDate: record.dueDate ? record.dueDate.toISOString() : null, updatedAt: record.updatedAt.toISOString(), rollups: buildProjectRollups(record), }; } function mapProjectDetail(record: ProjectRecord, cockpit: ProjectCockpitDto): ProjectDetailDto { return { ...mapProjectSummary(record), notes: record.notes, createdAt: record.createdAt.toISOString(), salesQuoteId: record.salesQuote?.id ?? null, salesQuoteNumber: record.salesQuote?.documentNumber ?? null, salesOrderId: record.salesOrder?.id ?? null, salesOrderNumber: record.salesOrder?.documentNumber ?? null, shipmentId: record.shipment?.id ?? null, shipmentNumber: record.shipment?.shipmentNumber ?? null, customerEmail: record.customer.email, customerPhone: record.customer.phone, milestones: record.milestones.map(mapProjectMilestone), cockpit, }; } function buildInclude() { return { customer: { select: { id: true, name: true, email: true, phone: true, }, }, owner: { select: { id: true, firstName: true, lastName: true, }, }, salesQuote: { select: { id: true, documentNumber: true, }, }, salesOrder: { select: { id: true, documentNumber: true, }, }, shipment: { select: { id: true, shipmentNumber: true, }, }, milestones: { select: { id: true, title: true, status: true, dueDate: true, completedAt: true, notes: true, sortOrder: true, }, orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], }, workOrders: { select: { id: true, status: true, dueDate: true, }, }, }; } async function nextProjectNumber() { const next = (await projectModel.count()) + 1; return `PRJ-${String(next).padStart(5, "0")}`; } async function validateProjectInput(payload: ProjectInput) { const customer = await prisma.customer.findUnique({ where: { id: payload.customerId }, select: { id: true }, }); if (!customer) { return { ok: false as const, reason: "Customer was not found." }; } if (payload.ownerId) { const owner = await prisma.user.findUnique({ where: { id: payload.ownerId }, select: { id: true, isActive: true }, }); if (!owner?.isActive) { return { ok: false as const, reason: "Project owner was not found." }; } } if (payload.salesQuoteId) { const quote = await prisma.salesQuote.findUnique({ where: { id: payload.salesQuoteId }, select: { id: true, customerId: true }, }); if (!quote) { return { ok: false as const, reason: "Linked quote was not found." }; } if (quote.customerId !== payload.customerId) { return { ok: false as const, reason: "Linked quote must belong to the selected customer." }; } } if (payload.salesOrderId) { const order = await prisma.salesOrder.findUnique({ where: { id: payload.salesOrderId }, select: { id: true, customerId: true }, }); if (!order) { return { ok: false as const, reason: "Linked sales order was not found." }; } if (order.customerId !== payload.customerId) { return { ok: false as const, reason: "Linked sales order must belong to the selected customer." }; } } if (payload.shipmentId) { const shipment = await prisma.shipment.findUnique({ where: { id: payload.shipmentId }, include: { salesOrder: { select: { customerId: true, }, }, }, }); if (!shipment) { return { ok: false as const, reason: "Linked shipment was not found." }; } if (shipment.salesOrder.customerId !== payload.customerId) { return { ok: false as const, reason: "Linked shipment must belong to the selected customer." }; } } return { ok: true as const }; } function normalizeMilestoneInput(milestone: ProjectMilestoneInput, index: number) { const title = milestone.title.trim(); const notes = milestone.notes.trim(); const sortOrder = Number.isFinite(milestone.sortOrder) ? milestone.sortOrder : index * 10; return { id: milestone.id?.trim() || null, title, status: milestone.status, dueDate: milestone.dueDate ? new Date(milestone.dueDate) : null, notes, sortOrder, }; } async function syncProjectMilestones(transaction: any, projectId: string, milestones: ProjectMilestoneInput[]) { const milestoneModel = (transaction as any).projectMilestone; const existingMilestones = await milestoneModel.findMany({ where: { projectId }, select: { id: true, completedAt: true, }, }); const existingById = new Map( existingMilestones.map((milestone: { id: string; completedAt: Date | null }) => [milestone.id, { completedAt: milestone.completedAt }]) ); const normalized = milestones.map(normalizeMilestoneInput); const retainedIds = normalized.flatMap((milestone) => (milestone.id && existingById.has(milestone.id) ? [milestone.id] : [])); await milestoneModel.deleteMany({ where: { projectId, ...(retainedIds.length > 0 ? { id: { notIn: retainedIds } } : {}), }, }); if (retainedIds.length === 0) { await milestoneModel.deleteMany({ where: { projectId } }); } for (const milestone of normalized) { const existing = milestone.id ? existingById.get(milestone.id) : null; const completedAt = milestone.status === "COMPLETE" ? existing?.completedAt ?? new Date() : null; if (milestone.id && existing) { await milestoneModel.update({ where: { id: milestone.id }, data: { title: milestone.title, status: milestone.status, dueDate: milestone.dueDate, completedAt, notes: milestone.notes, sortOrder: milestone.sortOrder, }, }); continue; } await milestoneModel.create({ data: { projectId, title: milestone.title, status: milestone.status, dueDate: milestone.dueDate, completedAt, notes: milestone.notes, sortOrder: milestone.sortOrder, }, }); } } export async function listProjectCustomerOptions(): Promise { const customers = await prisma.customer.findMany({ where: { status: { not: "INACTIVE", }, }, select: { id: true, name: true, email: true, }, orderBy: [{ name: "asc" }], }); return customers; } export async function listProjectOwnerOptions(): Promise { const users = await prisma.user.findMany({ where: { isActive: true, }, select: { id: true, firstName: true, lastName: true, email: true, }, orderBy: [{ firstName: "asc" }, { lastName: "asc" }], }); return users.map((user) => ({ id: user.id, fullName: `${user.firstName} ${user.lastName}`.trim(), email: user.email, })); } export async function listProjectQuoteOptions(customerId?: string | null): Promise { const quotes = await prisma.salesQuote.findMany({ where: { ...(customerId ? { customerId } : {}), }, include: { customer: { select: { name: true, }, }, }, orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }], }); return quotes.map((quote) => ({ id: quote.id, documentNumber: quote.documentNumber, customerName: quote.customer.name, status: quote.status, })); } export async function listProjectOrderOptions(customerId?: string | null): Promise { const orders = await prisma.salesOrder.findMany({ where: { ...(customerId ? { customerId } : {}), }, include: { customer: { select: { name: true, }, }, }, orderBy: [{ issueDate: "desc" }, { createdAt: "desc" }], }); return orders.map((order) => ({ id: order.id, documentNumber: order.documentNumber, customerName: order.customer.name, status: order.status, })); } export async function listProjectShipmentOptions(customerId?: string | null): Promise { const shipments = await prisma.shipment.findMany({ where: { ...(customerId ? { salesOrder: { customerId } } : {}), }, include: { salesOrder: { include: { customer: { select: { name: true, }, }, }, }, }, orderBy: [{ createdAt: "desc" }], }); return shipments.map((shipment) => ({ id: shipment.id, shipmentNumber: shipment.shipmentNumber, salesOrderNumber: shipment.salesOrder.documentNumber, customerName: shipment.salesOrder.customer.name, status: shipment.status, })); } export async function listProjects(filters: { q?: string; status?: ProjectStatus; priority?: ProjectPriority; customerId?: string; ownerId?: string; } = {}) { const query = filters.q?.trim(); const projects = await projectModel.findMany({ where: { ...(filters.status ? { status: filters.status } : {}), ...(filters.priority ? { priority: filters.priority } : {}), ...(filters.customerId ? { customerId: filters.customerId } : {}), ...(filters.ownerId ? { ownerId: filters.ownerId } : {}), ...(query ? { OR: [ { projectNumber: { contains: query } }, { name: { contains: query } }, { customer: { name: { contains: query } } }, ], } : {}), }, include: buildInclude(), orderBy: [{ dueDate: "asc" }, { updatedAt: "desc" }], }); return projects.map((project: unknown) => mapProjectSummary(project as ProjectRecord)); } export async function getProjectById(projectId: string) { const project = await projectModel.findUnique({ where: { id: projectId }, include: buildInclude(), }); if (!project) { return null; } const mappedProject = project as ProjectRecord; const cockpit = await buildProjectCockpit(mappedProject, buildProjectRollups(mappedProject)); return mapProjectDetail(mappedProject, cockpit); } export async function createProject(payload: ProjectInput, actorId?: string | null) { const validated = await validateProjectInput(payload); if (!validated.ok) { return { ok: false as const, reason: validated.reason }; } const projectNumber = await nextProjectNumber(); const created = await prisma.$transaction(async (transaction) => { const transactionProjectModel = (transaction as any).project; const createdProject = await transactionProjectModel.create({ data: { projectNumber, name: payload.name.trim(), status: payload.status, priority: payload.priority, customerId: payload.customerId, salesQuoteId: payload.salesQuoteId, salesOrderId: payload.salesOrderId, shipmentId: payload.shipmentId, ownerId: payload.ownerId, dueDate: payload.dueDate ? new Date(payload.dueDate) : null, notes: payload.notes, }, select: { id: true, }, }); await syncProjectMilestones(transaction, createdProject.id, payload.milestones ?? []); return createdProject; }); const project = await getProjectById(created.id); if (project) { await logAuditEvent({ actorId, entityType: "project", entityId: created.id, action: "created", summary: `Created project ${project.projectNumber}.`, metadata: { projectNumber: project.projectNumber, customerId: project.customerId, status: project.status, priority: project.priority, milestoneCount: project.rollups.milestoneCount, }, }); } return project ? { ok: true as const, project } : { ok: false as const, reason: "Unable to load saved project." }; } export async function updateProject(projectId: string, payload: ProjectInput, actorId?: string | null) { const existing = await projectModel.findUnique({ where: { id: projectId }, select: { id: true }, }); if (!existing) { return { ok: false as const, reason: "Project was not found." }; } const validated = await validateProjectInput(payload); if (!validated.ok) { return { ok: false as const, reason: validated.reason }; } await prisma.$transaction(async (transaction) => { const transactionProjectModel = (transaction as any).project; await transactionProjectModel.update({ where: { id: projectId }, data: { name: payload.name.trim(), status: payload.status, priority: payload.priority, customerId: payload.customerId, salesQuoteId: payload.salesQuoteId, salesOrderId: payload.salesOrderId, shipmentId: payload.shipmentId, ownerId: payload.ownerId, dueDate: payload.dueDate ? new Date(payload.dueDate) : null, notes: payload.notes, }, select: { id: true, }, }); await syncProjectMilestones(transaction, projectId, payload.milestones ?? []); }); const project = await getProjectById(projectId); if (project) { await logAuditEvent({ actorId, entityType: "project", entityId: projectId, action: "updated", summary: `Updated project ${project.projectNumber}.`, metadata: { projectNumber: project.projectNumber, customerId: project.customerId, status: project.status, priority: project.priority, milestoneCount: project.rollups.milestoneCount, }, }); } return project ? { ok: true as const, project } : { ok: false as const, reason: "Unable to load saved project." }; }