< 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: 251
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
 3028    private bool _isLoading = true;
 29    private DashboardDto? _dashboard;
 3030    private List<DashboardWorldDto> _orderedWorlds = new();
 31    private Quote? _quote;
 32
 3033    public DashboardViewModel(
 3034        IDashboardApiService dashboardApi,
 3035        IUserApiService userApi,
 3036        IWorldApiService worldApi,
 3037        IArticleApiService articleApi,
 3038        IQuoteService quoteService,
 3039        ITreeStateService treeState,
 3040        IDialogService dialogService,
 3041        IAppNavigator navigator,
 3042        IUserNotifier notifier,
 3043        ILogger<DashboardViewModel> logger)
 44    {
 3045        _dashboardApi = dashboardApi;
 3046        _userApi = userApi;
 3047        _worldApi = worldApi;
 3048        _articleApi = articleApi;
 3049        _quoteService = quoteService;
 3050        _treeState = treeState;
 3051        _dialogService = dialogService;
 3052        _navigator = navigator;
 3053        _notifier = notifier;
 3054        _logger = logger;
 55
 3056        _treeState.OnStateChanged += OnTreeStateChanged;
 3057    }
 58
 59    /// <summary>Whether the initial data load is in progress.</summary>
 60    public bool IsLoading
 61    {
 862        get => _isLoading;
 2863        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;
 1370        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;
 1177        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                    await _navigator.GoToWorldAsync(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                var world = _orderedWorlds.FirstOrDefault(w => w.Id == joinResult.WorldId.Value);
 204                if (world != null)
 205                    await _navigator.GoToWorldAsync(world.Slug);
 206            }
 207        }
 208    }
 209
 210    /// <summary>Resolves a character's article path and navigates to it.</summary>
 211    public async Task NavigateToCharacterAsync(Guid characterId)
 212    {
 213        var article = await _articleApi.GetArticleDetailAsync(characterId);
 214        if (article != null && article.Breadcrumbs.Any())
 215        {
 216            var worldSlug = article.Breadcrumbs[0].Slug;
 217            var articleSlugs = article.Breadcrumbs.Skip(1).Select(b => b.Slug).ToList();
 218            if (articleSlugs.Count > 0)
 219                await _navigator.GoToWikiArticleAsync(worldSlug, articleSlugs);
 220            else
 221                await _navigator.GoToWorldAsync(worldSlug);
 222        }
 223    }
 224
 225    /// <summary>Handles a dashboard prompt click, navigating if the prompt has an action URL.</summary>
 226    public void HandlePromptClick(PromptDto prompt)
 227    {
 3228        if (!string.IsNullOrEmpty(prompt.ActionUrl))
 229        {
 1230            _navigator.NavigateTo(prompt.ActionUrl);
 231        }
 3232    }
 233
 234    /// <summary>Returns the CSS class for a prompt category.</summary>
 235    public static string GetCategoryClass(PromptCategory category) =>
 6236        category switch
 6237        {
 2238            PromptCategory.MissingFundamental => "missing-fundamental",
 1239            PromptCategory.NeedsAttention => "needs-attention",
 2240            PromptCategory.Suggestion => "suggestion",
 1241            _ => string.Empty
 6242        };
 243
 1244    private void OnTreeStateChanged() => RaisePropertyChanged(nameof(OrderedWorlds));
 245
 246    /// <inheritdoc />
 247    public void Dispose()
 248    {
 6249        _treeState.OnStateChanged -= OnTreeStateChanged;
 6250    }
 251}