using DongfangHydro.Dashboard.Api.Domain; namespace DongfangHydro.Dashboard.Api.Services; public sealed class ProgressAggregationService { public void Recalculate(ProductionOrder order, IReadOnlyCollection nodes) { var childrenByParent = nodes .Where(node => node.ParentNodeId.HasValue) .GroupBy(node => node.ParentNodeId!.Value) .ToDictionary(group => group.Key, group => group.ToList()); var roots = nodes.Where(node => node.ParentNodeId is null).ToList(); foreach (var rootNode in roots) { RecalculateNode(rootNode, childrenByParent); } var root = roots.FirstOrDefault(node => node.NodeType == "order") ?? roots.FirstOrDefault(); if (root is null) { return; } order.RequiredQty = root.RequiredQty; order.CompletedQty = root.CompletedQty; order.Progress = root.Progress; order.Status = root.Status; order.RiskLevel = root.RiskLevel; order.DelayDays = root.DelayDays; } private static void RecalculateNode( ProductionNode node, IReadOnlyDictionary> childrenByParent) { if (!childrenByParent.TryGetValue(node.Id, out var children) || children.Count == 0) { node.Progress = Percentage(node.CompletedQty, node.RequiredQty); if (node.Progress >= 100) { node.Status = "done"; node.RiskLevel = "normal"; node.DelayDays = 0; node.DelayReason = string.Empty; } return; } foreach (var child in children) { RecalculateNode(child, childrenByParent); } var childRequiredQty = children.Sum(child => child.RequiredQty); var weightedProgress = childRequiredQty == 0 ? 0 : children.Sum(child => child.Progress * child.RequiredQty) / (double)childRequiredQty; node.RequiredQty = node.RequiredQty > 0 ? node.RequiredQty : childRequiredQty; node.CompletedQty = (int)Math.Round( node.RequiredQty * weightedProgress / 100d, MidpointRounding.AwayFromZero); node.DefectQty = children.Sum(child => child.DefectQty); node.Progress = Math.Clamp( (int)Math.Round(weightedProgress, MidpointRounding.AwayFromZero), 0, 100); node.DelayDays = children.Max(child => child.DelayDays); node.RiskLevel = AggregateRisk(children); node.Status = AggregateStatus(children); } private static int Percentage(int completed, int required) { return required <= 0 ? 0 : Math.Clamp((int)Math.Round(completed * 100d / required, MidpointRounding.AwayFromZero), 0, 100); } private static string AggregateRisk(IEnumerable children) { var childList = children.ToList(); if (childList.Any(child => child.RiskLevel == "critical" || child.Status == "blocked" || child.DelayDays >= 3)) { return "critical"; } return childList.Any(child => child.RiskLevel == "warning" || child.Status == "delayed" || child.DelayDays > 0) ? "warning" : "normal"; } private static string AggregateStatus(IReadOnlyCollection children) { if (children.Any(child => child.Status == "blocked")) { return "blocked"; } if (children.Any(child => child.Status == "delayed" || child.DelayDays > 0)) { return "delayed"; } if (children.All(child => child.Status == "done")) { return "done"; } return children.Any(child => child.Status == "in_progress" || child.CompletedQty > 0) ? "in_progress" : "waiting"; } }