95 lines
4.5 KiB
TypeScript
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('、')} 仍处于挂载状态;请先卸载未勾选配件或重新核对后提交`
|
|
)
|
|
}
|