GuDingZiChan/backend/src/controllers/statisticsController.ts

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 || '获取价值统计失败')
}
}