using GB5Shared.DTO.Report; namespace GB5Shared.Export { /// /// Recursive multi-level group tree builder, extracted from ReportExport.cs's PDF/HTML render path /// so any other export format (e.g. Excel) can reuse the exact same grouping structure instead of /// re-implementing it. Node shape is deliberately untyped (Dictionary<string,object>) to match /// how PDF/HTML's Handlebars "renderGroupNodes" helper already consumes it via IDictionary/reflection /// — kept unchanged here so extraction is behavior-neutral for that path. /// public static class GroupTreeBuilder { /// /// Builds one nesting level per entry in (ordered by /// DisplaySlNo), starting at (callers pass 0). Each node is: /// LevelTitle / LevelValue — this node's group column title + the value all its rows share /// IsLeaf — true at the innermost (last) level /// Children — child nodes (non-leaf levels only) — one per distinct value at the NEXT level /// Rows — the detail rows for this bucket (leaf level only) /// SubTotal — whatever returns for this node's rows /// (null when the caller has nothing to subtotal), so a subtotal can render when /// closing any level, not only the leaf. /// A single group column produces a tree exactly one level deep. /// public static List BuildGroupNodes( IEnumerable> rows, List groupColumns, int levelIndex, Func>, Dictionary?> computeSubTotal) { var levelField = groupColumns[levelIndex]; string levelTitle = levelField.ReportViewFieldsFieldTitle ?? ""; bool isLeafLevel = levelIndex == groupColumns.Count - 1; return rows .GroupBy(r => r.ContainsKey(levelTitle) ? r[levelTitle]?.ToString() ?? "" : "") .Select(g => { var groupRows = g.ToList(); var node = new Dictionary { ["LevelTitle"] = levelTitle, ["LevelValue"] = g.Key, ["IsLeaf"] = isLeafLevel, ["SubTotal"] = (object?)computeSubTotal(groupRows)! }; if (isLeafLevel) node["Rows"] = groupRows.Cast().ToList(); else node["Children"] = BuildGroupNodes(groupRows, groupColumns, levelIndex + 1, computeSubTotal); return (object)node; }) .ToList(); } } }