wip
This commit is contained in:
parent
abcf8ee75e
commit
12869e35bf
34 changed files with 1177 additions and 156 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue