| | | 1 | | using Microsoft.AspNetCore.Components; |
| | | 2 | | using Microsoft.JSInterop; |
| | | 3 | | using Pomodoro.Web.Services; |
| | | 4 | | using Pomodoro.Web.Services.Formatters; |
| | | 5 | | |
| | | 6 | | namespace Pomodoro.Web.Components.History; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Displays a doughnut chart showing time distribution across tasks and breaks |
| | | 10 | | /// </summary> |
| | | 11 | | public partial class TimeDistributionChart : IDisposable |
| | | 12 | | { |
| | 325 | 13 | | [Inject] private IJSRuntime JS { get; set; } = default!; |
| | 525 | 14 | | [Inject] private IActivityService ActivityService { get; set; } = default!; |
| | 418 | 15 | | [Inject] private ILogger<TimeDistributionChart> Logger { get; set; } = default!; |
| | 237 | 16 | | [Inject] private TimeFormatter TimeFormatter { get; set; } = default!; |
| | | 17 | | |
| | | 18 | | [Parameter] |
| | 336 | 19 | | public DateTime SelectedDate { get; set; } |
| | | 20 | | |
| | | 21 | | /// <summary> |
| | | 22 | | /// Static canvas ID for the chart (single instance) |
| | | 23 | | /// </summary> |
| | 2 | 24 | | private static readonly string CanvasId = Constants.Charts.TimeDistributionCanvasId; |
| | | 25 | | |
| | | 26 | | private DateTime _lastRenderedDate; |
| | | 27 | | |
| | | 28 | | /// <summary> |
| | | 29 | | /// Total minutes displayed in the chart |
| | | 30 | | /// </summary> |
| | 53 | 31 | | public int TotalMinutes { get; private set; } |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// Formatted total time for display (safe from null reference) |
| | | 35 | | /// </summary> |
| | | 36 | | public string FormattedTotalMinutes |
| | | 37 | | { |
| | | 38 | | get |
| | 16 | 39 | | { |
| | | 40 | | try |
| | 16 | 41 | | { |
| | 16 | 42 | | return TimeFormatter?.FormatTime(TotalMinutes) ?? TotalMinutes.ToString(); |
| | | 43 | | } |
| | 2 | 44 | | catch (Exception ex) |
| | 2 | 45 | | { |
| | 2 | 46 | | Logger?.LogError(ex, "Error formatting total minutes in TimeDistributionChart"); |
| | 2 | 47 | | return TotalMinutes.ToString(); |
| | | 48 | | } |
| | 16 | 49 | | } |
| | | 50 | | } |
| | | 51 | | |
| | | 52 | | /// <summary> |
| | | 53 | | /// Whether there is data to display |
| | | 54 | | /// </summary> |
| | 146 | 55 | | public bool HasData { get; private set; } |
| | | 56 | | |
| | | 57 | | private bool _isRendered; |
| | | 58 | | private bool _isDisposed; |
| | | 59 | | |
| | | 60 | | protected override void OnInitialized() |
| | 103 | 61 | | { |
| | 103 | 62 | | ActivityService.OnActivityChanged += OnActivityChanged; |
| | 103 | 63 | | } |
| | | 64 | | |
| | | 65 | | protected override async Task OnAfterRenderAsync(bool firstRender) |
| | 126 | 66 | | { |
| | 126 | 67 | | if (firstRender) |
| | 103 | 68 | | { |
| | 103 | 69 | | _isRendered = true; |
| | 103 | 70 | | await UpdateChartAsync(); |
| | 103 | 71 | | } |
| | 126 | 72 | | } |
| | | 73 | | |
| | | 74 | | protected override async Task OnParametersSetAsync() |
| | 106 | 75 | | { |
| | | 76 | | // Only update if the date has changed and we've already rendered |
| | 106 | 77 | | if (_isRendered && _lastRenderedDate != SelectedDate.Date) |
| | 3 | 78 | | { |
| | 3 | 79 | | await UpdateChartAsync(); |
| | 3 | 80 | | } |
| | 106 | 81 | | } |
| | | 82 | | |
| | | 83 | | private void OnActivityChanged() |
| | 8 | 84 | | { |
| | 8 | 85 | | if (_isRendered && !_isDisposed) |
| | 7 | 86 | | { |
| | 7 | 87 | | SafeTaskRunner.RunAndForget(async () => |
| | 7 | 88 | | { |
| | 7 | 89 | | await InvokeAsync(async () => |
| | 7 | 90 | | { |
| | 7 | 91 | | await UpdateChartAsync(); |
| | 7 | 92 | | StateHasChanged(); |
| | 14 | 93 | | }); |
| | 14 | 94 | | }, Logger, "OnActivityChanged"); |
| | 7 | 95 | | } |
| | 8 | 96 | | } |
| | | 97 | | |
| | | 98 | | private async Task UpdateChartAsync() |
| | 114 | 99 | | { |
| | 115 | 100 | | if (_isDisposed) return; |
| | | 101 | | |
| | | 102 | | try |
| | 113 | 103 | | { |
| | 113 | 104 | | var distribution = ActivityService.GetTimeDistribution(SelectedDate); |
| | 113 | 105 | | _lastRenderedDate = SelectedDate.Date; |
| | | 106 | | |
| | 113 | 107 | | if (distribution.Count == 0) |
| | 4 | 108 | | { |
| | 4 | 109 | | TotalMinutes = 0; |
| | 4 | 110 | | HasData = false; |
| | | 111 | | // Destroy existing chart when no data |
| | 4 | 112 | | await JS.InvokeVoidAsync(Constants.ChartJsFunctions.DestroyChart, CanvasId); |
| | 3 | 113 | | StateHasChanged(); |
| | 3 | 114 | | return; |
| | | 115 | | } |
| | | 116 | | |
| | 14 | 117 | | var labels = distribution.Keys.ToList(); |
| | 14 | 118 | | var data = distribution.Values.ToList(); |
| | 14 | 119 | | TotalMinutes = data.Sum(); |
| | 14 | 120 | | HasData = true; |
| | | 121 | | |
| | 14 | 122 | | var centerText = TimeFormatter.FormatTime(TotalMinutes); |
| | | 123 | | |
| | 12 | 124 | | await JS.InvokeVoidAsync(Constants.ChartJsFunctions.CreateDoughnutChart, |
| | 12 | 125 | | CanvasId, |
| | 12 | 126 | | labels, |
| | 12 | 127 | | data, |
| | 12 | 128 | | centerText); |
| | | 129 | | |
| | 11 | 130 | | StateHasChanged(); |
| | 11 | 131 | | } |
| | 99 | 132 | | catch (Exception ex) |
| | 99 | 133 | | { |
| | 99 | 134 | | Logger.LogError(ex, Constants.Messages.ErrorUpdatingTimeDistributionChart); |
| | 99 | 135 | | } |
| | 114 | 136 | | } |
| | | 137 | | |
| | | 138 | | public void Dispose() |
| | 109 | 139 | | { |
| | 115 | 140 | | if (_isDisposed) return; |
| | 103 | 141 | | _isDisposed = true; |
| | | 142 | | |
| | 103 | 143 | | ActivityService.OnActivityChanged -= OnActivityChanged; |
| | | 144 | | |
| | 103 | 145 | | SafeTaskRunner.RunAndForget( |
| | 307 | 146 | | async () => { await JS.InvokeVoidAsync(Constants.ChartJsFunctions.DestroyChart, CanvasId); }, |
| | 103 | 147 | | Logger, |
| | 103 | 148 | | "DestroyChart"); |
| | 109 | 149 | | } |
| | | 150 | | } |