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 = { 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([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) }