import type { Prisma } from "@/generated/prisma/client" import { paginate } from "@/lib/paginate" import prisma from "@/lib/prisma" import type { CreateAssignmentData } from "@/schemas/assignment.schema" import type { Assignment, AssignmentStockReturnInput, AssignmentWithPersonItemAsset, } from "@/types" type LegacyAssignmentWriteData = CreateAssignmentData & { createdBy?: string createdById?: string } type LegacyAssignmentUpdateData = Partial & { returnDate?: Date | null } type ReturnStockAssignmentResult = { returnedQuantity: number fullyReturned: boolean returnedLines: { itemId: string; quantity: number }[] } type AssignmentWithLines = Prisma.AssignmentGetPayload<{ include: { person: true stockLines: { include: { item: true } } assetLines: { include: { asset: { include: { item: true } } } } } }> const assignmentInclude = { person: true, stockLines: { include: { item: true } }, assetLines: { include: { asset: { include: { item: true } } } }, } satisfies Prisma.AssignmentInclude function normalizeReturnLines( returns: AssignmentStockReturnInput[], ): AssignmentStockReturnInput[] { const aggregated = new Map() for (const entry of returns) { const existing = aggregated.get(entry.assignmentLineId) if (existing) { aggregated.set(entry.assignmentLineId, { ...existing, quantity: existing.quantity + entry.quantity, notes: existing.notes ?? entry.notes, }) continue } aggregated.set(entry.assignmentLineId, { ...entry }) } return [...aggregated.values()] } function toLegacyAssignment( assignment: AssignmentWithLines, ): AssignmentWithPersonItemAsset { const stockLine = assignment.stockLines[0] const assetLine = assignment.assetLines[0] const item = stockLine?.item ?? assetLine?.asset.item ?? null const asset = assetLine?.asset ?? null const quantity = stockLine?.quantity ?? (assetLine ? 1 : null) const returnDate = assignment.closedAt ?? assetLine?.returnedAt ?? null return { ...assignment, assignmentDate: assignment.assignedAt, returnDate, person: assignment.person, item, asset, itemId: item?.id ?? null, assetId: asset?.id ?? null, quantity, } } function toLegacyAssignmentRecord( assignment: Prisma.AssignmentGetPayload, data: Pick, ): Assignment { return { ...assignment, assignmentDate: assignment.assignedAt, returnDate: assignment.closedAt, itemId: data.itemId ?? null, assetId: data.assetId ?? null, quantity: data.quantity ?? null, } } export const AssignmentService = { findAllWithPerson: async (): Promise => { const assignments = await prisma.assignment.findMany({ where: { status: { in: ["OPEN", "PARTIALLY_RETURNED"] }, }, include: assignmentInclude, orderBy: { createdAt: "desc", }, }) return assignments.map(toLegacyAssignment) }, findAllWithPersonPaginated: async ({ page, pageSize, search, }: { page?: number pageSize?: number search?: string }) => { const result = await paginate({ model: prisma.assignment, page, pageSize, where: { status: { in: ["OPEN", "PARTIALLY_RETURNED"] }, ...(search ? { OR: [ { person: { firstName: { contains: search, mode: "insensitive" }, }, }, { person: { lastName: { contains: search, mode: "insensitive" }, }, }, ], } : {}), }, include: assignmentInclude, orderBy: { createdAt: "desc", }, }) return { ...result, data: result.data.map(toLegacyAssignment), } }, findById: async ( id: string, db: Prisma.TransactionClient | typeof prisma = prisma, ): Promise => { const assignment = await db.assignment.findUnique({ where: { id }, include: assignmentInclude, }) return assignment ? toLegacyAssignment(assignment) : null }, returnStockAssignment: async ( id: string, receivedById: string | undefined, returns: AssignmentStockReturnInput[] | undefined, db: Prisma.TransactionClient | typeof prisma = prisma, ): Promise => { const assignment = await db.assignment.findFirst({ where: { id, status: { in: ["OPEN", "PARTIALLY_RETURNED"] }, }, include: { stockLines: { orderBy: { createdAt: "asc", }, }, }, }) if (!assignment || assignment.stockLines.length === 0) { return null } const returnerId = receivedById ?? assignment.createdById const remainingReturns = returns !== undefined ? normalizeReturnLines(returns) : assignment.stockLines .filter((line) => line.returnedQuantity < line.quantity) .map((line) => ({ assignmentLineId: line.id, quantity: line.quantity - line.returnedQuantity, notes: undefined, })) if (remainingReturns.length === 0) { return null } const stockLinesById = new Map( assignment.stockLines.map((line) => [line.id, line]), ) const returnTimestamp = new Date() let returnedQuantity = 0 const returnedLines: { itemId: string; quantity: number }[] = [] for (const returnLine of remainingReturns) { const stockLine = stockLinesById.get(returnLine.assignmentLineId) if (!stockLine) { return null } if (!Number.isInteger(returnLine.quantity) || returnLine.quantity <= 0) { return null } const remainingQuantity = stockLine.quantity - stockLine.returnedQuantity if (returnLine.quantity > remainingQuantity) { return null } await db.assignmentStockReturn.create({ data: { assignmentLineId: stockLine.id, quantity: returnLine.quantity, receivedById: returnerId, returnedAt: returnTimestamp, notes: returnLine.notes, }, }) await db.assignmentStockLine.update({ where: { id: stockLine.id }, data: { returnedQuantity: { increment: returnLine.quantity, }, }, }) returnedQuantity += returnLine.quantity returnedLines.push({ itemId: stockLine.itemId, quantity: returnLine.quantity, }) } const fullyReturned = assignment.stockLines.every( (line) => line.returnedQuantity + (remainingReturns.find((entry) => entry.assignmentLineId === line.id) ?.quantity ?? 0) >= line.quantity, ) await db.assignment.update({ where: { id }, data: { status: fullyReturned ? "RETURNED" : "PARTIALLY_RETURNED", closedAt: fullyReturned ? returnTimestamp : null, closedById: fullyReturned ? returnerId : null, }, }) return { returnedQuantity, fullyReturned, returnedLines, } }, findAllByPerson: async ( personId: string, ): Promise => { const assignments = await prisma.assignment.findMany({ where: { personId }, include: assignmentInclude, }) return assignments.map(toLegacyAssignment) }, create: async ( data: LegacyAssignmentWriteData, db: Prisma.TransactionClient | typeof prisma = prisma, ): Promise => { const { personId, itemId, assetId, quantity, assignmentDate, createdBy, createdById, notes, } = data const assignment = await db.assignment.create({ data: { personId, notes, assignedAt: assignmentDate, createdById: createdById ?? createdBy ?? "", stockLines: !assetId && itemId ? { create: { itemId, quantity, notes, }, } : undefined, assetLines: assetId ? { create: { assetId, assignedAt: assignmentDate, notes, }, } : undefined, }, }) return toLegacyAssignmentRecord(assignment, { itemId, assetId, quantity }) }, delete: async (id: string): Promise => { const closedAt = new Date() return prisma.$transaction(async (tx) => { const assignmentWithAssetLines = await tx.assignment.findUniqueOrThrow({ where: { id }, include: { assetLines: { include: { asset: true } } }, }) await Promise.all( assignmentWithAssetLines.assetLines .filter((line) => !line.returnedAt) .map((line) => tx.assignmentAssetLine.update({ where: { id: line.id }, data: { returnedAt: closedAt, returnedById: assignmentWithAssetLines.createdById, returnStatus: line.asset.status, }, }), ), ) const assignment = await tx.assignment.update({ where: { id }, data: { status: "RETURNED", closedAt, }, }) return toLegacyAssignmentRecord(assignment, {}) }) }, markReturnedIfActive: async ( id: string, actorId?: string, db: Prisma.TransactionClient | typeof prisma = prisma, ): Promise => { const closedAt = new Date() const assignment = await db.assignment.findFirst({ where: { id, status: { in: ["OPEN", "PARTIALLY_RETURNED"] }, }, include: { stockLines: true, assetLines: { include: { asset: true } }, }, }) if (!assignment) return false if (assignment.stockLines.length > 0) { const result = await AssignmentService.returnStockAssignment( id, actorId ?? assignment.createdById, undefined, db, ) return Boolean(result?.fullyReturned) } await Promise.all( assignment.assetLines .filter((line) => !line.returnedAt) .map((line) => db.assignmentAssetLine.update({ where: { id: line.id }, data: { returnedAt: closedAt, returnedById: actorId ?? assignment.createdById, returnStatus: line.asset.status, }, }), ), ) await db.assignment.update({ where: { id }, data: { status: "RETURNED", closedAt, closedById: actorId, }, }) return true }, update: async ( id: string, data: LegacyAssignmentUpdateData, db: Prisma.TransactionClient | typeof prisma = prisma, ): Promise => { const { itemId, assetId, quantity, assignmentDate, returnDate, createdBy, createdById, ...assignmentData } = data const assignment = await db.assignment.update({ where: { id }, data: { ...assignmentData, assignedAt: assignmentDate, closedAt: returnDate, status: returnDate ? "RETURNED" : "OPEN", createdById: createdById ?? createdBy, }, }) if (itemId || quantity) { await db.assignmentStockLine.updateMany({ where: { assignmentId: id }, data: { itemId, quantity, }, }) } if (assetId || returnDate) { await db.assignmentAssetLine.updateMany({ where: { assignmentId: id }, data: { assetId, returnedAt: returnDate, }, }) } return toLegacyAssignmentRecord(assignment, { itemId, assetId, quantity }) }, }