< Summary

Information
Class: Chronicis.Client.Pages.ArcDetail
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/ArcDetail.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 157
Coverable lines: 157
Total lines: 458
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 88
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildRenderTree(...)0%2040%
get_ArcId()100%210%
.ctor()100%210%
OnParametersSetAsync()100%210%
LoadArcAsync()0%1056320%
OnNameChanged()100%210%
OnDescriptionChanged()100%210%
OnSortOrderChanged()100%210%
OnActiveToggle()0%7280%
SaveArc()0%7280%
DeleteArc()0%7280%
CreateSession()0%7280%
NavigateToSession()0%2040%
OnQuestUpdated(...)100%210%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Pages/ArcDetail.razor

#LineLine coverage
 1@page "/arc/{ArcId:guid}"
 2@attribute [Authorize]
 3@using Chronicis.Shared.DTOs
 4@using Chronicis.Shared.DTOs.Quests
 5@using Chronicis.Shared.Enums
 6@using Chronicis.Client.Components.Dialogs
 7@using Chronicis.Client.Components.Shared
 8@using Chronicis.Client.Components.Quests
 9@using Chronicis.Client.Utilities
 10@inject IArcApiService ArcApi
 11@inject ICampaignApiService CampaignApi
 12@inject IWorldApiService WorldApi
 13@inject IArticleApiService ArticleApi
 14@inject IQuestApiService QuestApi
 15@inject IAuthService AuthService
 16@inject ITreeStateService TreeState
 17@inject IBreadcrumbService BreadcrumbService
 18@inject ISnackbar Snackbar
 19@inject IDialogService DialogService
 20@inject NavigationManager Navigation
 21@inject IJSRuntime JSRuntime
 22
 023@if (_isLoading)
 24{
 25    <LoadingSkeleton />
 26}
 027else if (_arc != null)
 28{
 29    <MudPaper Elevation="2" Class="chronicis-article-card chronicis-fade-in">
 30        <DetailPageHeader Breadcrumbs="_breadcrumbs"
 31                          Icon="@Icons.Material.Filled.Bookmark"
 32                          @bind-Title="_editName"
 33                          Placeholder="Arc Name"
 34                          OnTitleEdited="OnNameChanged"
 35                          OnEnterPressed="SaveArc" />
 36
 37        <!-- Description -->
 38        <MudTextField @bind-Value="_editDescription"
 39                      Label="Description"
 40                      Variant="Variant.Outlined"
 41                      Lines="3"
 42                      Placeholder="Describe this arc..."
 43                      Class="mb-4"
 44                      Immediate="true"
 45                      @onkeyup="OnDescriptionChanged" />
 46
 47        <!-- Sort Order -->
 48        <MudNumericField @bind-Value="_editSortOrder"
 49                         Label="Sort Order"
 50                         Variant="Variant.Outlined"
 51                         Min="0"
 52                         Class="mb-4"
 53                         Style="max-width: 150px;"
 54                         Immediate="true"
 55                         @onkeyup="OnSortOrderChanged" />
 56
 57        <!-- Sessions Section -->
 58        <div class="d-flex align-center justify-space-between mb-3">
 59            <MudText Typo="Typo.h6" Style="color: var(--chronicis-beige-gold);">
 60                <MudIcon Icon="@Icons.Material.Filled.EventNote" Size="Size.Small" Class="mr-2" />
 61                Sessions
 62            </MudText>
 63            <MudButton Variant="Variant.Filled"
 64                       Color="Color.Primary"
 65                       Size="Size.Small"
 66                       StartIcon="@Icons.Material.Filled.Add"
 67                       OnClick="CreateSession">
 68                New Session
 69            </MudButton>
 70        </div>
 71
 072        @if (_sessions.Any())
 73        {
 74            <MudList T="ArticleTreeDto" Dense="true" Class="mb-4">
 075                @foreach (var session in _sessions.OrderBy(s => s.Title))
 76                {
 77                    <EntityListItem Icon="@Icons.Material.Filled.EventNote"
 78                                    Title="@(string.IsNullOrEmpty(session.Title) ? "Untitled Session" : session.Title)"
 079                                    OnClick="@(() => NavigateToSession(session))" />
 80                }
 81            </MudList>
 82        }
 83        else
 84        {
 85            <MudAlert Severity="Severity.Info" Class="mb-4">
 86                No sessions yet. Create your first session to start documenting your adventures!
 87            </MudAlert>
 88        }
 89
 90        <!-- Quests Section -->
 91        <ArcQuestList ArcId="@ArcId" IsGm="_isCurrentUserGM" />
 92
 093        @if (_selectedQuest != null)
 94        {
 95            <MudDivider Class="my-4" />
 96
 97            <!-- Quest Editor -->
 98            <ArcQuestEditor Quest="_selectedQuest"
 99                            OnQuestUpdated="OnQuestUpdated" />
 100
 101            <!-- Quest Timeline -->
 102            <ArcQuestTimeline Quest="_selectedQuest"
 103                              IsGm="_isCurrentUserGM"
 104                              CurrentUserId="_currentUserId" />
 105        }
 106
 107        <!-- Arc Info -->
 108        <MudText Typo="Typo.h6" Class="mb-3" Style="color: var(--chronicis-beige-gold);">
 109            Arc Info
 110        </MudText>
 111
 112        <MudSimpleTable Dense="true" Hover="true" Class="mb-4">
 113            <tbody>
 114                <tr>
 115                    <td><MudIcon Icon="@Icons.Material.Filled.PlayCircle" Size="Size.Small" Class="mr-2" />Active Arc</t
 116                    <td>
 117                        <MudSwitch T="bool"
 118                                   Value="_arc.IsActive"
 119                                   ValueChanged="OnActiveToggle"
 120                                   Color="Color.Success"
 121                                   Size="Size.Small"
 122                                   Disabled="_isTogglingActive" />
 123                    </td>
 124                </tr>
 125                <tr>
 126                    <td><MudIcon Icon="@Icons.Material.Filled.EventNote" Size="Size.Small" Class="mr-2" />Sessions</td>
 0127                    <td>@_arc.SessionCount</td>
 128                </tr>
 129                <tr>
 130                    <td><MudIcon Icon="@Icons.Material.Filled.Sort" Size="Size.Small" Class="mr-2" />Sort Order</td>
 0131                    <td>@_arc.SortOrder</td>
 132                </tr>
 133                <tr>
 134                    <td><MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="mr-2" />Created</t
 0135                    <td>@_arc.CreatedAt.ToString("MMMM d, yyyy")</td>
 136                </tr>
 137            </tbody>
 138        </MudSimpleTable>
 139
 140        <!-- AI Summary Section -->
 141        <AISummarySection EntityId="ArcId"
 142                          EntityType="Arc"
 143                          @bind-IsExpanded="_summaryExpanded" />
 144
 145        <!-- Save Status -->
 146        <div class="chronicis-flex-between mt-4">
 147            <SaveStatusIndicator IsSaving="_isSaving" HasUnsavedChanges="_hasUnsavedChanges" />
 148
 149            <div class="d-flex gap-2">
 150                <MudButton Variant="Variant.Filled"
 151                           Color="Color.Primary"
 152                           OnClick="SaveArc"
 153                           Disabled="_isSaving"
 154                           StartIcon="@Icons.Material.Filled.Save">
 155                    Save
 156                </MudButton>
 157
 158                <MudButton Variant="Variant.Outlined"
 159                           Color="Color.Error"
 160                           OnClick="DeleteArc"
 161                           Disabled="_isSaving || _sessions.Any()"
 162                           StartIcon="@Icons.Material.Filled.Delete">
 163                    Delete
 164                </MudButton>
 165            </div>
 166        </div>
 167
 0168        @if (_sessions.Any())
 169        {
 170            <MudText Typo="Typo.caption" Class="mt-2" Style="color: var(--mud-palette-text-secondary);">
 171                Delete all sessions before deleting this arc.
 172            </MudText>
 173        }
 174    </MudPaper>
 175}
 176
 177@code {
 178    [Parameter]
 0179    public Guid ArcId { get; set; }
 180
 181    private ArcDto? _arc;
 182    private CampaignDto? _campaign;
 183    private WorldDto? _world;
 0184    private List<ArticleTreeDto> _sessions = new();
 0185    private string _editName = string.Empty;
 0186    private string _editDescription = string.Empty;
 187    private int _editSortOrder = 0;
 0188    private bool _isLoading = true;
 189    private bool _isSaving = false;
 190    private bool _isTogglingActive = false;
 191    private bool _hasUnsavedChanges = false;
 192    private bool _summaryExpanded = false;
 0193    private List<BreadcrumbItem> _breadcrumbs = new();
 194
 195    // Quest state
 196    private QuestDto? _selectedQuest;
 197    private Guid _currentUserId;
 198    private bool _isCurrentUserGM = false;
 199
 200    protected override async Task OnParametersSetAsync()
 201    {
 0202        await LoadArcAsync();
 0203    }
 204
 205    private async Task LoadArcAsync()
 206    {
 0207        _isLoading = true;
 0208        StateHasChanged();
 209
 210        try
 211        {
 0212            _arc = await ArcApi.GetArcAsync(ArcId);
 0213            if (_arc == null)
 214            {
 0215                Navigation.NavigateTo("/dashboard", replace: true);
 0216                return;
 217            }
 218            else
 219            {
 0220                _editName = _arc.Name;
 0221                _editDescription = _arc.Description ?? string.Empty;
 0222                _editSortOrder = _arc.SortOrder;
 0223                _hasUnsavedChanges = false;
 224
 225                // Load sessions (articles with ArcId and Session type)
 0226                var allArticles = await ArticleApi.GetAllArticlesAsync();
 0227                _sessions = allArticles
 0228                    .Where(a => a.ArcId == ArcId && a.Type == ArticleType.Session)
 0229                    .ToList();
 230
 231                // Load campaign and world for breadcrumbs
 0232                _campaign = await CampaignApi.GetCampaignAsync(_arc.CampaignId);
 0233                if (_campaign != null)
 234                {
 0235                    var worldDetail = await WorldApi.GetWorldAsync(_campaign.WorldId);
 0236                    _world = worldDetail != null ? new WorldDto
 0237                    {
 0238                        Id = worldDetail.Id,
 0239                        Name = worldDetail.Name,
 0240                        Slug = worldDetail.Slug
 0241                    } : null;
 242                }
 243
 0244                if (_arc != null && _campaign != null && _world != null)
 245                {
 0246                    _breadcrumbs = BreadcrumbService.ForArc(_arc, _campaign, _world);
 247                }
 248                else
 249                {
 250                    // Fallback if data not fully loaded
 0251                    _breadcrumbs = new List<BreadcrumbItem>
 0252                    {
 0253                        new("Dashboard", href: "/dashboard"),
 0254                        new(_arc?.Name ?? "Arc", href: null, disabled: true)
 0255                    };
 256                }
 257
 0258                await JSRuntime.InvokeVoidAsync("eval", $"document.title = '{JsUtilities.EscapeForJs(_arc?.Name ?? "Arc"
 259
 260                // Highlight in tree
 0261                TreeState.ExpandPathToAndSelect(ArcId);
 262
 263                // Get current user info for quest permissions
 0264                var user = await AuthService.GetCurrentUserAsync();
 0265                if (user != null && _world != null)
 266                {
 0267                    var worldDetail = await WorldApi.GetWorldAsync(_world.Id);
 0268                    if (worldDetail?.Members != null)
 269                    {
 0270                        var currentMember = worldDetail.Members.FirstOrDefault(m =>
 0271                            m.Email.Equals(user.Email, StringComparison.OrdinalIgnoreCase));
 272
 0273                        if (currentMember != null)
 274                        {
 0275                            _currentUserId = currentMember.UserId;
 0276                            _isCurrentUserGM = currentMember.Role == WorldRole.GM;
 277                        }
 278                    }
 279                }
 0280            }
 0281        }
 0282        catch (Exception ex)
 283        {
 0284            Snackbar.Add($"Failed to load arc: {ex.Message}", Severity.Error);
 0285        }
 286        finally
 287        {
 0288            _isLoading = false;
 289        }
 0290    }
 291
 0292    private void OnNameChanged() => _hasUnsavedChanges = true;
 0293    private void OnDescriptionChanged() => _hasUnsavedChanges = true;
 0294    private void OnSortOrderChanged() => _hasUnsavedChanges = true;
 295
 296    private async Task OnActiveToggle(bool isActive)
 297    {
 0298        if (_arc == null || _isTogglingActive) return;
 299
 0300        _isTogglingActive = true;
 0301        StateHasChanged();
 302
 303        try
 304        {
 0305            if (isActive)
 306            {
 0307                var success = await ArcApi.ActivateArcAsync(ArcId);
 0308                if (success)
 309                {
 0310                    _arc.IsActive = true;
 0311                    Snackbar.Add("Arc set as active", Severity.Success);
 312                }
 313                else
 314                {
 0315                    Snackbar.Add("Failed to activate arc", Severity.Error);
 316                }
 317            }
 318            else
 319            {
 0320                Snackbar.Add("To deactivate, set another arc as active", Severity.Info);
 321            }
 0322        }
 0323        catch (Exception ex)
 324        {
 0325            Snackbar.Add($"Error: {ex.Message}", Severity.Error);
 0326        }
 327        finally
 328        {
 0329            _isTogglingActive = false;
 0330            StateHasChanged();
 331        }
 0332    }
 333
 334    private async Task SaveArc()
 335    {
 0336        if (_arc == null || _isSaving) return;
 337
 0338        _isSaving = true;
 0339        StateHasChanged();
 340
 341        try
 342        {
 0343            var updateDto = new ArcUpdateDto
 0344            {
 0345                Name = _editName.Trim(),
 0346                Description = string.IsNullOrWhiteSpace(_editDescription) ? null : _editDescription.Trim(),
 0347                SortOrder = _editSortOrder
 0348            };
 349
 0350            var updated = await ArcApi.UpdateArcAsync(ArcId, updateDto);
 0351            if (updated != null)
 352            {
 0353                _arc.Name = updated.Name;
 0354                _arc.Description = updated.Description;
 0355                _arc.SortOrder = updated.SortOrder;
 0356                _hasUnsavedChanges = false;
 357
 0358                await TreeState.RefreshAsync();
 0359                await JSRuntime.InvokeVoidAsync("eval", $"document.title = '{JsUtilities.EscapeForJs(_editName)} - Chron
 360
 0361                Snackbar.Add("Arc saved", Severity.Success);
 362            }
 0363        }
 0364        catch (Exception ex)
 365        {
 0366            Snackbar.Add($"Failed to save: {ex.Message}", Severity.Error);
 0367        }
 368        finally
 369        {
 0370            _isSaving = false;
 0371            StateHasChanged();
 372        }
 0373    }
 374
 375    private async Task DeleteArc()
 376    {
 0377        if (_arc == null || _sessions.Any()) return;
 378
 0379        var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
 0380            $"Are you sure you want to delete '{_arc.Name}'?\n\nThis action cannot be undone.");
 381
 0382        if (!confirmed) return;
 383
 384        try
 385        {
 0386            await ArcApi.DeleteArcAsync(ArcId);
 0387            await TreeState.RefreshAsync();
 388
 0389            Snackbar.Add("Arc deleted", Severity.Success);
 0390            Navigation.NavigateTo($"/campaign/{_arc.CampaignId}");
 0391        }
 0392        catch (Exception ex)
 393        {
 0394            Snackbar.Add($"Failed to delete: {ex.Message}", Severity.Error);
 0395        }
 0396    }
 397
 398    private async Task CreateSession()
 399    {
 0400        if (_arc == null || _campaign == null) return;
 401
 0402        var sessionNumber = _sessions.Count + 1;
 0403        var createDto = new ArticleCreateDto
 0404        {
 0405            Title = $"Session {sessionNumber}",
 0406            Body = string.Empty,
 0407            WorldId = _campaign.WorldId,
 0408            CampaignId = _campaign.Id,
 0409            ArcId = ArcId,
 0410            Type = ArticleType.Session,
 0411            EffectiveDate = DateTime.Now
 0412        };
 413
 0414        var created = await ArticleApi.CreateArticleAsync(createDto);
 0415        if (created == null)
 416        {
 0417            Snackbar.Add("Failed to create session", Severity.Error);
 0418            return;
 419        }
 420
 0421        await TreeState.RefreshAsync();
 0422        await LoadArcAsync();
 423
 424        // Navigate using full path from breadcrumbs
 0425        if (created.Breadcrumbs.Any())
 426        {
 0427            var path = BreadcrumbService.BuildArticleUrl(created.Breadcrumbs);
 0428            Navigation.NavigateTo(path);
 429        }
 430        else
 431        {
 0432            Navigation.NavigateTo($"/article/{created.Slug}");
 433        }
 0434        Snackbar.Add("Session created", Severity.Success);
 0435    }
 436
 437    private async Task NavigateToSession(ArticleTreeDto session)
 438    {
 439        // Load full article to get breadcrumbs for proper path
 0440        var article = await ArticleApi.GetArticleDetailAsync(session.Id);
 0441        if (article != null && article.Breadcrumbs.Any())
 442        {
 0443            var path = BreadcrumbService.BuildArticleUrl(article.Breadcrumbs);
 0444            Navigation.NavigateTo(path);
 445        }
 446        else
 447        {
 448            // Fallback
 0449            Navigation.NavigateTo($"/article/{session.Slug}");
 450        }
 0451    }
 452
 453    private void OnQuestUpdated(QuestDto updatedQuest)
 454    {
 0455        _selectedQuest = updatedQuest;
 0456        StateHasChanged();
 0457    }
 458}