Files
BI/src/lib/production.ts
T

397 lines
11 KiB
TypeScript

import type {
OrderSummary,
ProductionNode,
ProductionStatus,
RiskEvent,
RiskLevel,
SupplyType,
} from '@/types'
import { clamp, formatTime } from '@/lib/utils'
export const supplyMeta: Record<
SupplyType,
{ label: string; shortLabel: string; className: string }
> = {
self_made: { label: '自产', shortLabel: '自产', className: 'supply-self' },
outsourced: { label: '委外', shortLabel: '委外', className: 'supply-outsourced' },
purchased: { label: '外购', shortLabel: '外购', className: 'supply-purchased' },
}
export const statusMeta: Record<
ProductionStatus,
{ label: string; className: string }
> = {
waiting: { label: '待开始', className: 'status-waiting' },
in_progress: { label: '进行中', className: 'status-progress' },
done: { label: '已完成', className: 'status-done' },
delayed: { label: '已延期', className: 'status-delayed' },
blocked: { label: '阻塞', className: 'status-blocked' },
}
export const riskMeta: Record<RiskLevel, { label: string; className: string }> = {
normal: { label: '正常', className: 'risk-normal' },
warning: { label: '预警', className: 'risk-warning' },
critical: { label: '严重', className: 'risk-critical' },
}
export function flattenTree(node: ProductionNode): ProductionNode[] {
return [node, ...(node.children ?? []).flatMap(flattenTree)]
}
export interface OrderTreeStats {
totalNodes: number
partNodes: number
componentNodes: number
materialNodes: number
processNodes: number
maxDepth: number
}
export function calculateOrderTreeStats(root: ProductionNode): OrderTreeStats {
const stats: OrderTreeStats = {
totalNodes: 0,
partNodes: 0,
componentNodes: 0,
materialNodes: 0,
processNodes: 0,
maxDepth: 0,
}
const visit = (node: ProductionNode, depth: number) => {
stats.totalNodes += 1
stats.maxDepth = Math.max(stats.maxDepth, depth)
if (node.nodeType === 'part') stats.partNodes += 1
if (node.nodeType === 'component') stats.componentNodes += 1
if (node.nodeType === 'material') stats.materialNodes += 1
if (node.nodeType === 'process') stats.processNodes += 1
for (const child of node.children ?? []) {
visit(child, depth + 1)
}
}
visit(root, 0)
return stats
}
export function findNode(
root: ProductionNode | undefined,
nodeId: string | null,
): ProductionNode | undefined {
if (!root || !nodeId) {
return undefined
}
if (root.id === nodeId) {
return root
}
for (const child of root.children ?? []) {
const found = findNode(child, nodeId)
if (found) {
return found
}
}
return undefined
}
export function findNodePath(
root: ProductionNode,
nodeId: string | null,
): ProductionNode[] {
if (!nodeId) {
return []
}
if (root.id === nodeId) {
return [root]
}
for (const child of root.children ?? []) {
const childPath = findNodePath(child, nodeId)
if (childPath.length > 0) {
return [root, ...childPath]
}
}
return []
}
export function getPathEdgeIds(path: ProductionNode[]) {
return new Set(
path.slice(1).map((node, index) => `${path[index].id}-${node.id}`),
)
}
export function defaultExpandedIds(root: ProductionNode) {
const expanded = new Set<string>([root.id])
const priorityPart = root.children?.[1] ?? root.children?.[0]
if (priorityPart) {
expanded.add(priorityPart.id)
}
for (const component of priorityPart?.children?.slice(0, 2) ?? []) {
expanded.add(component.id)
}
return expanded
}
export function defaultSelectedNodeId(root: ProductionNode) {
const priorityPart = root.children?.[1] ?? root.children?.[0]
const priorityComponent = priorityPart?.children?.[1] ?? priorityPart?.children?.[0]
const priorityOperation = priorityComponent?.children?.[0]
return priorityOperation?.id ?? priorityComponent?.id ?? priorityPart?.id ?? root.id
}
export function recalculateTree(node: ProductionNode): ProductionNode {
const children = node.children?.map(recalculateTree) ?? []
const hasChildren = children.length > 0
if (!hasChildren) {
const progress = node.requiredQty <= 0
? 0
: clamp((node.completedQty / node.requiredQty) * 100, 0, 100)
const status = deriveLeafStatus(node, progress)
const riskLevel = deriveLeafRisk(node, progress, status)
return {
...node,
completedQty: Math.min(node.completedQty, node.requiredQty),
defectQty: Math.min(node.defectQty, Math.max(0, node.completedQty)),
progress,
status,
riskLevel,
}
}
const totalWeight = children.reduce((total, child) => total + child.requiredQty, 0)
const weightedProgress =
totalWeight === 0
? 0
: children.reduce(
(total, child) => total + child.progress * child.requiredQty,
0,
) / totalWeight
const delayDays = Math.max(node.delayDays, ...children.map((child) => child.delayDays))
const status = deriveParentStatus(children, weightedProgress)
const riskLevel = deriveParentRisk(children, delayDays, status)
return {
...node,
children,
completedQty: Math.round((node.requiredQty * weightedProgress) / 100),
defectQty: children.reduce((total, child) => total + child.defectQty, 0),
progress: weightedProgress,
status,
riskLevel,
delayDays,
}
}
export function recalculateOrder(order: OrderSummary): OrderSummary {
const root = recalculateTree(order.root)
const nodes = flattenTree(root)
const events = buildRiskEvents(order, nodes)
const riskCount = nodes.filter((node) => node.riskLevel !== 'normal').length
const plannedQty = Math.max(root.completedQty, Math.round(root.requiredQty * clamp(root.progress + 10, 0, 100) / 100))
const planAchievement = plannedQty === 0 ? 0 : clamp((root.completedQty / plannedQty) * 100, 0, 120)
return {
...order,
root,
requiredQty: root.requiredQty,
completedQty: root.completedQty,
progress: root.progress,
status: root.status,
riskLevel: root.riskLevel,
delayDays: root.delayDays,
planAchievement,
dailyDelta: clamp(order.dailyDelta + (Math.random() - 0.45) * 0.28, -9.9, 9.9),
events,
trend: [
...order.trend.slice(-11),
{
time: formatTime(),
completion: Math.round(root.progress),
risk: riskCount,
plannedQty,
actualQty: root.completedQty,
achievement: planAchievement,
},
],
}
}
export function simulateOrdersTick(orders: OrderSummary[]): OrderSummary[] {
return orders.map((order, orderIndex) => {
const root = updateLeaves(order.root, orderIndex)
return recalculateOrder({ ...order, root })
})
}
function updateLeaves(node: ProductionNode, orderIndex: number): ProductionNode {
if (node.children?.length) {
return {
...node,
children: node.children.map((child) => updateLeaves(child, orderIndex)),
}
}
if (node.status === 'done') {
return node
}
const steadyBlocked = node.status === 'blocked' && Math.random() < 0.72
const criticalPause = node.riskLevel === 'critical' && Math.random() < 0.45
if (steadyBlocked || criticalPause) {
return node
}
const baseRate =
node.supplyType === 'self_made'
? 0.055
: node.supplyType === 'outsourced'
? 0.036
: 0.028
const momentum = 0.65 + Math.random() * 0.7 + orderIndex * 0.035
const increment = Math.max(1, Math.round(node.requiredQty * baseRate * momentum))
const completedQty = Math.min(node.requiredQty, node.completedQty + increment)
const progress = clamp((completedQty / node.requiredQty) * 100, 0, 100)
const defectBump =
node.riskLevel === 'critical'
? Math.random() < 0.28
: node.riskLevel === 'warning'
? Math.random() < 0.14
: Math.random() < 0.04
const delayDays =
progress < 100 && node.riskLevel !== 'normal' && Math.random() < 0.12
? Math.min(node.delayDays + 1, 9)
: node.delayDays
return {
...node,
completedQty,
defectQty: Math.min(completedQty, node.defectQty + (defectBump ? Math.max(1, Math.round(increment * 0.018)) : 0)),
progress,
delayDays,
actualStart: node.actualStart || node.plannedStart,
actualEnd: completedQty >= node.requiredQty ? formatDateOffset(0) : '',
}
}
function deriveLeafStatus(
node: ProductionNode,
progress: number,
): ProductionStatus {
if (progress >= 100) {
return 'done'
}
if (node.status === 'blocked') {
return 'blocked'
}
if (node.delayDays > 0 || node.riskLevel === 'critical') {
return 'delayed'
}
if (progress > 0) {
return 'in_progress'
}
return 'waiting'
}
function deriveLeafRisk(
node: ProductionNode,
progress: number,
status: ProductionStatus,
): RiskLevel {
if (status === 'blocked' || node.delayDays >= 3) {
return 'critical'
}
if (status === 'delayed' || node.delayDays > 0 || progress < 35) {
return 'warning'
}
return 'normal'
}
function deriveParentStatus(
children: ProductionNode[],
progress: number,
): ProductionStatus {
if (children.some((child) => child.status === 'blocked')) {
return 'blocked'
}
if (children.some((child) => child.status === 'delayed')) {
return 'delayed'
}
if (progress >= 100) {
return 'done'
}
if (progress > 0) {
return 'in_progress'
}
return 'waiting'
}
function deriveParentRisk(
children: ProductionNode[],
delayDays: number,
status: ProductionStatus,
): RiskLevel {
if (
delayDays >= 3 ||
status === 'blocked' ||
children.some((child) => child.riskLevel === 'critical')
) {
return 'critical'
}
if (
delayDays > 0 ||
status === 'delayed' ||
children.some((child) => child.riskLevel === 'warning')
) {
return 'warning'
}
return 'normal'
}
function buildRiskEvents(order: OrderSummary, nodes: ProductionNode[]): RiskEvent[] {
return nodes
.filter((node) => node.riskLevel !== 'normal' || node.delayDays > 0)
.sort((a, b) => {
const riskWeight = riskRank(b.riskLevel) - riskRank(a.riskLevel)
return riskWeight || b.delayDays - a.delayDays
})
.slice(0, 8)
.map((node, index) => ({
id: `${order.id}-${node.id}-${index}`,
orderId: order.id,
nodeId: node.id,
orderCode: order.code,
nodeName: node.name,
riskLevel: node.riskLevel,
message:
node.delayReason ||
(node.delayDays > 0
? `计划偏差 ${node.delayDays} 天,需要复核节拍`
: '进度低于当前节拍预测'),
time: formatTime(new Date(Date.now() - index * 1000 * 64)),
}))
}
function riskRank(riskLevel: RiskLevel) {
if (riskLevel === 'critical') {
return 3
}
if (riskLevel === 'warning') {
return 2
}
return 1
}
function formatDateOffset(offset: number) {
const date = new Date()
date.setDate(date.getDate() + offset)
return date.toISOString().slice(0, 10)
}