GuDingZiChan/backend/src/utils/disposalSubmission.ts

95 lines
4.5 KiB
TypeScript

import type { Prisma } from '@prisma/client'
export const MAX_DISPOSAL_BATCH_ROOTS = 500
export const disposalTreeSelect = {
id: true, equipment_code: true, equipment_name: true, parent_id: true,
department_id: true, storage_location_id: true, status: true, mount_status: true,
total_quantity: true, remaining_quantity: true, unit_price: true, updated_at: true
} as const
export type DisposalTreeItem = Prisma.EquipmentGetPayload<{ select: typeof disposalTreeSelect }>
export class DisposalConflictError extends Error {
status = 409
}
const blockedStatuses = new Set(['已借出', '维护中', '待报废', '已报废', '已丢失'])
// One lookup per depth across the entire set of candidates, not per asset.
export const loadDisposalTrees = async (client: Prisma.TransactionClient, roots: DisposalTreeItem[]) => {
const byParent = new Map<number, DisposalTreeItem[]>()
const visited = new Set(roots.map(root => root.id))
let frontier = [...visited]
while (frontier.length > 0) {
const children = await client.equipment.findMany({
where: { parent_id: { in: frontier } }, select: disposalTreeSelect, orderBy: { id: 'asc' }
})
frontier = []
for (const child of children) {
const parentId = child.parent_id!
const siblings = byParent.get(parentId)
if (siblings) siblings.push(child)
else byParent.set(parentId, [child])
if (!visited.has(child.id)) {
visited.add(child.id)
frontier.push(child.id)
}
}
}
return new Map(roots.map(root => {
const tree = [root]
const seen = new Set([root.id])
for (const item of tree) {
for (const child of byParent.get(item.id) || []) {
if (seen.has(child.id)) throw new DisposalConflictError('资产挂载关系存在循环,请先核查')
seen.add(child.id)
tree.push(child)
}
}
return [root.id, tree] as const
}))
}
export const getDisposalOccupancy = async (client: Prisma.TransactionClient, ids: number[]) => {
const equipmentIds = { in: ids }
const [loans, maintenance, disposals] = await Promise.all([
client.borrowRecord.findMany({ where: { equipment_id: equipmentIds, status: '已借出' }, select: { equipment_id: true }, distinct: ['equipment_id'] }),
client.maintenanceRecord.findMany({ where: { equipment_id: equipmentIds, status: 'in_progress' }, select: { equipment_id: true }, distinct: ['equipment_id'] }),
client.disposalRecord.findMany({
where: { status: { in: ['pending', 'approved'] }, OR: [{ equipment_id: equipmentIds }, { items: { some: { equipment_id: equipmentIds } } }] },
select: { equipment_id: true, items: { where: { equipment_id: equipmentIds }, select: { equipment_id: true } } }
})
])
return {
borrowed: new Set(loans.map(row => row.equipment_id)),
maintenance: new Set(maintenance.map(row => row.equipment_id)),
disposal: new Set(disposals.flatMap(row => [row.equipment_id, ...row.items.map(item => item.equipment_id)]))
}
}
export const getDisposalBlockedReason = (tree: DisposalTreeItem[], occupancy?: Awaited<ReturnType<typeof getDisposalOccupancy>>) => {
if (tree[0].parent_id !== null) return '挂载中的子资产不能单独申请报废,请先卸载'
const blocked = tree.find(item => blockedStatuses.has(item.status))
if (blocked) return blocked.status === '维护中'
? '资产正在维修中,请先完成或取消维修'
: `资产 ${blocked.equipment_code} 当前为${blocked.status},不能申请报废`
if (tree.some(item => occupancy?.borrowed.has(item.id))) return '资产树存在未归还记录,不能申请报废'
if (tree.some(item => occupancy?.maintenance.has(item.id))) return '资产树存在进行中的维修,请先完成或取消维修'
if (tree.some(item => occupancy?.disposal.has(item.id))) return '资产树已有进行中的报废申请'
return null
}
export const assertDisposalItemsConfirmed = (tree: DisposalTreeItem[], requestedIds: number[]) => {
if (!requestedIds.includes(tree[0].id)) throw new Error('报废明细必须包含主资产')
const treeIds = new Set(tree.map(item => item.id))
if (requestedIds.some(id => !treeIds.has(id))) {
throw new DisposalConflictError('报废明细包含不属于当前资产树的资产,请重新核对')
}
const requested = new Set(requestedIds)
const omitted = tree.filter(item => !requested.has(item.id))
if (omitted.length > 0) throw new DisposalConflictError(
`配件 ${omitted.map(item => item.equipment_code).join('、')} 仍处于挂载状态;请先卸载未勾选配件或重新核对后提交`
)
}