This commit is contained in:
Janus C. H. Knudsen 2026-01-11 18:18:36 +01:00
parent abcf8ee75e
commit 12869e35bf
34 changed files with 1177 additions and 156 deletions

View file

@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
public class AttentionItemViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = AttentionItemCatalog.Get(key);
return View(model);
}
}
public class AttentionItemViewModel
{
public required string Key { get; init; }
public required string Icon { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public required string ActionText { get; init; }
public required string Severity { get; init; }
}
public static class AttentionItemCatalog
{
private static readonly Dictionary<string, AttentionItemViewModel> Attentions = new()
{
["attention-1"] = new AttentionItemViewModel
{
Key = "attention-1",
Icon = "x-circle",
Title = "Aflyst booking",
Description = "Mette Hansen aflyste kl. 15:00 tid nu ledig",
ActionText = "Fyld tid",
Severity = "urgent"
},
["attention-2"] = new AttentionItemViewModel
{
Key = "attention-2",
Icon = "clock",
Title = "Ubekræftet booking",
Description = "Ida Rasmussen har ikke bekræftet kl. 11:30",
ActionText = "Send påmindelse",
Severity = "warning"
},
["attention-3"] = new AttentionItemViewModel
{
Key = "attention-3",
Icon = "gift",
Title = "Gavekort udløber snart",
Description = "GC-D2R4-6TY9 udløber om 3 uger (200 DKK)",
ActionText = "Se gavekort",
Severity = "info"
}
};
public static AttentionItemViewModel Get(string key)
{
if (Attentions.TryGetValue(key, out var attention))
return attention;
throw new KeyNotFoundException($"AttentionItem with key '{key}' not found");
}
public static IEnumerable<string> AllKeys => Attentions.Keys;
}

View file

@ -0,0 +1,12 @@
@model PlanTempus.Application.Features.Dashboard.Components.AttentionItemViewModel
<swp-attention-item data-key="@Model.Key" class="@Model.Severity">
<swp-attention-icon>
<i class="ph ph-@Model.Icon"></i>
</swp-attention-icon>
<swp-attention-content>
<swp-item-title>@Model.Title</swp-item-title>
<swp-item-desc>@Model.Description</swp-item-desc>
</swp-attention-content>
<swp-attention-action>@Model.ActionText</swp-attention-action>
</swp-attention-item>

View file

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
public class AttentionListViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = AttentionListCatalog.Get(key);
return View(model);
}
}
public class AttentionListViewModel
{
public required string Key { get; init; }
public required string Title { get; init; }
public required IReadOnlyList<string> AttentionKeys { get; init; }
}
public static class AttentionListCatalog
{
private static readonly Dictionary<string, AttentionListViewModel> Lists = new()
{
["current-attentions"] = new AttentionListViewModel
{
Key = "current-attentions",
Title = "Opmærksomheder",
AttentionKeys = ["attention-1", "attention-2", "attention-3"]
}
};
public static AttentionListViewModel Get(string key)
{
if (Lists.TryGetValue(key, out var list))
return list;
throw new KeyNotFoundException($"AttentionList with key '{key}' not found");
}
}

View file

@ -0,0 +1,18 @@
@model PlanTempus.Application.Features.Dashboard.Components.AttentionListViewModel
<swp-card data-key="@Model.Key">
<swp-card-header>
<swp-card-title>
<i class="ph ph-warning-circle"></i>
@Model.Title
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-attention-list>
@foreach (var attentionKey in Model.AttentionKeys)
{
@await Component.InvokeAsync("AttentionItem", attentionKey)
}
</swp-attention-list>
</swp-card-content>
</swp-card>

View file

@ -7,8 +7,8 @@
</swp-booking-time>
<swp-booking-indicator class="@Model.IndicatorColor"></swp-booking-indicator>
<swp-booking-details>
<swp-booking-service>@Model.Service</swp-booking-service>
<swp-booking-customer>@Model.CustomerName</swp-booking-customer>
<swp-item-title>@Model.Service</swp-item-title>
<swp-item-desc>@Model.CustomerName</swp-item-desc>
</swp-booking-details>
<swp-booking-employee title="@Model.EmployeeName">
<swp-avatar-small>@Model.EmployeeInitials</swp-avatar-small>

View file

@ -5,9 +5,9 @@
<i class="ph ph-@Model.Icon"></i>
</swp-notification-icon>
<swp-notification-content>
<swp-notification-text>
<swp-item-title>
<strong>@Model.Title</strong> @Model.Text
</swp-notification-text>
<swp-notification-time>@Model.Time</swp-notification-time>
</swp-item-title>
<swp-item-desc>@Model.Time</swp-item-desc>
</swp-notification-content>
</swp-notification-item>

View file

@ -0,0 +1,6 @@
@model PlanTempus.Application.Features.Dashboard.Components.QuickStatViewModel
<swp-quick-stat data-key="@Model.Key">
<swp-stat-value>@Model.Value</swp-stat-value>
<swp-stat-label>@Model.Label</swp-stat-label>
</swp-quick-stat>

View file

@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
/// <summary>
/// ViewComponent for rendering a quick stat item in the sidebar.
/// </summary>
public class QuickStatViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = QuickStatCatalog.Get(key);
return View(model);
}
}
/// <summary>
/// ViewModel for the QuickStat component.
/// </summary>
public class QuickStatViewModel
{
public required string Key { get; init; }
public required string Value { get; init; }
public required string Label { get; init; }
}
/// <summary>
/// Catalog of available quick stats with their data.
/// </summary>
public static class QuickStatCatalog
{
private static readonly Dictionary<string, QuickStatViewModel> Stats = new()
{
["bookings-week"] = new QuickStatViewModel
{
Key = "bookings-week",
Value = "47",
Label = "Bookinger"
},
["revenue-week"] = new QuickStatViewModel
{
Key = "revenue-week",
Value = "38.200 kr",
Label = "Omsætning"
},
["new-customers"] = new QuickStatViewModel
{
Key = "new-customers",
Value = "8",
Label = "Nye kunder"
},
["avg-occupancy"] = new QuickStatViewModel
{
Key = "avg-occupancy",
Value = "72%",
Label = "Gns. belægning"
}
};
public static QuickStatViewModel Get(string key)
{
if (Stats.TryGetValue(key, out var stat))
return stat;
throw new KeyNotFoundException($"QuickStat with key '{key}' not found");
}
public static IEnumerable<string> AllKeys => Stats.Keys;
}

View file

@ -0,0 +1,18 @@
@model PlanTempus.Application.Features.Dashboard.Components.QuickStatListViewModel
<swp-card data-key="@Model.Key">
<swp-card-header>
<swp-card-title>
<i class="ph ph-@Model.Icon"></i>
@Model.Title
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-quick-stats>
@foreach (var statKey in Model.StatKeys)
{
@await Component.InvokeAsync("QuickStat", statKey)
}
</swp-quick-stats>
</swp-card-content>
</swp-card>

View file

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
public class QuickStatListViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = QuickStatListCatalog.Get(key);
return View(model);
}
}
public class QuickStatListViewModel
{
public required string Key { get; init; }
public required string Title { get; init; }
public required string Icon { get; init; }
public required IReadOnlyList<string> StatKeys { get; init; }
}
public static class QuickStatListCatalog
{
private static readonly Dictionary<string, QuickStatListViewModel> Lists = new()
{
["this-week"] = new QuickStatListViewModel
{
Key = "this-week",
Title = "Denne uge",
Icon = "chart-line-up",
StatKeys = ["bookings-week", "revenue-week", "new-customers", "avg-occupancy"]
}
};
public static QuickStatListViewModel Get(string key)
{
if (Lists.TryGetValue(key, out var list))
return list;
throw new KeyNotFoundException($"QuickStatList with key '{key}' not found");
}
}

View file

@ -0,0 +1,9 @@
@model PlanTempus.Application.Features.Dashboard.Components.WaitlistCardViewModel
<swp-waitlist-card data-key="@Model.Key" data-drawer-trigger="@Model.DrawerTarget">
<swp-waitlist-icon>
<i class="ph ph-@Model.Icon"></i>
<swp-waitlist-badge>@Model.Count</swp-waitlist-badge>
</swp-waitlist-icon>
<swp-waitlist-label>@Model.Label</swp-waitlist-label>
</swp-waitlist-card>

View file

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
/// <summary>
/// ViewComponent for rendering the waitlist mini card on the dashboard.
/// Displays a count badge and triggers the waitlist drawer when clicked.
/// </summary>
public class WaitlistCardViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = WaitlistCardCatalog.Get(key);
return View(model);
}
}
/// <summary>
/// ViewModel for the WaitlistCard component.
/// </summary>
public class WaitlistCardViewModel
{
public required string Key { get; init; }
public required string Label { get; init; }
public required string Icon { get; init; }
public required int Count { get; init; }
public required string DrawerTarget { get; init; }
}
/// <summary>
/// Catalog of waitlist cards with their data.
/// </summary>
public static class WaitlistCardCatalog
{
private static readonly Dictionary<string, WaitlistCardViewModel> Cards = new()
{
["waitlist"] = new WaitlistCardViewModel
{
Key = "waitlist",
Label = "På venteliste",
Icon = "users-three",
Count = 4,
DrawerTarget = "waitlist-drawer"
}
};
public static WaitlistCardViewModel Get(string key)
{
if (Cards.TryGetValue(key, out var card))
return card;
throw new KeyNotFoundException($"WaitlistCard with key '{key}' not found");
}
}

View file

@ -0,0 +1,41 @@
@model PlanTempus.Application.Features.Dashboard.Components.WaitlistItemViewModel
<swp-waitlist-item data-key="@Model.Key">
<swp-waitlist-customer>
<swp-avatar>@Model.CustomerInitials</swp-avatar>
<swp-waitlist-customer-info>
<swp-waitlist-name>@Model.CustomerName</swp-waitlist-name>
<swp-waitlist-phone>@Model.CustomerPhone</swp-waitlist-phone>
</swp-waitlist-customer-info>
</swp-waitlist-customer>
<swp-waitlist-service>@Model.Service</swp-waitlist-service>
<swp-waitlist-meta>
<swp-waitlist-periods>
<swp-label>Ønsker:</swp-label>
@foreach (var period in Model.PreferredPeriods)
{
<swp-waitlist-period-tag>@period</swp-waitlist-period-tag>
}
</swp-waitlist-periods>
<swp-waitlist-dates>
<swp-waitlist-date>
<i class="ph ph-calendar"></i>
Tilmeldt: @Model.RegisteredDate
</swp-waitlist-date>
<swp-waitlist-date class="expires @(Model.ExpiresSoon ? "soon" : "")">
<i class="ph ph-clock"></i>
Udløber: @Model.ExpiresDate
</swp-waitlist-date>
</swp-waitlist-dates>
</swp-waitlist-meta>
<swp-waitlist-actions>
<swp-btn class="secondary">
<i class="ph ph-phone"></i>
Kontakt
</swp-btn>
<swp-btn class="primary">
<i class="ph ph-calendar-plus"></i>
Book
</swp-btn>
</swp-waitlist-actions>
</swp-waitlist-item>

View file

@ -0,0 +1,99 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Dashboard.Components;
/// <summary>
/// ViewComponent for rendering a waitlist item in the waitlist drawer.
/// </summary>
public class WaitlistItemViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string key)
{
var model = WaitlistItemCatalog.Get(key);
return View(model);
}
}
/// <summary>
/// ViewModel for the WaitlistItem component.
/// </summary>
public class WaitlistItemViewModel
{
public required string Key { get; init; }
public required string CustomerName { get; init; }
public required string CustomerInitials { get; init; }
public required string CustomerPhone { get; init; }
public required string Service { get; init; }
public required IReadOnlyList<string> PreferredPeriods { get; init; }
public required string RegisteredDate { get; init; }
public required string ExpiresDate { get; init; }
public bool ExpiresSoon { get; init; }
}
/// <summary>
/// Catalog of waitlist items with demo data.
/// </summary>
public static class WaitlistItemCatalog
{
private static readonly Dictionary<string, WaitlistItemViewModel> Items = new()
{
["waitlist-1"] = new WaitlistItemViewModel
{
Key = "waitlist-1",
CustomerName = "Emma Christensen",
CustomerInitials = "EC",
CustomerPhone = "+45 12 34 56 78",
Service = "Dameklip + Farve",
PreferredPeriods = ["Mandag-Onsdag", "Formiddag"],
RegisteredDate = "2. jan 2026",
ExpiresDate = "16. jan 2026",
ExpiresSoon = false
},
["waitlist-2"] = new WaitlistItemViewModel
{
Key = "waitlist-2",
CustomerName = "Mikkel Sørensen",
CustomerInitials = "MS",
CustomerPhone = "+45 23 45 67 89",
Service = "Herreklip",
PreferredPeriods = ["Weekend"],
RegisteredDate = "30. dec 2025",
ExpiresDate = "6. jan 2026",
ExpiresSoon = true
},
["waitlist-3"] = new WaitlistItemViewModel
{
Key = "waitlist-3",
CustomerName = "Lise Andersen",
CustomerInitials = "LA",
CustomerPhone = "+45 34 56 78 90",
Service = "Balayage",
PreferredPeriods = ["Tirsdag-Torsdag", "Eftermiddag"],
RegisteredDate = "28. dec 2025",
ExpiresDate = "11. jan 2026",
ExpiresSoon = false
},
["waitlist-4"] = new WaitlistItemViewModel
{
Key = "waitlist-4",
CustomerName = "Peter Hansen",
CustomerInitials = "PH",
CustomerPhone = "+45 45 67 89 01",
Service = "Herreklip + Skæg",
PreferredPeriods = ["Fleksibel"],
RegisteredDate = "27. dec 2025",
ExpiresDate = "10. jan 2026",
ExpiresSoon = false
}
};
public static WaitlistItemViewModel Get(string key)
{
if (Items.TryGetValue(key, out var item))
return item;
throw new KeyNotFoundException($"WaitlistItem with key '{key}' not found");
}
public static IEnumerable<string> AllKeys => Items.Keys;
}

View file

@ -33,12 +33,21 @@
<!-- Today's Bookings -->
@await Component.InvokeAsync("BookingList", "todays-bookings")
<!-- Attention Items -->
@await Component.InvokeAsync("AttentionList", "current-attentions")
</swp-main-column>
<swp-side-column>
<!-- Notifications -->
@await Component.InvokeAsync("NotificationList", "recent-notifications")
<!-- Waitlist Card -->
@await Component.InvokeAsync("WaitlistCard", "waitlist")
<!-- Quick Stats (This Week) -->
@await Component.InvokeAsync("QuickStatList", "this-week")
<!-- Quick Actions -->
<swp-card>
<swp-card-header>

View file

@ -0,0 +1,21 @@
@using PlanTempus.Application.Features.Dashboard.Components
<swp-waitlist-drawer data-drawer="lg" id="waitlist-drawer">
<swp-drawer-header>
<swp-drawer-title>
Venteliste <swp-count>(@WaitlistItemCatalog.AllKeys.Count())</swp-count>
</swp-drawer-title>
<swp-drawer-close data-drawer-close>
<i class="ph ph-x"></i>
</swp-drawer-close>
</swp-drawer-header>
<swp-drawer-body>
<swp-waitlist-list>
@foreach (var key in WaitlistItemCatalog.AllKeys)
{
@await Component.InvokeAsync("WaitlistItem", key)
}
</swp-waitlist-list>
</swp-drawer-body>
</swp-waitlist-drawer>

View file

@ -20,6 +20,9 @@
<link rel="stylesheet" href="~/css/stats.css">
<link rel="stylesheet" href="~/css/bookings.css">
<link rel="stylesheet" href="~/css/notifications.css">
<link rel="stylesheet" href="~/css/attentions.css">
<link rel="stylesheet" href="~/css/quick-stats.css">
<link rel="stylesheet" href="~/css/waitlist.css">
@await RenderSectionAsync("Styles", required: false)
</head>
<body class="has-demo-banner">
@ -47,6 +50,7 @@
</swp-app-layout>
<partial name="_ProfileDrawer" />
<partial name="_WaitlistDrawer" />
<swp-drawer-overlay id="drawerOverlay"></swp-drawer-overlay>
<script type="module" src="~/js/app.js"></script>