< 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: 60
Uncovered lines: 0
Coverable lines: 60
Total lines: 339
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
 2732    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;
 2739    private List<SessionTreeDto> _sessions = new();
 2740    private string _editName = string.Empty;
 2741    private string _editDescription = string.Empty;
 2742    private string _editPrivateNotes = string.Empty;
 43    private int _editSortOrder;
 2744    private List<BreadcrumbItem> _breadcrumbs = new();
 45    private QuestDto? _selectedQuest;
 46    private Guid _currentUserId;
 47    private bool _isCurrentUserGM;
 48    private bool _isCurrentUserWorldOwner;
 49
 2750    public ArcDetailViewModel(
 2751        IArcApiService arcApi,
 2752        ICampaignApiService campaignApi,
 2753        IWorldApiService worldApi,
 2754        ISessionApiService sessionApi,
 2755        IQuestApiService questApi,
 2756        IAuthService authService,
 2757        ITreeStateService treeState,
 2758        IBreadcrumbService breadcrumbService,
 2759        IAppNavigator navigator,
 2760        IUserNotifier notifier,
 2761        IPageTitleService titleService,
 2762        IConfirmationService confirmation,
 2763        ILogger<ArcDetailViewModel> logger)
 64    {
 2765        _arcApi = arcApi;
 2766        _campaignApi = campaignApi;
 2767        _worldApi = worldApi;
 2768        _sessionApi = sessionApi;
 2769        _questApi = questApi;
 2770        _authService = authService;
 2771        _treeState = treeState;
 2772        _breadcrumbService = breadcrumbService;
 2773        _navigator = navigator;
 2774        _notifier = notifier;
 2775        _titleService = titleService;
 2776        _confirmation = confirmation;
 2777        _logger = logger;
 2778    }
 79
 5180    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); }
 8983    public bool HasUnsavedChanges { get => _hasUnsavedChanges; private set => SetField(ref _hasUnsavedChanges, value); }
 684    public bool SummaryExpanded { get => _summaryExpanded; set => SetField(ref _summaryExpanded, value); }
 4885    public ArcDto? Arc { get => _arc; private set => SetField(ref _arc, value); }
 2986    public CampaignDto? Campaign { get => _campaign; private set => SetField(ref _campaign, value); }
 3487    public List<SessionTreeDto> Sessions { get => _sessions; private set => SetField(ref _sessions, value); }
 2288    public List<BreadcrumbItem> Breadcrumbs { get => _breadcrumbs; private set => SetField(ref _breadcrumbs, value); }
 1289    public QuestDto? SelectedQuest { get => _selectedQuest; private set => SetField(ref _selectedQuest, value); }
 3490    public Guid CurrentUserId { get => _currentUserId; private set => SetField(ref _currentUserId, value); }
 8891    public bool IsCurrentUserGM { get => _isCurrentUserGM; private set => SetField(ref _isCurrentUserGM, value); }
 6292    public bool IsCurrentUserWorldOwner { get => _isCurrentUserWorldOwner; private set => SetField(ref _isCurrentUserWor
 3493    public bool CanManageArcDetails => IsCurrentUserGM || IsCurrentUserWorldOwner;
 694    public bool CanViewPrivateNotes => CanManageArcDetails;
 7095    public int EditSortOrder { get => _editSortOrder; set { if (SetField(ref _editSortOrder, value)) HasUnsavedChanges =
 96
 97    public string EditName
 98    {
 699        get => _editName;
 66100        set { if (SetField(ref _editName, value)) HasUnsavedChanges = true; }
 101    }
 102
 103    public string EditDescription
 104    {
 11105        get => _editDescription;
 58106        set { if (SetField(ref _editDescription, value)) HasUnsavedChanges = true; }
 107    }
 108
 109    public string EditPrivateNotes
 110    {
 2111        get => _editPrivateNotes;
 45112        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            _navigator.NavigateTo($"/campaign/{_arc.CampaignId}");
 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            _navigator.NavigateTo($"/session/{createdSessionId.Value}");
 318            _notifier.Success("Session created");
 319        }
 320        catch (Exception ex)
 321        {
 322            _logger.LogErrorSanitized(ex, "Error creating session");
 323            _notifier.Error($"Failed to create session: {ex.Message}");
 324        }
 325    }
 326
 327    /// <summary>Navigates to a Session entity.</summary>
 328    public Task NavigateToSessionAsync(SessionTreeDto session)
 329    {
 1330        _navigator.NavigateTo($"/session/{session.Id}");
 1331        return Task.CompletedTask;
 332    }
 333
 334    /// <summary>Sets the currently selected quest for the editor panel.</summary>
 2335    public void OnEditQuest(QuestDto quest) => SelectedQuest = quest;
 336
 337    /// <summary>Updates the selected quest after an edit.</summary>
 1338    public void OnQuestUpdated(QuestDto updatedQuest) => SelectedQuest = updatedQuest;
 339}