feat(inventory): support line-based assignments and movements

This commit is contained in:
2026-06-19 01:05:33 +02:00
parent 8e6a00c2a9
commit 6d34a2f74f
17 changed files with 713 additions and 189 deletions
+230 -56
View File
@@ -4,23 +4,79 @@ import prisma from "@/lib/prisma"
import type { CreateAssignmentData } from "@/schemas/assignment.schema"
import type { Assignment, AssignmentWithPersonItemAsset } from "@/types"
type LegacyAssignmentWriteData = CreateAssignmentData & {
createdBy?: string
createdById?: string
}
type LegacyAssignmentUpdateData = Partial<LegacyAssignmentWriteData> & {
returnDate?: Date | null
}
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 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<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[]> => {
return prisma.assignment.findMany({
const assignments = await prisma.assignment.findMany({
where: {
returnDate: {
equals: null,
},
},
include: {
person: true,
item: true,
asset: true,
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
},
include: assignmentInclude,
orderBy: {
createdAt: "desc",
},
})
return assignments.map(toLegacyAssignment)
},
findAllWithPersonPaginated: async ({
page,
@@ -31,14 +87,12 @@ export const AssignmentService = {
pageSize?: number
search?: string
}) => {
return paginate<AssignmentWithPersonItemAsset>({
const result = await paginate<AssignmentWithLines>({
model: prisma.assignment,
page,
pageSize,
where: {
returnDate: {
equals: null,
},
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
...(search
? {
OR: [
@@ -56,93 +110,213 @@ export const AssignmentService = {
}
: {}),
},
include: {
person: true,
item: true,
asset: true,
},
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> => {
return db.assignment.findUnique({
const assignment = await db.assignment.findUnique({
where: { id },
include: {
person: true,
item: true,
asset: true,
},
include: assignmentInclude,
})
return assignment ? toLegacyAssignment(assignment) : null
},
findAllByPerson: async (
personId: string,
): Promise<AssignmentWithPersonItemAsset[]> => {
return prisma.assignment.findMany({
where: { personId: personId },
include: {
person: true,
item: true,
asset: true,
},
const assignments = await prisma.assignment.findMany({
where: { personId },
include: assignmentInclude,
})
return assignments.map(toLegacyAssignment)
},
create: async (
data: CreateAssignmentData & { createdBy: string },
data: LegacyAssignmentWriteData,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Assignment> => {
const { personId, ...rest } = data
return db.assignment.create({
const {
personId,
itemId,
assetId,
quantity,
assignmentDate,
createdBy,
createdById,
notes,
} = data
const assignment = await db.assignment.create({
data: {
...rest,
personId: personId,
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> => {
return prisma.assignment.update({
where: { id },
data: {
returnDate: new Date(),
personId: null,
quantity: null,
assetId: null,
itemId: null,
},
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 result = await db.assignment.updateMany({
const closedAt = new Date()
const assignment = await db.assignment.findFirst({
where: {
id,
returnDate: null,
status: { in: ["OPEN", "PARTIALLY_RETURNED"] },
},
include: { stockLines: true, assetLines: { include: { asset: true } } },
})
if (!assignment) return false
await Promise.all(
assignment.stockLines.map((line) =>
db.assignmentStockLine.update({
where: { id: line.id },
data: { returnedQuantity: line.quantity },
}),
),
)
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: {
returnDate: new Date(),
personId: null,
quantity: null,
assetId: null,
itemId: null,
status: "RETURNED",
closedAt,
closedById: actorId,
},
})
return result.count === 1
return true
},
update: async (
id: string,
data: Prisma.AssignmentUpdateInput | Prisma.AssignmentUncheckedUpdateInput,
data: LegacyAssignmentUpdateData,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Assignment> => {
return db.assignment.update({
const {
itemId,
assetId,
quantity,
assignmentDate,
returnDate,
createdBy,
createdById,
...assignmentData
} = data
const assignment = await db.assignment.update({
where: { id },
data,
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 })
},
}