< Summary

Information
Class: Chronicis.Client.ViewModels.ArcDetailViewModel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/ArcDetailViewModel.cs
Line coverage
100%
Covered lines: 59
Uncovered lines: 0
Coverable lines: 59
Total lines: 344
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
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%
get_IsSaving()100%11100%
get_IsTogglingActive()100%11100%
get_HasUnsavedChanges()100%11100%
get_SummaryExpanded()100%11100%
get_Arc()100%11100%
get_Campaign()100%11100%
get_Sessions()100%11100%
get_Breadcrumbs()100%11100%
get_SelectedQuest()100%11100%
get_CurrentUserId()100%11100%
get_IsCurrentUserGM()100%11100%
get_IsCurrentUserWorldOwner()100%11100%
get_CanManageArcDetails()100%22100%
get_CanViewPrivateNotes()100%11100%
get_EditSortOrder()100%11100%
get_EditName()100%11100%
set_EditName(...)100%22100%
get_EditDescription()100%11100%
set_EditDescription(...)100%22100%
get_EditPrivateNotes()100%11100%
set_EditPrivateNotes(...)100%44100%
NavigateToSessionAsync(...)100%11100%
OnEditQuest(...)100%11100%
OnQuestUpdated(...)100%11100%

File(s)

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

#LineLine coverage
 1using Chronicis.Client.Abstractions;
 2using Chronicis.Client.Services;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.DTOs.Quests;
 5using Chronicis.Shared.DTOs.Sessions;
 6using Chronicis.Shared.Enums;
 7using Chronicis.Shared.Extensions;
 8using MudBlazor;
 9
 10namespace Chronicis.Client.ViewModels;
 11
 12/// <summary>
 13/// ViewModel for the ArcDetail page.
 14/// Manages loading, editing, session creation, and quest selection for a single arc.
 15/// </summary>
 16public sealed class ArcDetailViewModel : ViewModelBase
 17{
 18    private readonly IArcApiService _arcApi;
 19    private readonly ICampaignApiService _campaignApi;
 20    private readonly IWorldApiService _worldApi;
 21    private readonly ISessionApiService _sessionApi;
 22    private readonly IQuestApiService _questApi;
 23    private readonly IAuthService _authService;
 24    private readonly ITreeStateService _treeState;
 25    private readonly IBreadcrumbService _breadcrumbService;
 26    private readonly IAppNavigator _navigator;
 27    private readonly IUserNotifier _notifier;
 28    private readonly IPageTitleService _titleService;
 29    private readonly IConfirmationService _confirmation;
 30    private readonly ILogger<ArcDetailViewModel> _logger;
 31
 2832    private bool _isLoading = true;
 33    private bool _isSaving;
 34    private bool _isTogglingActive;
 35    private bool _hasUnsavedChanges;
 36    private bool _summaryExpanded;
 37    private ArcDto? _arc;
 38    private CampaignDto? _campaign;
 2839    private List<SessionTreeDto> _sessions = new();
 2840    private string _editName = string.Empty;
 2841    private string _editDescription = string.Empty;
 2842    private string _editPrivateNotes = string.Empty;
 43    private int _editSortOrder;
 2844    private List<BreadcrumbItem> _breadcrumbs = new();
 45    private QuestDto? _selectedQuest;
 46    private Guid _currentUserId;
 47    private bool _isCurrentUserGM;
 48    private bool _isCurrentUserWorldOwner;
 49
 2850    public ArcDetailViewModel(
 2851        IArcApiService arcApi,
 2852        ICampaignApiService campaignApi,
 2853        IWorldApiService worldApi,
 2854        ISessionApiService sessionApi,
 2855        IQuestApiService questApi,
 2856        IAuthService authService,
 2857        ITreeStateService treeState,
 2858        IBreadcrumbService breadcrumbService,
 2859        IAppNavigator navigator,
 2860        IUserNotifier notifier,
 2861        IPageTitleService titleService,
 2862        IConfirmationService confirmation,
 2863        ILogger<ArcDetailViewModel> logger)
 64    {
 2865        _arcApi = arcApi;
 2866        _campaignApi = campaignApi;
 2867        _worldApi = worldApi;
 2868        _sessionApi = sessionApi;
 2869        _questApi = questApi;
 2870        _authService = authService;
 2871        _treeState = treeState;
 2872        _breadcrumbService = breadcrumbService;
 2873        _navigator = navigator;
 2874        _notifier = notifier;
 2875        _titleService = titleService;
 2876        _confirmation = confirmation;
 2877        _logger = logger;
 2878    }
 79
 5380    public bool IsLoading { get => _isLoading; private set => SetField(ref _isLoading, value); }
 1481    public bool IsSaving { get => _isSaving; private set => SetField(ref _isSaving, value); }
 1282    public bool IsTogglingActive { get => _isTogglingActive; private set => SetField(ref _isTogglingActive, value); }
 9383    public bool HasUnsavedChanges { get => _hasUnsavedChanges; private set => SetField(ref _hasUnsavedChanges, value); }
 684    public bool SummaryExpanded { get => _summaryExpanded; set => SetField(ref _summaryExpanded, value); }
 4985    public ArcDto? Arc { get => _arc; private set => SetField(ref _arc, value); }
 3086    public CampaignDto? Campaign { get => _campaign; private set => SetField(ref _campaign, value); }
 3587    public List<SessionTreeDto> Sessions { get => _sessions; private set => SetField(ref _sessions, value); }
 2388    public List<BreadcrumbItem> Breadcrumbs { get => _breadcrumbs; private set => SetField(ref _breadcrumbs, value); }
 1289    public QuestDto? SelectedQuest { get => _selectedQuest; private set => SetField(ref _selectedQuest, value); }
 3690    public Guid CurrentUserId { get => _currentUserId; private set => SetField(ref _currentUserId, value); }
 9191    public bool IsCurrentUserGM { get => _isCurrentUserGM; private set => SetField(ref _isCurrentUserGM, value); }
 6492    public bool IsCurrentUserWorldOwner { get => _isCurrentUserWorldOwner; private set => SetField(ref _isCurrentUserWor
 3493    public bool CanManageArcDetails => IsCurrentUserGM || IsCurrentUserWorldOwner;
 694    public bool CanViewPrivateNotes => CanManageArcDetails;
 7395    public int EditSortOrder { get => _editSortOrder; set { if (SetField(ref _editSortOrder, value)) HasUnsavedChanges =
 96
 97    public string EditName
 98    {
 699        get => _editName;
 69100        set { if (SetField(ref _editName, value)) HasUnsavedChanges = true; }
 101    }
 102
 103    public string EditDescription
 104    {
 11105        get => _editDescription;
 61106        set { if (SetField(ref _editDescription, value)) HasUnsavedChanges = true; }
 107    }
 108
 109    public string EditPrivateNotes
 110    {
 2111        get => _editPrivateNotes;
 47112        set { if (SetField(ref _editPrivateNotes, value) && CanManageArcDetails) HasUnsavedChanges = true; }
 113    }
 114
 115    /// <summary>Loads the arc and all related data for the given <paramref name="arcId"/>.</summary>
 116    public async Task LoadAsync(Guid arcId)
 117    {
 118        IsLoading = true;
 119
 120        try
 121        {
 122            CurrentUserId = Guid.Empty;
 123            IsCurrentUserGM = false;
 124            IsCurrentUserWorldOwner = false;
 125
 126            var arc = await _arcApi.GetArcAsync(arcId);
 127            if (arc == null)
 128            {
 129                _navigator.NavigateTo("/dashboard", replace: true);
 130                return;
 131            }
 132
 133            Arc = arc;
 134            EditName = arc.Name;
 135            EditDescription = arc.Description ?? string.Empty;
 136            EditPrivateNotes = arc.PrivateNotes ?? string.Empty;
 137            EditSortOrder = arc.SortOrder;
 138            HasUnsavedChanges = false;
 139
 140            Sessions = await _sessionApi.GetSessionsByArcAsync(arcId);
 141
 142            var campaign = await _campaignApi.GetCampaignAsync(arc.CampaignId);
 143            Campaign = campaign;
 144
 145            WorldDetailDto? world = null;
 146            if (campaign != null)
 147                world = await _worldApi.GetWorldAsync(campaign.WorldId);
 148
 149            Breadcrumbs = (arc != null && campaign != null && world != null)
 150                ? _breadcrumbService.ForArc(arc, campaign, world)
 151                : new List<BreadcrumbItem>
 152                {
 153                    new("Dashboard", href: "/dashboard"),
 154                    new(arc?.Name ?? "Arc", href: null, disabled: true)
 155                };
 156
 157            await _titleService.SetTitleAsync(arc?.Name ?? "Arc");
 158            _treeState.ExpandPathToAndSelect(arcId);
 159
 160            // Resolve GM status
 161            var user = await _authService.GetCurrentUserAsync();
 162            if (user != null && world != null)
 163            {
 164                var worldDetail = await _worldApi.GetWorldAsync(world.Id);
 165                if (worldDetail?.Members != null)
 166                {
 167                    var member = worldDetail.Members.FirstOrDefault(m =>
 168                        m.Email.Equals(user.Email, StringComparison.OrdinalIgnoreCase));
 169
 170                    if (member != null)
 171                    {
 172                        CurrentUserId = member.UserId;
 173                        IsCurrentUserGM = member.Role == WorldRole.GM;
 174                        IsCurrentUserWorldOwner = member.UserId == world.OwnerId;
 175                    }
 176                }
 177            }
 178        }
 179        catch (Exception ex)
 180        {
 181            _logger.LogErrorSanitized(ex, "Error loading arc {ArcId}", arcId);
 182            _notifier.Error($"Failed to load arc: {ex.Message}");
 183        }
 184        finally
 185        {
 186            IsLoading = false;
 187        }
 188    }
 189
 190    /// <summary>Toggles the active state of the arc.</summary>
 191    public async Task OnActiveToggleAsync(bool isActive)
 192    {
 193        if (_arc == null || !CanManageArcDetails || IsTogglingActive)
 194            return;
 195
 196        IsTogglingActive = true;
 197
 198        try
 199        {
 200            if (isActive)
 201            {
 202                var success = await _arcApi.ActivateArcAsync(_arc.Id);
 203                if (success)
 204                {
 205                    _arc.IsActive = true;
 206                    RaisePropertyChanged(nameof(Arc));
 207                    _notifier.Success("Arc set as active");
 208                }
 209                else
 210                {
 211                    _notifier.Error("Failed to activate arc");
 212                }
 213            }
 214            else
 215            {
 216                _notifier.Info("To deactivate, set another arc as active");
 217            }
 218        }
 219        catch (Exception ex)
 220        {
 221            _logger.LogErrorSanitized(ex, "Error toggling arc active state");
 222            _notifier.Error($"Error: {ex.Message}");
 223        }
 224        finally
 225        {
 226            IsTogglingActive = false;
 227        }
 228    }
 229
 230    /// <summary>Persists name, description, and sort order changes to the API.</summary>
 231    public async Task SaveAsync()
 232    {
 233        if (_arc == null || !CanManageArcDetails || IsSaving)
 234            return;
 235
 236        IsSaving = true;
 237
 238        try
 239        {
 240            var updateDto = new ArcUpdateDto
 241            {
 242                Name = EditName.Trim(),
 243                Description = string.IsNullOrWhiteSpace(EditDescription) ? null : EditDescription.Trim(),
 244                PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes,
 245                SortOrder = EditSortOrder
 246            };
 247
 248            var updated = await _arcApi.UpdateArcAsync(_arc.Id, updateDto);
 249            if (updated != null)
 250            {
 251                _arc.Name = updated.Name;
 252                _arc.Description = updated.Description;
 253                _arc.PrivateNotes = updated.PrivateNotes;
 254                _arc.SortOrder = updated.SortOrder;
 255                HasUnsavedChanges = false;
 256
 257                await _treeState.RefreshAsync();
 258                await _titleService.SetTitleAsync(EditName);
 259                _notifier.Success("Arc saved");
 260            }
 261        }
 262        catch (Exception ex)
 263        {
 264            _logger.LogErrorSanitized(ex, "Error saving arc");
 265            _notifier.Error($"Failed to save: {ex.Message}");
 266        }
 267        finally
 268        {
 269            IsSaving = false;
 270        }
 271    }
 272
 273    /// <summary>Confirms and deletes the arc, then navigates back to the campaign.</summary>
 274    public async Task DeleteAsync()
 275    {
 276        if (_arc == null || !IsCurrentUserGM || Sessions.Any())
 277            return;
 278
 279        var confirmed = await _confirmation.ConfirmAsync(
 280            "Delete Arc",
 281            $"Are you sure you want to delete '{_arc.Name}'? This action cannot be undone.",
 282            "Delete",
 283            "Cancel");
 284
 285        if (!confirmed)
 286            return;
 287
 288        try
 289        {
 290            await _arcApi.DeleteArcAsync(_arc.Id);
 291            await _treeState.RefreshAsync();
 292            _notifier.Success("Arc deleted");
 293            await _navigator.GoToCampaignAsync(_arc.WorldSlug, _arc.CampaignSlug);
 294        }
 295        catch (Exception ex)
 296        {
 297            _logger.LogErrorSanitized(ex, "Error deleting arc");
 298            _notifier.Error($"Failed to delete: {ex.Message}");
 299        }
 300    }
 301
 302    /// <summary>Creates a new Session entity under this arc and navigates to it.</summary>
 303    public async Task CreateSessionAsync()
 304    {
 305        if (_arc == null || !IsCurrentUserGM)
 306            return;
 307
 308        try
 309        {
 310            var createdSessionId = await _treeState.CreateChildArticleAsync(_arc.Id);
 311            if (!createdSessionId.HasValue)
 312            {
 313                _notifier.Error("Failed to create session");
 314                return;
 315            }
 316
 317            _treeState.TryGetNode(createdSessionId.Value, out var sessionNode);
 318            if (sessionNode != null && !string.IsNullOrEmpty(sessionNode.Slug))
 319            {
 320                await _navigator.GoToSessionAsync(sessionNode.WorldSlug, sessionNode.CampaignSlug, sessionNode.ArcSlug, 
 321            }
 322            else
 323            {
 324                _logger.LogWarningSanitized("Created session node has empty slug; staying on current page");
 325            }
 326            _notifier.Success("Session created");
 327        }
 328        catch (Exception ex)
 329        {
 330            _logger.LogErrorSanitized(ex, "Error creating session");
 331            _notifier.Error($"Failed to create session: {ex.Message}");
 332        }
 333    }
 334
 335    /// <summary>Navigates to a Session entity.</summary>
 336    public Task NavigateToSessionAsync(SessionTreeDto session) =>
 1337        _navigator.GoToSessionAsync(session);
 338
 339    /// <summary>Sets the currently selected quest for the editor panel.</summary>
 2340    public void OnEditQuest(QuestDto quest) => SelectedQuest = quest;
 341
 342    /// <summary>Updates the selected quest after an edit.</summary>
 1343    public void OnQuestUpdated(QuestDto updatedQuest) => SelectedQuest = updatedQuest;
 344}