@* The bill's composition (brief §7.1 item 5, D-42): the disjoint cost categories, Uncategorized and the standing charges no category holds — rows that add up to the bill, manual costs in them exactly once. A donut only when every slice is ≥ 0 (DonutAllowed); a credit larger than its charges is drawn as signed bars around zero instead. Categories that overlap another (a view on the bill) are listed apart, as views, and never added up. Setting up categories is never required: without any, the whole bill is Uncategorized, and a hint says how to split it. *@

@S.Overview_Composition

@if (_rows.Count == 0) { @S.Overview_CompositionEmpty } else { @if (_donut.Count > 1) { } else if (_signed) { @S.Overview_SignedBarsNote } @S.Dashboard_ColCategory @if (_signed) { @S.Overview_SignedBars } @S.AnalysisTable_Cost @if (!_signed) { @S.Overview_Share } @if (row.Href is not null) { @row.Name } else { @row.Name } @if (_signed) { @if (!row.IsTotal) { } } @if (row.Status.IsKnown) { @Format.Money(row.Amount, View.Currency) @if (!row.Status.IsComplete) { @row.Chip } } else { @row.Status.Status } @if (!_signed) { @(row.Share is { } share ? Format.Number(share * 100, 1) + " %" : row.IsTotal ? string.Empty : Format.Unknown) } } @if (_views.Count > 0) {

@S.Overview_Views

@S.Overview_ViewsNote
@foreach (var view in _views) { @view.Name @if (view.Overlaps is not null) {
@view.Overlaps
} @(view.Status.IsKnown ? Format.Money(view.Amount, View.Currency) : view.Status.Status) }
} @if (View.Data.Setup?.FirstGap is CostSetupGap.NoCategories or CostSetupGap.NoMembers) { @(View.Data.Setup.FirstGap == CostSetupGap.NoCategories ? S.Dashboard_SetupNoCategories : S.Dashboard_SetupNoMembers) @S.Nav_CostCategories }
@code { /// The committed Overview. [Parameter, EditorRequired] public OverviewView View { get; set; } = null!; private sealed record Row(string Name, string? Href, string? Color, double? Amount, FigureStatus Status, double? Share) { /// The bill's total, the last line. public bool IsTotal { get; init; } /// What qualifies a known amount: the quantities' state for a priced one, else its price coverage. public string Chip { get; init; } = Status.Status; /// The slice's place among the donut's slices, for its palette colour. public int? DonutIndex { get; set; } } private sealed record ViewRow(string Name, string Href, double? Amount, FigureStatus Status, string? Overlaps); private CategoryComposition? _composition; private List _rows = []; private List _views = []; private IReadOnlyList _donut = []; private bool _signed; private double _maxAbs; private object? _builtFrom; protected override void OnParametersSet() { // Built once per committed value: the donut re-keys only for a new list. if (ReferenceEquals(_builtFrom, View)) { return; } _builtFrom = View; _composition = View.Data.Cost.Composition; _rows = []; _views = []; _donut = []; if (_composition is not { } composition) { return; } var data = View.Data; _signed = !composition.DonutAllowed; var positive = composition.Slices.Sum(s => s.Total.Cost is { } c && c > 0 ? c : 0); foreach (var slice in composition.Slices) { var status = FigureText.Of(slice.Total); var category = slice.CategoryId is { } id ? composition.Categories.FirstOrDefault(c => c.CategoryId == id) : null; // A slice with nothing in it (a category without members) is not a row; one that could not be priced is, and // so is a category whose members price nothing (calculated views, generation, A-22): it says so. if (!status.IsKnown && slice.Total.Status == CostStatus.Priced) { if (category?.Cover is not { CoverMeterIds.Count: 0, AnalysisOnlyMeterIds.Count: > 0 }) { continue; } status = new FigureStatus(false, false, true, S.Overview_CategoryPricesNothing, S.Overview_CategoryPricesNothingDetail, string.Empty); } var href = slice.CategoryId is { } categoryId ? AnalysisLinks.Analysis(QueryScope.ForCategory(categoryId), AnalysisMetric.Cost, View.Query) : null; var amount = status.IsKnown ? slice.Total.Cost : null; double? share = !_signed && amount is { } a && positive > CategoryComposition.DonutTolerance ? a / positive : null; _rows.Add(new Row(OverviewText.SliceName(slice, composition, data.EnergyTypes, data.MeterNames), href, OverviewDonutSlice.SafeColor(category?.ColorHex), amount, status, share) { Chip = OverviewText.CostQualifier(slice.Total, status), }); } _maxAbs = _rows.Select(r => Math.Abs(r.Amount ?? 0)).DefaultIfEmpty(0).Max(); if (!_signed) { var inDonut = _rows.Where(r => r.Amount is > CategoryComposition.DonutTolerance).ToList(); for (var i = 0; i < inDonut.Count; i++) { inDonut[i].DonutIndex = i; } _donut = [.. inDonut.Select(r => new OverviewDonutSlice(r.Name, r.Amount!.Value, r.Color))]; } if (_rows.Count > 0) { var totalStatus = FigureText.Of(composition.Total); _rows.Add(new Row(S.Overview_BillTotal, null, null, totalStatus.IsKnown ? composition.Total.Cost : null, totalStatus, null) { IsTotal = true, Chip = OverviewText.CostQualifier(composition.Total, totalStatus), }); } var names = composition.Categories.ToDictionary(c => c.CategoryId, c => c.Name); foreach (var category in composition.Categories.Where(c => c.IsOverlappingView)) { var status = FigureText.Of(category.Total); var others = category.OverlapsWith.Select(o => names.GetValueOrDefault(o)).OfType().ToList(); _views.Add(new ViewRow( category.Name, AnalysisLinks.Analysis(QueryScope.ForCategory(category.CategoryId), AnalysisMetric.Cost, View.Query), status.IsKnown ? category.Total.Cost : null, status, others.Count > 0 ? Loc.F(S.Overview_ViewOverlaps, string.Join(", ", others)) : category.Cover.LiesOutsideBill ? S.Overview_ViewOutsideBill : null)); } } /// The swatch of a donut slice: its category's colour, else the palette hue the donut gives it (same order). private string SwatchStyle(Row row) => _donut.Count <= 1 || row.DonutIndex is not { } index ? "visibility:hidden" : "background:" + (row.Color ?? OverviewDonutSlice.PaletteVariable(index)); /// The width of a signed bar in its half: the share of the largest amount either way. private string BarStyle(double? amount, bool negative) { if (amount is not { } value || _maxAbs <= 0 || (negative ? value >= 0 : value <= 0)) { return "width:0"; } var percent = Math.Abs(value) / _maxAbs * 100; return string.Create(System.Globalization.CultureInfo.InvariantCulture, $"width:{percent:0.#}%"); } }