< Summary

Information
Class: Chronicis.Client.ViewModels.CampaignDetailViewModel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/CampaignDetailViewModel.cs
Line coverage
100%
Covered lines: 55
Uncovered lines: 0
Coverable lines: 55
Total lines: 292
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_Campaign()100%11100%
get_Arcs()100%11100%
get_Breadcrumbs()100%11100%
get_IsCurrentUserGM()100%11100%
get_IsCurrentUserWorldOwner()100%11100%
get_CanManageCampaignDetails()100%22100%
get_CanViewPrivateNotes()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%
NavigateToArc(...)100%11100%

File(s)

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

#LineLine coverage
 1using Chronicis.Client.Abstractions;
 2using Chronicis.Client.Components.Dialogs;
 3using Chronicis.Client.Services;
 4using Chronicis.Shared.DTOs;
 5using Chronicis.Shared.Enums;
 6using Chronicis.Shared.Extensions;
 7using MudBlazor;
 8
 9namespace Chronicis.Client.ViewModels;
 10
 11/// <summary>
 12/// ViewModel for the CampaignDetail page.
 13/// Manages loading, editing, saving, and arc creation for a single campaign.
 14/// </summary>
 15public sealed class CampaignDetailViewModel : ViewModelBase
 16{
 17    private readonly ICampaignApiService _campaignApi;
 18    private readonly IArcApiService _arcApi;
 19    private readonly IWorldApiService _worldApi;
 20    private readonly IAuthService _authService;
 21    private readonly ITreeStateService _treeState;
 22    private readonly IBreadcrumbService _breadcrumbService;
 23    private readonly IAppNavigator _navigator;
 24    private readonly IUserNotifier _notifier;
 25    private readonly IPageTitleService _titleService;
 26    private readonly IDialogService _dialogService;
 27    private readonly ILogger<CampaignDetailViewModel> _logger;
 28
 2029    private bool _isLoading = true;
 30    private bool _isSaving;
 31    private bool _isTogglingActive;
 32    private bool _hasUnsavedChanges;
 33    private bool _summaryExpanded;
 34    private CampaignDetailDto? _campaign;
 2035    private List<ArcDto> _arcs = new();
 2036    private string _editName = string.Empty;
 2037    private string _editDescription = string.Empty;
 2038    private string _editPrivateNotes = string.Empty;
 2039    private List<BreadcrumbItem> _breadcrumbs = new();
 40    private bool _isCurrentUserGm;
 41    private bool _isCurrentUserWorldOwner;
 42
 2043    public CampaignDetailViewModel(
 2044        ICampaignApiService campaignApi,
 2045        IArcApiService arcApi,
 2046        IWorldApiService worldApi,
 2047        IAuthService authService,
 2048        ITreeStateService treeState,
 2049        IBreadcrumbService breadcrumbService,
 2050        IAppNavigator navigator,
 2051        IUserNotifier notifier,
 2052        IPageTitleService titleService,
 2053        IDialogService dialogService,
 2054        ILogger<CampaignDetailViewModel> logger)
 55    {
 2056        _campaignApi = campaignApi;
 2057        _arcApi = arcApi;
 2058        _worldApi = worldApi;
 2059        _authService = authService;
 2060        _treeState = treeState;
 2061        _breadcrumbService = breadcrumbService;
 2062        _navigator = navigator;
 2063        _notifier = notifier;
 2064        _titleService = titleService;
 2065        _dialogService = dialogService;
 2066        _logger = logger;
 2067    }
 68
 4169    public bool IsLoading { get => _isLoading; private set => SetField(ref _isLoading, value); }
 1270    public bool IsSaving { get => _isSaving; private set => SetField(ref _isSaving, value); }
 1571    public bool IsTogglingActive { get => _isTogglingActive; private set => SetField(ref _isTogglingActive, value); }
 5672    public bool HasUnsavedChanges { get => _hasUnsavedChanges; private set => SetField(ref _hasUnsavedChanges, value); }
 673    public bool SummaryExpanded { get => _summaryExpanded; set => SetField(ref _summaryExpanded, value); }
 5874    public CampaignDetailDto? Campaign { get => _campaign; private set => SetField(ref _campaign, value); }
 2175    public List<ArcDto> Arcs { get => _arcs; private set => SetField(ref _arcs, value); }
 1976    public List<BreadcrumbItem> Breadcrumbs { get => _breadcrumbs; private set => SetField(ref _breadcrumbs, value); }
 7777    public bool IsCurrentUserGM { get => _isCurrentUserGm; private set => SetField(ref _isCurrentUserGm, value); }
 6578    public bool IsCurrentUserWorldOwner { get => _isCurrentUserWorldOwner; private set => SetField(ref _isCurrentUserWor
 2979    public bool CanManageCampaignDetails => IsCurrentUserGM || IsCurrentUserWorldOwner;
 680    public bool CanViewPrivateNotes => CanManageCampaignDetails;
 81
 82    public string EditName
 83    {
 684        get => _editName;
 85        set
 86        {
 1787            if (SetField(ref _editName, value))
 1788                HasUnsavedChanges = true;
 1789        }
 90    }
 91
 92    public string EditDescription
 93    {
 1194        get => _editDescription;
 95        set
 96        {
 1697            if (SetField(ref _editDescription, value))
 1498                HasUnsavedChanges = true;
 1699        }
 100    }
 101
 102    public string EditPrivateNotes
 103    {
 3104        get => _editPrivateNotes;
 105        set
 106        {
 17107            if (SetField(ref _editPrivateNotes, value) && CanManageCampaignDetails)
 1108                HasUnsavedChanges = true;
 17109        }
 110    }
 111
 112    /// <summary>Loads the campaign and all related data for the given <paramref name="campaignId"/>.</summary>
 113    public async Task LoadAsync(Guid campaignId)
 114    {
 115        IsLoading = true;
 116
 117        try
 118        {
 119            IsCurrentUserGM = false;
 120            IsCurrentUserWorldOwner = false;
 121
 122            var campaign = await _campaignApi.GetCampaignAsync(campaignId);
 123            if (campaign == null)
 124            {
 125                _navigator.NavigateTo("/dashboard", replace: true);
 126                return;
 127            }
 128
 129            Campaign = campaign;
 130            EditName = campaign.Name;
 131            EditDescription = campaign.Description ?? string.Empty;
 132            EditPrivateNotes = campaign.PrivateNotes ?? string.Empty;
 133            HasUnsavedChanges = false;
 134
 135            Arcs = await _arcApi.GetArcsByCampaignAsync(campaignId);
 136
 137            var world = await _worldApi.GetWorldAsync(campaign.WorldId);
 138            Breadcrumbs = world != null
 139                ? _breadcrumbService.ForCampaign(campaign, world)
 140                : new List<BreadcrumbItem>
 141                {
 142                    new("Dashboard", href: "/dashboard"),
 143                    new(campaign.Name, href: null, disabled: true)
 144                };
 145
 146            await ResolveCurrentUserRoleAsync(world);
 147
 148            await _titleService.SetTitleAsync(campaign.Name);
 149            _treeState.ExpandPathToAndSelect(campaignId);
 150        }
 151        catch (Exception ex)
 152        {
 153            _logger.LogErrorSanitized(ex, "Error loading campaign {CampaignId}", campaignId);
 154            _notifier.Error($"Failed to load campaign: {ex.Message}");
 155        }
 156        finally
 157        {
 158            IsLoading = false;
 159        }
 160    }
 161
 162    /// <summary>Toggles the active state of the campaign.</summary>
 163    public async Task OnActiveToggleAsync(bool isActive)
 164    {
 165        if (_campaign == null || !CanManageCampaignDetails || IsTogglingActive)
 166            return;
 167
 168        IsTogglingActive = true;
 169
 170        try
 171        {
 172            if (isActive)
 173            {
 174                var success = await _campaignApi.ActivateCampaignAsync(_campaign.Id);
 175                if (success)
 176                {
 177                    _campaign.IsActive = true;
 178                    RaisePropertyChanged(nameof(Campaign));
 179                    _notifier.Success("Campaign set as active");
 180                }
 181                else
 182                {
 183                    _notifier.Error("Failed to activate campaign");
 184                }
 185            }
 186            else
 187            {
 188                _notifier.Info("To deactivate, set another campaign as active");
 189            }
 190        }
 191        catch (Exception ex)
 192        {
 193            _logger.LogErrorSanitized(ex, "Error toggling campaign active state");
 194            _notifier.Error($"Error: {ex.Message}");
 195        }
 196        finally
 197        {
 198            IsTogglingActive = false;
 199        }
 200    }
 201
 202    /// <summary>Persists name and description changes to the API.</summary>
 203    public async Task SaveAsync()
 204    {
 205        if (_campaign == null || !CanManageCampaignDetails || IsSaving)
 206            return;
 207
 208        IsSaving = true;
 209
 210        try
 211        {
 212            var updateDto = new CampaignUpdateDto
 213            {
 214                Name = EditName.Trim(),
 215                Description = string.IsNullOrWhiteSpace(EditDescription) ? null : EditDescription.Trim(),
 216                PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes
 217            };
 218
 219            var updated = await _campaignApi.UpdateCampaignAsync(_campaign.Id, updateDto);
 220            if (updated != null)
 221            {
 222                _campaign.Name = updated.Name;
 223                _campaign.Description = updated.Description;
 224                _campaign.PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes;
 225                HasUnsavedChanges = false;
 226
 227                await _treeState.RefreshAsync();
 228                await _titleService.SetTitleAsync(EditName);
 229                _notifier.Success("Campaign saved");
 230            }
 231        }
 232        catch (Exception ex)
 233        {
 234            _logger.LogErrorSanitized(ex, "Error saving campaign");
 235            _notifier.Error($"Failed to save: {ex.Message}");
 236        }
 237        finally
 238        {
 239            IsSaving = false;
 240        }
 241    }
 242
 243    /// <summary>Opens the create-arc dialog and navigates to the new arc on success.</summary>
 244    public async Task CreateArcAsync()
 245    {
 246        if (_campaign == null || !IsCurrentUserGM)
 247            return;
 248
 249        var parameters = new DialogParameters { { "CampaignId", _campaign.Id } };
 250        var dialog = await _dialogService.ShowAsync<CreateArcDialog>("New Arc", parameters);
 251        var result = await dialog.Result;
 252
 253        if (result != null && !result.Canceled && result.Data is ArcDto arc)
 254        {
 255            await _treeState.RefreshAsync();
 256            await LoadAsync(_campaign.Id);
 257            _navigator.NavigateTo($"/arc/{arc.Id}");
 258            _notifier.Success("Arc created");
 259        }
 260    }
 261
 262    /// <summary>Navigates to the arc detail page.</summary>
 1263    public void NavigateToArc(Guid arcId) => _navigator.NavigateTo($"/arc/{arcId}");
 264
 265    private async Task ResolveCurrentUserRoleAsync(WorldDetailDto? world)
 266    {
 267        IsCurrentUserGM = false;
 268        IsCurrentUserWorldOwner = false;
 269
 270        if (world?.Members == null)
 271        {
 272            return;
 273        }
 274
 275        var user = await _authService.GetCurrentUserAsync();
 276        if (user == null || string.IsNullOrWhiteSpace(user.Email))
 277        {
 278            return;
 279        }
 280
 281        var member = world.Members.FirstOrDefault(m =>
 282            m.Email.Equals(user.Email, StringComparison.OrdinalIgnoreCase));
 283
 284        if (member == null)
 285        {
 286            return;
 287        }
 288
 289        IsCurrentUserGM = member.Role == WorldRole.GM;
 290        IsCurrentUserWorldOwner = member.UserId == world.OwnerId;
 291    }
 292}