365 lines
11 KiB
TypeScript
365 lines
11 KiB
TypeScript
import { Request, Response } from 'express'
|
|
import { Prisma } from '@prisma/client'
|
|
import prisma from '../utils/prisma'
|
|
import { successResponse, errorResponse } from '../utils/response'
|
|
import {
|
|
getEquipmentDisplayAvailableValue,
|
|
getEquipmentDisplayBorrowedValue,
|
|
getEquipmentDisplayItemValue,
|
|
getEquipmentDisplayTotalValue,
|
|
sumSnapshotChildrenValue
|
|
} from '../utils/assetValue'
|
|
|
|
type DisplayValueRow = {
|
|
equipment_type?: string | null
|
|
status?: string | null
|
|
total_value?: unknown
|
|
borrowed_value?: unknown
|
|
available_value?: unknown
|
|
total_value_with_children?: unknown
|
|
}
|
|
|
|
type DisplayValueSummary = {
|
|
total_value: number
|
|
borrowed_value: number
|
|
available_value: number
|
|
maintenance_value: number
|
|
}
|
|
|
|
const toNumber = (value: unknown) => {
|
|
if (value === null || value === undefined) return 0
|
|
const parsed = Number(value)
|
|
return Number.isNaN(parsed) ? 0 : parsed
|
|
}
|
|
|
|
const buildEquipmentWhere = (equipmentType?: string, startDate?: string, endDate?: string) => {
|
|
const where: Prisma.EquipmentWhereInput = {}
|
|
|
|
if (equipmentType) {
|
|
where.equipment_type = equipmentType
|
|
}
|
|
|
|
if (startDate || endDate) {
|
|
where.created_at = {}
|
|
if (startDate) {
|
|
where.created_at.gte = new Date(startDate)
|
|
}
|
|
if (endDate) {
|
|
const endOfDay = new Date(endDate)
|
|
endOfDay.setHours(23, 59, 59, 999)
|
|
where.created_at.lte = endOfDay
|
|
}
|
|
}
|
|
|
|
return where
|
|
}
|
|
|
|
const summarizeDisplayValues = (rows: DisplayValueRow[]): DisplayValueSummary =>
|
|
rows.reduce<DisplayValueSummary>(
|
|
(summary, row) => {
|
|
const totalValue = getEquipmentDisplayTotalValue(row)
|
|
summary.total_value += totalValue
|
|
summary.borrowed_value += getEquipmentDisplayBorrowedValue(row)
|
|
summary.available_value += getEquipmentDisplayAvailableValue(row)
|
|
|
|
if (row.status === '维护中') {
|
|
summary.maintenance_value += totalValue
|
|
}
|
|
|
|
return summary
|
|
},
|
|
{
|
|
total_value: 0,
|
|
borrowed_value: 0,
|
|
available_value: 0,
|
|
maintenance_value: 0
|
|
}
|
|
)
|
|
|
|
// 获取首页统计数据
|
|
export const getDashboardStats = async (req: Request, res: Response) => {
|
|
try {
|
|
const startOfToday = new Date(new Date().setHours(0, 0, 0, 0))
|
|
|
|
const [
|
|
equipmentTotals,
|
|
availableInventoryTotals,
|
|
borrowableInventoryTotals,
|
|
maintenanceTotals,
|
|
inventoryByType,
|
|
displayValueRows,
|
|
todayBorrowCount,
|
|
todayReturnCount,
|
|
todayBorrowRecords,
|
|
todayReturnRecords
|
|
] = await Promise.all([
|
|
prisma.equipment.aggregate({
|
|
_sum: {
|
|
total_quantity: true,
|
|
total_value: true,
|
|
borrowed_value: true,
|
|
available_value: true
|
|
}
|
|
}),
|
|
prisma.equipment.aggregate({
|
|
where: {
|
|
status: '可用'
|
|
},
|
|
_sum: {
|
|
remaining_quantity: true
|
|
}
|
|
}),
|
|
prisma.equipment.aggregate({
|
|
where: {
|
|
status: {
|
|
in: ['可用', '已借出']
|
|
}
|
|
},
|
|
_sum: {
|
|
total_quantity: true,
|
|
remaining_quantity: true
|
|
}
|
|
}),
|
|
prisma.equipment.aggregate({
|
|
where: { status: '维护中' },
|
|
_sum: {
|
|
total_quantity: true,
|
|
total_value: true
|
|
}
|
|
}),
|
|
prisma.equipment.groupBy({
|
|
by: ['equipment_type', 'status'],
|
|
_sum: {
|
|
total_quantity: true,
|
|
remaining_quantity: true
|
|
}
|
|
}),
|
|
prisma.equipment.findMany({
|
|
where: {
|
|
parent_id: null
|
|
},
|
|
select: {
|
|
status: true,
|
|
total_value: true,
|
|
borrowed_value: true,
|
|
available_value: true,
|
|
total_value_with_children: true
|
|
}
|
|
}),
|
|
prisma.borrowRecord.count({
|
|
where: {
|
|
borrow_time: {
|
|
gte: startOfToday
|
|
}
|
|
}
|
|
}),
|
|
prisma.borrowRecord.count({
|
|
where: {
|
|
return_time: {
|
|
gte: startOfToday
|
|
}
|
|
}
|
|
}),
|
|
prisma.borrowRecord.findMany({
|
|
where: {
|
|
borrow_time: {
|
|
gte: startOfToday
|
|
}
|
|
},
|
|
select: {
|
|
children_snapshot: true,
|
|
equipment: {
|
|
select: {
|
|
unit_price: true
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
prisma.borrowRecord.findMany({
|
|
where: {
|
|
return_time: {
|
|
gte: startOfToday
|
|
}
|
|
},
|
|
select: {
|
|
children_snapshot: true,
|
|
equipment: {
|
|
select: {
|
|
unit_price: true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
])
|
|
|
|
const totalEquipmentQuantity = toNumber(equipmentTotals._sum.total_quantity)
|
|
const availableEquipmentQuantity = toNumber(availableInventoryTotals._sum.remaining_quantity)
|
|
const borrowedEquipmentQuantity = Math.max(
|
|
toNumber(borrowableInventoryTotals._sum.total_quantity) - toNumber(borrowableInventoryTotals._sum.remaining_quantity),
|
|
0
|
|
)
|
|
const maintenanceEquipmentQuantity = toNumber(maintenanceTotals._sum.total_quantity)
|
|
const displayValueSummary = summarizeDisplayValues(displayValueRows)
|
|
const totalValue = displayValueSummary.total_value
|
|
const borrowedValue = displayValueSummary.borrowed_value
|
|
const availableValue = displayValueSummary.available_value
|
|
const maintenanceValue = displayValueSummary.maintenance_value
|
|
const todayBorrowValue = todayBorrowRecords.reduce((sum, record) => {
|
|
return sum + toNumber(record.equipment?.unit_price) + sumSnapshotChildrenValue(record.children_snapshot)
|
|
}, 0)
|
|
const todayReturnValue = todayReturnRecords.reduce((sum, record) => {
|
|
return sum + toNumber(record.equipment?.unit_price) + sumSnapshotChildrenValue(record.children_snapshot)
|
|
}, 0)
|
|
const usageRate =
|
|
totalEquipmentQuantity > 0 ? (borrowedEquipmentQuantity / totalEquipmentQuantity) * 100 : 0
|
|
const valueUsageRate = totalValue > 0 ? (borrowedValue / totalValue) * 100 : 0
|
|
|
|
successResponse(res, {
|
|
total_equipment: totalEquipmentQuantity,
|
|
available_equipment: availableEquipmentQuantity,
|
|
borrowed_equipment: borrowedEquipmentQuantity,
|
|
maintenance_equipment: maintenanceEquipmentQuantity,
|
|
usage_rate: usageRate,
|
|
today_borrow: todayBorrowCount,
|
|
today_return: todayReturnCount,
|
|
total_value: totalValue,
|
|
borrowed_value: borrowedValue,
|
|
available_value: availableValue,
|
|
maintenance_value: maintenanceValue,
|
|
value_usage_rate: valueUsageRate,
|
|
today_borrow_value: todayBorrowValue,
|
|
today_return_value: todayReturnValue,
|
|
inventory_by_type: Array.from(
|
|
inventoryByType.reduce((map, item) => {
|
|
const equipmentType = item.equipment_type || '其他'
|
|
const current = map.get(equipmentType) || {
|
|
equipment_type: equipmentType,
|
|
total_quantity: 0,
|
|
available_quantity: 0,
|
|
borrowed_quantity: 0
|
|
}
|
|
const totalQuantity = toNumber(item._sum.total_quantity)
|
|
const remainingQuantity = toNumber(item._sum.remaining_quantity)
|
|
|
|
current.total_quantity += totalQuantity
|
|
|
|
if (item.status === '可用') {
|
|
current.available_quantity += remainingQuantity
|
|
current.borrowed_quantity += Math.max(totalQuantity - remainingQuantity, 0)
|
|
} else if (item.status === '已借出') {
|
|
current.borrowed_quantity += Math.max(totalQuantity - remainingQuantity, 0)
|
|
}
|
|
|
|
map.set(equipmentType, current)
|
|
return map
|
|
}, new Map<string, {
|
|
equipment_type: string
|
|
total_quantity: number
|
|
available_quantity: number
|
|
borrowed_quantity: number
|
|
}>()).values()
|
|
)
|
|
.sort((a, b) => b.total_quantity - a.total_quantity)
|
|
.filter((item) => item.total_quantity > 0)
|
|
}, '获取成功')
|
|
} catch (error: any) {
|
|
errorResponse(res, error.message || '获取统计数据失败')
|
|
}
|
|
}
|
|
|
|
// 获取资产价值统计
|
|
export const getValueStatistics = async (req: Request, res: Response) => {
|
|
try {
|
|
const { equipment_type, start_date, end_date } = req.query
|
|
|
|
const where = buildEquipmentWhere(
|
|
typeof equipment_type === 'string' ? equipment_type : undefined,
|
|
typeof start_date === 'string' ? start_date : undefined,
|
|
typeof end_date === 'string' ? end_date : undefined
|
|
)
|
|
|
|
const displayValueWhere: Prisma.EquipmentWhereInput = {
|
|
...where,
|
|
parent_id: null
|
|
}
|
|
|
|
const [displayValueRows, topEquipmentRows] = await Promise.all([
|
|
prisma.equipment.findMany({
|
|
where: displayValueWhere,
|
|
select: {
|
|
equipment_type: true,
|
|
status: true,
|
|
total_value: true,
|
|
borrowed_value: true,
|
|
available_value: true,
|
|
total_value_with_children: true
|
|
}
|
|
}),
|
|
prisma.equipment.findMany({
|
|
where,
|
|
select: {
|
|
equipment_name: true,
|
|
unit_price: true,
|
|
total_quantity: true,
|
|
total_value: true,
|
|
total_value_with_children: true,
|
|
borrowed_value: true,
|
|
available_value: true,
|
|
status: true
|
|
},
|
|
})
|
|
])
|
|
|
|
const summary = summarizeDisplayValues(displayValueRows)
|
|
const valueByType = Array.from(
|
|
displayValueRows.reduce((map, item) => {
|
|
const equipmentType = item.equipment_type || '未分类'
|
|
const current = map.get(equipmentType) || {
|
|
equipment_type: equipmentType,
|
|
total_value: 0,
|
|
borrowed_value: 0,
|
|
available_value: 0
|
|
}
|
|
|
|
current.total_value += getEquipmentDisplayTotalValue(item)
|
|
current.borrowed_value += getEquipmentDisplayBorrowedValue(item)
|
|
current.available_value += getEquipmentDisplayAvailableValue(item)
|
|
|
|
map.set(equipmentType, current)
|
|
return map
|
|
}, new Map<string, {
|
|
equipment_type: string
|
|
total_value: number
|
|
borrowed_value: number
|
|
available_value: number
|
|
}>())
|
|
.values()
|
|
)
|
|
.sort((left, right) => right.total_value - left.total_value)
|
|
.filter((item) => item.total_value > 0)
|
|
const topEquipment = topEquipmentRows
|
|
.map((eq) => ({
|
|
equipment_name: eq.equipment_name,
|
|
quantity: toNumber(eq.total_quantity) || 1,
|
|
unit_price: toNumber(eq.unit_price),
|
|
item_value: getEquipmentDisplayItemValue(eq),
|
|
stock_total_value: getEquipmentDisplayTotalValue(eq),
|
|
stock_borrowed_value: getEquipmentDisplayBorrowedValue(eq),
|
|
stock_available_value: getEquipmentDisplayAvailableValue(eq)
|
|
}))
|
|
.sort((a, b) => b.item_value - a.item_value || b.stock_total_value - a.stock_total_value)
|
|
.slice(0, 10)
|
|
|
|
successResponse(res, {
|
|
total_value: summary.total_value,
|
|
borrowed_value: summary.borrowed_value,
|
|
available_value: summary.available_value,
|
|
maintenance_value: summary.maintenance_value,
|
|
value_by_type: valueByType,
|
|
top_equipment: topEquipment
|
|
}, '获取成功')
|
|
} catch (error: any) {
|
|
errorResponse(res, error.message || '获取价值统计失败')
|
|
}
|
|
}
|