< Summary

Information
Class: Chronicis.Client.ViewModels.DashboardViewModel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/DashboardViewModel.cs
Line coverage
100%
Covered lines: 46
Uncovered lines: 0
Coverable lines: 46
Total lines: 245
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_IsLoading()100%11100%
set_IsLoading(...)100%11100%
get_Dashboard()100%11100%
set_Dashboard(...)100%11100%
get_OrderedWorlds()100%11100%
set_OrderedWorlds(...)100%11100%
get_Quote()100%11100%
set_Quote(...)100%11100%
HandlePromptClick(...)100%22100%
GetCategoryClass(...)100%44100%
OnTreeStateChanged()100%11100%
Dispose()100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/DashboardViewModel.cs

#LineLine coverage
 1using Chronicis.Client.Abstractions;
 2using Chronicis.Client.Components.Dialogs;
 3using Chronicis.Client.Services;
 4using Chronicis.Shared.DTOs;
 5using Chronicis.Shared.Extensions;
 6using MudBlazor;
 7
 8namespace Chronicis.Client.ViewModels;
 9
 10/// <summary>
 11/// ViewModel for the Dashboard page.
 12/// Coordinates data loading, world ordering, and user actions.
 13/// Implements <see cref="IDisposable"/> to clean up the tree state subscription.
 14/// </summary>
 15public sealed class DashboardViewModel : ViewModelBase, IDisposable
 16{
 17    private readonly IDashboardApiService _dashboardApi;
 18    private readonly IUserApiService _userApi;
 19    private readonly IWorldApiService _worldApi;
 20    private readonly IArticleApiService _articleApi;
 21    private readonly IQuoteService _quoteService;
 22    private readonly ITreeStateService _treeState;
 23    private readonly IDialogService _dialogService;
 24    private readonly IAppNavigator _navigator;
 25    private readonly IUserNotifier _notifier;
 26    private readonly ILogger<DashboardViewModel> _logger;
 27
 2928    private bool _isLoading = true;
 29    private DashboardDto? _dashboard;
 2930    private List<DashboardWorldDto> _orderedWorlds = new();
 31    private Quote? _quote;
 32
 2933    public DashboardViewModel(
 2934        IDashboardApiService dashboardApi,
 2935        IUserApiService userApi,
 2936        IWorldApiService worldApi,
 2937        IArticleApiService articleApi,
 2938        IQuoteService quoteService,
 2939        ITreeStateService treeState,
 2940        IDialogService dialogService,
 2941        IAppNavigator navigator,
 2942        IUserNotifier notifier,
 2943        ILogger<DashboardViewModel> logger)
 44    {
 2945        _dashboardApi = dashboardApi;
 2946        _userApi = userApi;
 2947        _worldApi = worldApi;
 2948        _articleApi = articleApi;
 2949        _quoteService = quoteService;
 2950        _treeState = treeState;
 2951        _dialogService = dialogService;
 2952        _navigator = navigator;
 2953        _notifier = notifier;
 2954        _logger = logger;
 55
 2956        _treeState.OnStateChanged += OnTreeStateChanged;
 2957    }
 58
 59    /// <summary>Whether the initial data load is in progress.</summary>
 60    public bool IsLoading
 61    {
 862        get => _isLoading;
 2663        private set => SetField(ref _isLoading, value);
 64    }
 65
 66    /// <summary>The loaded dashboard data, or <c>null</c> if loading failed.</summary>
 67    public DashboardDto? Dashboard
 68    {
 2869        get => _dashboard;
 1270        private set => SetField(ref _dashboard, value);
 71    }
 72
 73    /// <summary>Worlds ordered by activity for display.</summary>
 74    public List<DashboardWorldDto> OrderedWorlds
 75    {
 676        get => _orderedWorlds;
 1077        private set => SetField(ref _orderedWorlds, value);
 78    }
 79
 80    /// <summary>The motivational quote shown in the hero section.</summary>
 81    public Quote? Quote
 82    {
 883        get => _quote;
 884        private set => SetField(ref _quote, value);
 85    }
 86
 87    /// <summary>
 88    /// Runs onboarding redirect check, subscribes to tree state, and loads dashboard + quote in parallel.
 89    /// </summary>
 90    public async Task InitializeAsync()
 91    {
 92        var profile = await _userApi.GetUserProfileAsync();
 93        if (profile != null && !profile.HasCompletedOnboarding)
 94        {
 95            _navigator.NavigateTo("/getting-started", replace: true);
 96            return;
 97        }
 98
 99        await Task.WhenAll(LoadDashboardAsync(), LoadQuoteAsync());
 100    }
 101
 102    /// <summary>Loads (or reloads) the dashboard data and updates <see cref="OrderedWorlds"/>.</summary>
 103    public async Task LoadDashboardAsync()
 104    {
 105        IsLoading = true;
 106
 107        try
 108        {
 109            var data = await _dashboardApi.GetDashboardAsync();
 110            Dashboard = data;
 111
 112            if (data != null)
 113            {
 114                OrderedWorlds = data.Worlds
 115                    .OrderByDescending(w => w.Campaigns.Any(c => c.IsActive))
 116                    .ThenByDescending(w => w.Campaigns
 117                        .Where(c => c.CurrentArc?.LatestSessionDate != null)
 118                        .Select(c => c.CurrentArc!.LatestSessionDate)
 119                        .DefaultIfEmpty(DateTime.MinValue)
 120                        .Max())
 121                    .ThenByDescending(w => w.CreatedAt)
 122                    .ToList();
 123            }
 124        }
 125        catch (Exception ex)
 126        {
 127            _logger.LogErrorSanitized(ex, "Error loading dashboard");
 128            _notifier.Error("Failed to load dashboard");
 129        }
 130        finally
 131        {
 132            IsLoading = false;
 133        }
 134    }
 135
 136    /// <summary>Loads the motivational quote. Failures are swallowed silently.</summary>
 137    public async Task LoadQuoteAsync()
 138    {
 139        try
 140        {
 141            Quote = await _quoteService.GetRandomQuoteAsync();
 142        }
 143        catch (Exception ex)
 144        {
 145            _logger.LogErrorSanitized(ex, "Error loading quote");
 146        }
 147    }
 148
 149    /// <summary>Creates a new world and navigates to it.</summary>
 150    public async Task CreateNewWorldAsync()
 151    {
 152        try
 153        {
 154            var createDto = new WorldCreateDto
 155            {
 156                Name = "New World",
 157                Description = "A new world for your adventures"
 158            };
 159
 160            var world = await _worldApi.CreateWorldAsync(createDto);
 161
 162            if (world != null)
 163            {
 164                _notifier.Success($"World '{world.Name}' created!");
 165                await _treeState.RefreshAsync();
 166
 167                if (world.WorldRootArticleId.HasValue)
 168                {
 169                    _treeState.ShouldFocusTitle = true;
 170                    _navigator.NavigateTo($"/world/{world.Slug}");
 171                }
 172                else
 173                {
 174                    await LoadDashboardAsync();
 175                }
 176            }
 177            else
 178            {
 179                _notifier.Error("Failed to create world");
 180            }
 181        }
 182        catch (Exception ex)
 183        {
 184            _logger.LogErrorSanitized(ex, "Error creating world");
 185            _notifier.Error($"Error: {ex.Message}");
 186        }
 187    }
 188
 189    /// <summary>Opens the join-world dialog and handles the result.</summary>
 190    public async Task JoinWorldAsync()
 191    {
 192        var dialog = await _dialogService.ShowAsync<JoinWorldDialog>("Join a World");
 193        var result = await dialog.Result;
 194
 195        if (result != null && !result.Canceled && result.Data is WorldJoinResultDto joinResult)
 196        {
 197            _notifier.Success($"Welcome to {joinResult.WorldName}!");
 198            await _treeState.RefreshAsync();
 199            await LoadDashboardAsync();
 200
 201            if (joinResult.WorldId.HasValue)
 202            {
 203                _navigator.NavigateTo($"/world/{joinResult.WorldId}");
 204            }
 205        }
 206    }
 207
 208    /// <summary>Resolves a character's article path and navigates to it.</summary>
 209    public async Task NavigateToCharacterAsync(Guid characterId)
 210    {
 211        var article = await _articleApi.GetArticleDetailAsync(characterId);
 212        if (article != null && article.Breadcrumbs.Any())
 213        {
 214            var path = string.Join("/", article.Breadcrumbs.Select(b => b.Slug));
 215            _navigator.NavigateTo($"/article/{path}");
 216        }
 217    }
 218
 219    /// <summary>Handles a dashboard prompt click, navigating if the prompt has an action URL.</summary>
 220    public void HandlePromptClick(PromptDto prompt)
 221    {
 3222        if (!string.IsNullOrEmpty(prompt.ActionUrl))
 223        {
 1224            _navigator.NavigateTo(prompt.ActionUrl);
 225        }
 3226    }
 227
 228    /// <summary>Returns the CSS class for a prompt category.</summary>
 229    public static string GetCategoryClass(PromptCategory category) =>
 6230        category switch
 6231        {
 2232            PromptCategory.MissingFundamental => "missing-fundamental",
 1233            PromptCategory.NeedsAttention => "needs-attention",
 2234            PromptCategory.Suggestion => "suggestion",
 1235            _ => string.Empty
 6236        };
 237
 1238    private void OnTreeStateChanged() => RaisePropertyChanged(nameof(OrderedWorlds));
 239
 240    /// <inheritdoc />
 241    public void Dispose()
 242    {
 6243        _treeState.OnStateChanged -= OnTreeStateChanged;
 6244    }
 245}