476 lines
12 KiB
TypeScript
476 lines
12 KiB
TypeScript
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<LegacyAssignmentWriteData> & {
|
|
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<string, AssignmentStockReturnInput>()
|
|
|
|
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<Prisma.AssignmentDefaultArgs>,
|
|
data: Pick<LegacyAssignmentUpdateData, "itemId" | "assetId" | "quantity">,
|
|
): 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<AssignmentWithPersonItemAsset[]> => {
|
|
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<AssignmentWithLines>({
|
|
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<AssignmentWithPersonItemAsset | null> => {
|
|
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<ReturnStockAssignmentResult | null> => {
|
|
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<AssignmentWithPersonItemAsset[]> => {
|
|
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<Assignment> => {
|
|
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<Assignment> => {
|
|
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<boolean> => {
|
|
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<Assignment> => {
|
|
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 })
|
|
},
|
|
}
|