Add Dashboard page (F1)
- DashboardDto, IDashboardService, DashboardService - Summary cards: members, guests, days, open expenses - Recent days list with navigation - Top penalty recipients list - Home redirects to /dashboard - Updated HomePageTests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
005bfebe6d
commit
7c9f3c36d9
|
|
@ -0,0 +1,68 @@
|
||||||
|
namespace Koogle.Application.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data transfer object for dashboard summary data.
|
||||||
|
/// </summary>
|
||||||
|
public record DashboardDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of active members in the club.
|
||||||
|
/// </summary>
|
||||||
|
public int MemberCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of guests in the club.
|
||||||
|
/// </summary>
|
||||||
|
public int GuestCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of game days this year.
|
||||||
|
/// </summary>
|
||||||
|
public int DaysThisYear { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total open (unpaid) expense amount across all persons.
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalOpenAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of open expenses.
|
||||||
|
/// </summary>
|
||||||
|
public int OpenExpenseCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recent game days (last 5).
|
||||||
|
/// </summary>
|
||||||
|
public List<DaySummaryDto> RecentDays { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Top penalty recipients (top 5 by open amount).
|
||||||
|
/// </summary>
|
||||||
|
public List<TopPenaltyRecipientDto> TopPenaltyRecipients { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data transfer object for top penalty recipient.
|
||||||
|
/// </summary>
|
||||||
|
public record TopPenaltyRecipientDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Person ID.
|
||||||
|
/// </summary>
|
||||||
|
public Guid PersonId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Person name.
|
||||||
|
/// </summary>
|
||||||
|
public string PersonName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total open amount.
|
||||||
|
/// </summary>
|
||||||
|
public decimal OpenAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of open expenses.
|
||||||
|
/// </summary>
|
||||||
|
public int ExpenseCount { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ namespace Koogle.Application
|
||||||
services.AddScoped<IExpenseService, ExpenseService>();
|
services.AddScoped<IExpenseService, ExpenseService>();
|
||||||
services.AddScoped<IDayService, DayService>();
|
services.AddScoped<IDayService, DayService>();
|
||||||
services.AddScoped<IPersonExpenseService, PersonExpenseService>();
|
services.AddScoped<IPersonExpenseService, PersonExpenseService>();
|
||||||
|
services.AddScoped<IDashboardService, DashboardService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
using Koogle.Application.DTOs;
|
||||||
|
|
||||||
|
namespace Koogle.Application.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service interface for dashboard data aggregation.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDashboardService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves dashboard summary data for the current club.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>Dashboard summary data.</returns>
|
||||||
|
Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
using Koogle.Application.DTOs;
|
||||||
|
using Koogle.Application.Interfaces;
|
||||||
|
using Koogle.Domain.Enums;
|
||||||
|
using Koogle.Domain.Interfaces;
|
||||||
|
using KoogleApp.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Koogle.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for aggregating dashboard data for the current club.
|
||||||
|
/// </summary>
|
||||||
|
public class DashboardService : IDashboardService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||||
|
private readonly ICurrentClubContext _clubContext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DashboardService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contextFactory">The database context factory.</param>
|
||||||
|
/// <param name="clubContext">The current club context.</param>
|
||||||
|
public DashboardService(
|
||||||
|
IDbContextFactory<AppDbContext> contextFactory,
|
||||||
|
ICurrentClubContext clubContext)
|
||||||
|
{
|
||||||
|
_contextFactory = contextFactory;
|
||||||
|
_clubContext = clubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<DashboardDto> GetDashboardAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
var clubId = _clubContext.ClubId;
|
||||||
|
var currentYear = DateTime.Now.Year;
|
||||||
|
|
||||||
|
// Member and guest counts
|
||||||
|
var memberCount = await context.Persons
|
||||||
|
.CountAsync(p => p.ClubId == clubId && !p.IsDeleted && p.PersonStatus == PersonStatus.Member, ct);
|
||||||
|
|
||||||
|
var guestCount = await context.Persons
|
||||||
|
.CountAsync(p => p.ClubId == clubId && !p.IsDeleted && p.PersonStatus == PersonStatus.Guest, ct);
|
||||||
|
|
||||||
|
// Days this year
|
||||||
|
var daysThisYear = await context.Days
|
||||||
|
.CountAsync(d => d.ClubId == clubId && !d.IsDeleted && d.PostDate.Year == currentYear, ct);
|
||||||
|
|
||||||
|
// Open expenses
|
||||||
|
var openExpenses = await context.PersonExpenses
|
||||||
|
.Where(pe => pe.ClubId == clubId && !pe.IsDeleted && pe.PersonExpenseStatus == PersonExpenseStatus.Open)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var totalOpenAmount = openExpenses.Sum(pe => pe.Price);
|
||||||
|
var openExpenseCount = openExpenses.Count;
|
||||||
|
|
||||||
|
// Recent days (last 5)
|
||||||
|
var recentDays = await context.Days
|
||||||
|
.Where(d => d.ClubId == clubId && !d.IsDeleted)
|
||||||
|
.OrderByDescending(d => d.PostDate)
|
||||||
|
.Take(5)
|
||||||
|
.Include(d => d.DayPersons)
|
||||||
|
.Select(d => new DaySummaryDto
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
PostDate = d.PostDate,
|
||||||
|
Status = d.Status,
|
||||||
|
ParticipantCount = d.DayPersons.Count
|
||||||
|
})
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
// Top penalty recipients (top 5 by open amount this year)
|
||||||
|
var topRecipients = await context.PersonExpenses
|
||||||
|
.Where(pe => pe.ClubId == clubId
|
||||||
|
&& !pe.IsDeleted
|
||||||
|
&& pe.PersonExpenseStatus == PersonExpenseStatus.Open
|
||||||
|
&& pe.Day.PostDate.Year == currentYear)
|
||||||
|
.Include(pe => pe.Person)
|
||||||
|
.GroupBy(pe => new { pe.PersonId, pe.Person.Name })
|
||||||
|
.Select(g => new TopPenaltyRecipientDto
|
||||||
|
{
|
||||||
|
PersonId = g.Key.PersonId,
|
||||||
|
PersonName = g.Key.Name,
|
||||||
|
OpenAmount = g.Sum(pe => pe.Price),
|
||||||
|
ExpenseCount = g.Count()
|
||||||
|
})
|
||||||
|
.OrderByDescending(t => t.OpenAmount)
|
||||||
|
.Take(5)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
return new DashboardDto
|
||||||
|
{
|
||||||
|
MemberCount = memberCount,
|
||||||
|
GuestCount = guestCount,
|
||||||
|
DaysThisYear = daysThisYear,
|
||||||
|
TotalOpenAmount = totalOpenAmount,
|
||||||
|
OpenExpenseCount = openExpenseCount,
|
||||||
|
RecentDays = recentDays,
|
||||||
|
TopPenaltyRecipients = topRecipients
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
@page "/dashboard"
|
||||||
|
@attribute [Authorize(Policy = "ClubViewer")]
|
||||||
|
|
||||||
|
@using Koogle.Application.DTOs
|
||||||
|
@using Koogle.Application.Interfaces
|
||||||
|
@using Koogle.Domain.Enums
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
|
@inject IDashboardService DashboardService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>Dashboard</PageTitle>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">Dashboard</MudText>
|
||||||
|
|
||||||
|
@if (_isLoading)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||||
|
}
|
||||||
|
else if (_error is not null)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
||||||
|
}
|
||||||
|
else if (_dashboard is not null)
|
||||||
|
{
|
||||||
|
<MudGrid>
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudCard Elevation="2">
|
||||||
|
<MudCardContent Class="d-flex flex-column align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Large" Color="Color.Primary" />
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-2">@_dashboard.MemberCount</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Mitglieder</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudCard Elevation="2">
|
||||||
|
<MudCardContent Class="d-flex flex-column align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.PersonAdd" Size="Size.Large" Color="Color.Info" />
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-2">@_dashboard.GuestCount</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Gäste</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudCard Elevation="2">
|
||||||
|
<MudCardContent Class="d-flex flex-column align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Size="Size.Large" Color="Color.Success" />
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-2">@_dashboard.DaysThisYear</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Spieltage @DateTime.Now.Year</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudCard Elevation="2" Style="cursor: pointer" @onclick="NavigateToExpenses">
|
||||||
|
<MudCardContent Class="d-flex flex-column align-center">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.MonetizationOn" Size="Size.Large" Color="Color.Warning" />
|
||||||
|
<MudText Typo="Typo.h4" Class="mt-2">@_dashboard.TotalOpenAmount.ToString("C")</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Offene Strafen (@_dashboard.OpenExpenseCount)</MudText>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Recent Days -->
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCard Elevation="2">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.h6">Letzte Spieltage</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
<CardHeaderActions>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.ArrowForward" OnClick="NavigateToDays" />
|
||||||
|
</CardHeaderActions>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
@if (_dashboard.RecentDays.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Keine Spieltage vorhanden</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudList T="DaySummaryDto" Dense="true">
|
||||||
|
@foreach (var day in _dashboard.RecentDays)
|
||||||
|
{
|
||||||
|
<MudListItem T="DaySummaryDto" OnClick="@(() => NavigateToDayDetails(day.Id))">
|
||||||
|
<div class="d-flex justify-space-between align-center" style="width: 100%">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.body1">@day.PostDate.ToString("dd.MM.yyyy")</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">@day.ParticipantCount Teilnehmer</MudText>
|
||||||
|
</div>
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="GetStatusColor(day.Status)">
|
||||||
|
@GetStatusLabel(day.Status)
|
||||||
|
</MudChip>
|
||||||
|
</div>
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Top Penalty Recipients -->
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudCard Elevation="2">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.h6">Top Strafzahler @DateTime.Now.Year</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
@if (_dashboard.TopPenaltyRecipients.Count == 0)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">Keine offenen Strafen</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudList T="TopPenaltyRecipientDto" Dense="true">
|
||||||
|
@foreach (var recipient in _dashboard.TopPenaltyRecipients)
|
||||||
|
{
|
||||||
|
<MudListItem T="TopPenaltyRecipientDto">
|
||||||
|
<div class="d-flex justify-space-between align-center" style="width: 100%">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.body1">@recipient.PersonName</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">@recipient.ExpenseCount Strafen</MudText>
|
||||||
|
</div>
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Warning">
|
||||||
|
@recipient.OpenAmount.ToString("C")
|
||||||
|
</MudChip>
|
||||||
|
</div>
|
||||||
|
</MudListItem>
|
||||||
|
}
|
||||||
|
</MudList>
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private DashboardDto? _dashboard;
|
||||||
|
private bool _isLoading = true;
|
||||||
|
private string? _error;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadDashboardAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadDashboardAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
_dashboard = await DashboardService.GetDashboardAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_error = $"Fehler beim Laden des Dashboards: {ex.Message}";
|
||||||
|
Snackbar.Add(_error, Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetStatusLabel(DayStatus status) => status switch
|
||||||
|
{
|
||||||
|
DayStatus.New => "Neu",
|
||||||
|
DayStatus.Started => "Gestartet",
|
||||||
|
DayStatus.Postponed => "Verschoben",
|
||||||
|
DayStatus.Closed => "Abgeschlossen",
|
||||||
|
_ => status.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetStatusColor(DayStatus status) => status switch
|
||||||
|
{
|
||||||
|
DayStatus.New => Color.Info,
|
||||||
|
DayStatus.Started => Color.Warning,
|
||||||
|
DayStatus.Postponed => Color.Secondary,
|
||||||
|
DayStatus.Closed => Color.Success,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
private void NavigateToDays()
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/days");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToDayDetails(Guid dayId)
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo($"/days/{dayId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigateToExpenses()
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo("/expenses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
@page "/"
|
@page "/"
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
|
||||||
|
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
Welcome to your new app.
|
{
|
||||||
|
NavigationManager.NavigateTo("/dashboard", replace: true);
|
||||||
|
}
|
||||||
<AuthTest/>
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using Koogle.Application.Interfaces;
|
||||||
using Koogle.Web.Store.AuthState;
|
using Koogle.Web.Store.AuthState;
|
||||||
using Koogle.Web.Store.ClubState;
|
using Koogle.Web.Store.ClubState;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
@ -16,7 +15,7 @@ using System.Security.Claims;
|
||||||
namespace Koogle.Tests.Components;
|
namespace Koogle.Tests.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests for the Home page component.
|
/// Tests for the Home page component (now redirects to Dashboard).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HomePageTests : IDisposable
|
public class HomePageTests : IDisposable
|
||||||
{
|
{
|
||||||
|
|
@ -39,7 +38,7 @@ public class HomePageTests : IDisposable
|
||||||
_clubContextMock.Setup(c => c.ClubId).Returns(Guid.NewGuid());
|
_clubContextMock.Setup(c => c.ClubId).Returns(Guid.NewGuid());
|
||||||
_clubContextMock.Setup(c => c.ClubName).Returns("Test Club");
|
_clubContextMock.Setup(c => c.ClubName).Returns("Test Club");
|
||||||
|
|
||||||
// Setup Fluxor states - required by Home page and child components
|
// Setup Fluxor states
|
||||||
_authStateMock = new Mock<IState<AuthState>>();
|
_authStateMock = new Mock<IState<AuthState>>();
|
||||||
_authStateMock.Setup(s => s.Value).Returns(AuthState.Initial);
|
_authStateMock.Setup(s => s.Value).Returns(AuthState.Initial);
|
||||||
|
|
||||||
|
|
@ -55,7 +54,10 @@ public class HomePageTests : IDisposable
|
||||||
It.IsAny<string>()))
|
It.IsAny<string>()))
|
||||||
.ReturnsAsync(AuthorizationResult.Success());
|
.ReturnsAsync(AuthorizationResult.Success());
|
||||||
|
|
||||||
// Register all services before any rendering
|
// Add test authorization first before any other services
|
||||||
|
_ctx.AddTestAuthorization().SetAuthorized("testuser");
|
||||||
|
|
||||||
|
// Register all services
|
||||||
_ctx.Services.AddMudServices(options =>
|
_ctx.Services.AddMudServices(options =>
|
||||||
{
|
{
|
||||||
options.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight;
|
options.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight;
|
||||||
|
|
@ -68,16 +70,16 @@ public class HomePageTests : IDisposable
|
||||||
_ctx.Services.AddSingleton(_clubStateMock.Object);
|
_ctx.Services.AddSingleton(_clubStateMock.Object);
|
||||||
_ctx.Services.AddSingleton(_authorizationServiceMock.Object);
|
_ctx.Services.AddSingleton(_authorizationServiceMock.Object);
|
||||||
|
|
||||||
// Additional Fluxor services required by components
|
// Additional Fluxor services
|
||||||
_ctx.Services.AddSingleton(new Mock<IActionSubscriber>().Object);
|
_ctx.Services.AddSingleton(new Mock<IActionSubscriber>().Object);
|
||||||
_ctx.Services.AddSingleton(new Mock<IStore>().Object);
|
_ctx.Services.AddSingleton(new Mock<IStore>().Object);
|
||||||
|
|
||||||
// ASP.NET Core services needed by child components
|
// ASP.NET Core services
|
||||||
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
|
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
|
||||||
httpContextAccessorMock.Setup(x => x.HttpContext).Returns(new DefaultHttpContext());
|
httpContextAccessorMock.Setup(x => x.HttpContext).Returns(new DefaultHttpContext());
|
||||||
_ctx.Services.AddSingleton(httpContextAccessorMock.Object);
|
_ctx.Services.AddSingleton(httpContextAccessorMock.Object);
|
||||||
|
|
||||||
// Stub complex components to avoid deep dependency chains
|
// Stub complex components
|
||||||
_ctx.ComponentFactories.AddStub<Koogle.Web.Components.Shared.LogoutButton>();
|
_ctx.ComponentFactories.AddStub<Koogle.Web.Components.Shared.LogoutButton>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,89 +89,30 @@ public class HomePageTests : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HomePage_RendersWelcomeMessage_WhenAuthorized()
|
public void HomePage_RedirectsToDashboard_WhenAuthorized()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var authContext = _ctx.AddTestAuthorization();
|
_authStateMock.Setup(s => s.Value).Returns(CreateAuthState());
|
||||||
authContext.SetAuthorized("testuser");
|
|
||||||
|
|
||||||
// Setup authenticated auth state
|
// Act
|
||||||
|
_ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
||||||
|
|
||||||
|
// Assert - Home page should redirect to Dashboard
|
||||||
|
var navMan = _ctx.Services.GetRequiredService<FakeNavigationManager>();
|
||||||
|
navMan.Uri.Should().EndWith("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HomePage_RendersNoContent_BecauseItRedirects()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
_authStateMock.Setup(s => s.Value).Returns(CreateAuthState());
|
_authStateMock.Setup(s => s.Value).Returns(CreateAuthState());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var cut = _ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
var cut = _ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
||||||
|
|
||||||
// Assert
|
// Assert - Home page renders no visible content since it redirects
|
||||||
cut.Markup.Should().Contain("Hello, world!");
|
cut.Markup.Should().BeEmpty();
|
||||||
cut.Markup.Should().Contain("Welcome to your new app");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void HomePage_SetsPageTitle()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var authContext = _ctx.AddTestAuthorization();
|
|
||||||
authContext.SetAuthorized("testuser");
|
|
||||||
|
|
||||||
// Setup authenticated auth state
|
|
||||||
_authStateMock.Setup(s => s.Value).Returns(CreateAuthState());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var cut = _ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
var pageTitle = cut.FindComponent<Microsoft.AspNetCore.Components.Web.PageTitle>();
|
|
||||||
pageTitle.Should().NotBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void HomePage_DisplaysCurrentClub_WhenAuthorized()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var authContext = _ctx.AddTestAuthorization();
|
|
||||||
authContext.SetAuthorized("testuser");
|
|
||||||
|
|
||||||
_authStateMock.Setup(s => s.Value).Returns(CreateAuthState());
|
|
||||||
_clubContextMock.Setup(c => c.ClubName).Returns("Kegel Freunde 2025");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var cut = _ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
|
||||||
|
|
||||||
// Assert - AuthTest component shows current club name
|
|
||||||
cut.Markup.Should().Contain("Kegel Freunde 2025");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void HomePage_ShowsUserDisplayName_WhenAuthenticated()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var authContext = _ctx.AddTestAuthorization();
|
|
||||||
authContext.SetAuthorized("testuser");
|
|
||||||
|
|
||||||
var authState = CreateAuthState("Max Mustermann");
|
|
||||||
_authStateMock.Setup(s => s.Value).Returns(authState);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var cut = _ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
|
||||||
|
|
||||||
// Assert - AuthTest component shows user display name
|
|
||||||
cut.Markup.Should().Contain("Max Mustermann");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void HomePage_ShowsAuthTestComponent()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var authContext = _ctx.AddTestAuthorization();
|
|
||||||
authContext.SetAuthorized("testuser");
|
|
||||||
|
|
||||||
_authStateMock.Setup(s => s.Value).Returns(CreateAuthState());
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var cut = _ctx.RenderComponent<Koogle.Web.Components.Pages.Home>();
|
|
||||||
|
|
||||||
// Assert - AuthTest component header is rendered
|
|
||||||
cut.Markup.Should().Contain("AuthTest-Component");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthState CreateAuthState(string displayName = "Test User")
|
private static AuthState CreateAuthState(string displayName = "Test User")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue