namespace MMBLL.Indent { // CPM priority engine for multi-level work order generation. // Takes a flat list of WO nodes with BomLevel and Duration, runs forward/backward // pass, computes slack, and returns priority ranks (1 = critical path, highest priority). public static class IndentDependencyGraph { public sealed class Node { public int ItemId { get; set; } public int BomLevel { get; set; } public decimal Duration { get; set; } // sum of routing stage durations (days) public decimal DueDate { get; set; } // days from today (0 = root due date) // Computed by CPM public decimal EarliestStart { get; internal set; } public decimal EarliestFinish { get; internal set; } public decimal LatestStart { get; internal set; } public decimal LatestFinish { get; internal set; } public decimal Slack { get; internal set; } public short Priority { get; internal set; } // 1 = critical path } // Assigns priorities to nodes using CPM forward+backward pass. // Nodes must be ordered by BomLevel ascending (root=0 first). // rootDueDateDays: how many days from today the root is due. public static void AssignPriorities(IList nodes, decimal rootDueDateDays) { if (nodes.Count == 0) return; // Group by BomLevel — each level depends on the next deeper level completing first var maxLevel = nodes.Max(n => n.BomLevel); // Forward pass: EarliestStart = max(EarliestFinish of all predecessors at deeper level) // Level 0 (root) cannot start until all level-1 predecessors finish, etc. // For simplicity: level-N item EarliestStart = sum of durations below it (bottom-up) // Backward pass starts from root due date // LatestFinish[root] = rootDueDateDays // LatestStart[root] = LatestFinish[root] - Duration[root] // LatestFinish[level-N] = min(LatestStart of all items that depend on level-N) // Simplified CPM: treat each BOM level as a stage // All nodes at the same level have the same stage timings var stageEF = new decimal[maxLevel + 2]; // EarliestFinish per stage var stageLF = new decimal[maxLevel + 2]; // LatestFinish per stage // Forward pass (bottom-up: deepest level first). // When a level has no nodes, carry the child EF forward so gaps in sparse BOMs // do not reset timing to zero for ancestor levels. for (int level = maxLevel; level >= 0; level--) { var childEF = level < maxLevel ? stageEF[level + 1] : 0; var levelNodes = nodes.Where(n => n.BomLevel == level).ToList(); if (levelNodes.Count == 0) { stageEF[level] = childEF; // propagate child EF through empty level continue; } var maxDuration = levelNodes.Max(n => n.Duration); stageEF[level] = childEF + maxDuration; } // Backward pass (top-down: root first) stageLF[0] = rootDueDateDays; for (int level = 1; level <= maxLevel; level++) { var parentNodes = nodes.Where(n => n.BomLevel == level - 1).ToList(); var parentMaxDur = parentNodes.Count > 0 ? parentNodes.Max(n => n.Duration) : 0; stageLF[level] = stageLF[level - 1] - parentMaxDur; } // Assign EF, LF, Slack, Priority per node foreach (var node in nodes) { var level = node.BomLevel; var childEF = level < maxLevel ? stageEF[level + 1] : 0; node.EarliestStart = childEF; node.EarliestFinish = childEF + node.Duration; node.LatestFinish = stageLF[level]; node.LatestStart = stageLF[level] - node.Duration; node.Slack = Math.Max(0, node.LatestStart - node.EarliestStart); } // Rank by slack ascending (0 slack = critical path = priority 1) var ranked = nodes.OrderBy(n => n.Slack).ThenByDescending(n => n.BomLevel).ToList(); for (short i = 0; i < ranked.Count; i++) ranked[i].Priority = (short)(i + 1); } } }