Adds dashboard stat cards and demo banner

Introduces StatCard ViewComponent with configurable stat display
Adds demo mode banner to application layout
Refactors dashboard stats to use dynamic component rendering

Improves dashboard presentation and user experience
This commit is contained in:
Janus C. H. Knudsen 2026-01-11 11:17:51 +01:00
parent 217a9cd95c
commit 9b2ace7bc0
5 changed files with 272 additions and 30 deletions

View file

@ -0,0 +1,16 @@
@model PlanTempus.Application.Features.Dashboard.Components.StatCardViewModel
<swp-stat-card data-key="@Model.Key" class="@Model.Variant">
<swp-stat-value>@Model.Value</swp-stat-value>
<swp-stat-label>@Model.Label</swp-stat-label>
@if (Model.HasTrend)
{
<swp-stat-trend class="@Model.TrendDirection">
@if (!string.IsNullOrEmpty(Model.TrendIcon))
{
<i class="ph @Model.TrendIcon"></i>
}
@Model.TrendText
</swp-stat-trend>
}
</swp-stat-card>

View file

@ -0,0 +1,90 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
/// <summary>
/// ViewComponent for rendering a stat card on the dashboard.
/// </summary>
public class StatCardViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = StatCardCatalog.Get(key);
return View(model);
}
}
/// <summary>
/// ViewModel for the StatCard component.
/// </summary>
public class StatCardViewModel
{
public required string Key { get; init; }
public required string Value { get; init; }
public required string Label { get; init; }
public string? TrendText { get; init; }
public string? TrendIcon { get; init; }
public string? TrendDirection { get; init; }
public string? Variant { get; init; }
public bool HasTrend => !string.IsNullOrEmpty(TrendText);
}
/// <summary>
/// Catalog of available stat cards with their data.
/// </summary>
public static class StatCardCatalog
{
private static readonly Dictionary<string, StatCardViewModel> Cards = new()
{
["bookings-today"] = new StatCardViewModel
{
Key = "bookings-today",
Value = "12",
Label = "Bookinger i dag",
TrendText = "4 gennemført, 2 i gang",
TrendIcon = "ph-check-circle",
TrendDirection = "up",
Variant = "highlight"
},
["expected-revenue"] = new StatCardViewModel
{
Key = "expected-revenue",
Value = "8.450 kr",
Label = "Forventet omsætning",
TrendText = "+12% vs. gennemsnit",
TrendIcon = "ph-trend-up",
TrendDirection = "up",
Variant = "success"
},
["occupancy-rate"] = new StatCardViewModel
{
Key = "occupancy-rate",
Value = "78%",
Label = "Belægningsgrad",
TrendText = "God kapacitet",
TrendIcon = "ph-trend-up",
TrendDirection = "up",
Variant = null
},
["needs-attention"] = new StatCardViewModel
{
Key = "needs-attention",
Value = "4",
Label = "Kræver opmærksomhed",
TrendText = null,
TrendIcon = null,
TrendDirection = null,
Variant = "warning"
}
};
public static StatCardViewModel Get(string key)
{
if (Cards.TryGetValue(key, out var card))
return card;
throw new KeyNotFoundException($"StatCard with key '{key}' not found");
}
public static IEnumerable<string> AllKeys => Cards.Keys;
}

View file

@ -1,5 +1,5 @@
@page "/"
@using PlanTempus.Application.Features.Dashboard.Pages
@using PlanTempus.Application.Features.Dashboard.Pages
@model PlanTempus.Application.Features.Dashboard.Pages.IndexModel
@{
ViewData["Title"] = "Dashboard";
@ -8,34 +8,10 @@
<swp-page-container>
<!-- Stats Bar -->
<swp-stats-bar>
<swp-stat-card class="highlight">
<swp-stat-value>12</swp-stat-value>
<swp-stat-label>Bookinger i dag</swp-stat-label>
<swp-stat-trend class="up">
<i class="ph ph-check-circle"></i>
4 gennemført, 2 i gang
</swp-stat-trend>
</swp-stat-card>
<swp-stat-card class="success">
<swp-stat-value>8.450 kr</swp-stat-value>
<swp-stat-label>Forventet omsætning</swp-stat-label>
<swp-stat-trend class="up">
<i class="ph ph-trend-up"></i>
+12% vs. gennemsnit
</swp-stat-trend>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>78%</swp-stat-value>
<swp-stat-label>Belægningsgrad</swp-stat-label>
<swp-stat-trend class="up">
<i class="ph ph-trend-up"></i>
God kapacitet
</swp-stat-trend>
</swp-stat-card>
<swp-stat-card class="warning">
<swp-stat-value>4</swp-stat-value>
<swp-stat-label>Kræver opmærksomhed</swp-stat-label>
</swp-stat-card>
@await Component.InvokeAsync("StatCard", "bookings-today")
@await Component.InvokeAsync("StatCard", "expected-revenue")
@await Component.InvokeAsync("StatCard", "occupancy-rate")
@await Component.InvokeAsync("StatCard", "needs-attention")
</swp-stats-bar>
<!-- Dashboard Content -->