wip
This commit is contained in:
parent
abcf8ee75e
commit
12869e35bf
34 changed files with 1177 additions and 156 deletions
15
.hintrc
Normal file
15
.hintrc
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"development"
|
||||||
|
],
|
||||||
|
"hints": {
|
||||||
|
"compat-api/css": [
|
||||||
|
"default",
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"grid-template-columns: subgrid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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-time>
|
||||||
<swp-booking-indicator class="@Model.IndicatorColor"></swp-booking-indicator>
|
<swp-booking-indicator class="@Model.IndicatorColor"></swp-booking-indicator>
|
||||||
<swp-booking-details>
|
<swp-booking-details>
|
||||||
<swp-booking-service>@Model.Service</swp-booking-service>
|
<swp-item-title>@Model.Service</swp-item-title>
|
||||||
<swp-booking-customer>@Model.CustomerName</swp-booking-customer>
|
<swp-item-desc>@Model.CustomerName</swp-item-desc>
|
||||||
</swp-booking-details>
|
</swp-booking-details>
|
||||||
<swp-booking-employee title="@Model.EmployeeName">
|
<swp-booking-employee title="@Model.EmployeeName">
|
||||||
<swp-avatar-small>@Model.EmployeeInitials</swp-avatar-small>
|
<swp-avatar-small>@Model.EmployeeInitials</swp-avatar-small>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
<i class="ph ph-@Model.Icon"></i>
|
<i class="ph ph-@Model.Icon"></i>
|
||||||
</swp-notification-icon>
|
</swp-notification-icon>
|
||||||
<swp-notification-content>
|
<swp-notification-content>
|
||||||
<swp-notification-text>
|
<swp-item-title>
|
||||||
<strong>@Model.Title</strong> @Model.Text
|
<strong>@Model.Title</strong> @Model.Text
|
||||||
</swp-notification-text>
|
</swp-item-title>
|
||||||
<swp-notification-time>@Model.Time</swp-notification-time>
|
<swp-item-desc>@Model.Time</swp-item-desc>
|
||||||
</swp-notification-content>
|
</swp-notification-content>
|
||||||
</swp-notification-item>
|
</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 -->
|
<!-- Today's Bookings -->
|
||||||
@await Component.InvokeAsync("BookingList", "todays-bookings")
|
@await Component.InvokeAsync("BookingList", "todays-bookings")
|
||||||
|
|
||||||
|
<!-- Attention Items -->
|
||||||
|
@await Component.InvokeAsync("AttentionList", "current-attentions")
|
||||||
</swp-main-column>
|
</swp-main-column>
|
||||||
|
|
||||||
<swp-side-column>
|
<swp-side-column>
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
@await Component.InvokeAsync("NotificationList", "recent-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 -->
|
<!-- Quick Actions -->
|
||||||
<swp-card>
|
<swp-card>
|
||||||
<swp-card-header>
|
<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/stats.css">
|
||||||
<link rel="stylesheet" href="~/css/bookings.css">
|
<link rel="stylesheet" href="~/css/bookings.css">
|
||||||
<link rel="stylesheet" href="~/css/notifications.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)
|
@await RenderSectionAsync("Styles", required: false)
|
||||||
</head>
|
</head>
|
||||||
<body class="has-demo-banner">
|
<body class="has-demo-banner">
|
||||||
|
|
@ -47,6 +50,7 @@
|
||||||
</swp-app-layout>
|
</swp-app-layout>
|
||||||
|
|
||||||
<partial name="_ProfileDrawer" />
|
<partial name="_ProfileDrawer" />
|
||||||
|
<partial name="_WaitlistDrawer" />
|
||||||
<swp-drawer-overlay id="drawerOverlay"></swp-drawer-overlay>
|
<swp-drawer-overlay id="drawerOverlay"></swp-drawer-overlay>
|
||||||
|
|
||||||
<script type="module" src="~/js/app.js"></script>
|
<script type="module" src="~/js/app.js"></script>
|
||||||
|
|
|
||||||
114
PlanTempus.Application/wwwroot/css/attentions.css
Normal file
114
PlanTempus.Application/wwwroot/css/attentions.css
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* Attentions CSS
|
||||||
|
*
|
||||||
|
* Styling for attention/alert components on dashboard
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
ATTENTION LIST
|
||||||
|
=========================================== */
|
||||||
|
swp-attention-list {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
ATTENTION ITEM
|
||||||
|
=========================================== */
|
||||||
|
swp-attention-item {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: subgrid;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
padding: var(--spacing-5) var(--spacing-6);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
border-left: 3px solid var(--color-border);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-attention-item:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Severity: Urgent (red) */
|
||||||
|
swp-attention-item.urgent {
|
||||||
|
border-left-color: var(--color-red);
|
||||||
|
background: color-mix(in srgb, var(--color-red) 5%, var(--color-background-alt));
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-attention-item.urgent:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-red) 8%, var(--color-background-alt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Severity: Warning (amber) */
|
||||||
|
swp-attention-item.warning {
|
||||||
|
border-left-color: var(--color-amber);
|
||||||
|
background: color-mix(in srgb, var(--color-amber) 5%, var(--color-background-alt));
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-attention-item.warning:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-amber) 8%, var(--color-background-alt));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Severity: Info (blue) */
|
||||||
|
swp-attention-item.info {
|
||||||
|
border-left-color: var(--color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
ATTENTION ICON
|
||||||
|
=========================================== */
|
||||||
|
swp-attention-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon colors per severity */
|
||||||
|
swp-attention-item.urgent swp-attention-icon {
|
||||||
|
background: color-mix(in srgb, var(--color-red) 15%, transparent);
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-attention-item.warning swp-attention-icon {
|
||||||
|
background: color-mix(in srgb, var(--color-amber) 15%, transparent);
|
||||||
|
color: var(--color-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-attention-item.info swp-attention-icon {
|
||||||
|
background: color-mix(in srgb, var(--color-blue) 15%, transparent);
|
||||||
|
color: var(--color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
ATTENTION CONTENT
|
||||||
|
=========================================== */
|
||||||
|
swp-attention-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
ATTENTION ACTION
|
||||||
|
=========================================== */
|
||||||
|
swp-attention-action {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-teal);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: text-decoration var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-attention-action:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,7 @@
|
||||||
BOOKING LIST
|
BOOKING LIST
|
||||||
=========================================== */
|
=========================================== */
|
||||||
swp-booking-list {
|
swp-booking-list {
|
||||||
display: grid;
|
display: contents;
|
||||||
grid-template-columns: 50px 4px 1fr auto auto;
|
|
||||||
gap: var(--spacing-4);
|
|
||||||
padding: 0 var(--card-body-padding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
|
|
@ -90,22 +87,6 @@ swp-booking-details {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-booking-service {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--color-text);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-booking-customer {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
BOOKING EMPLOYEE
|
BOOKING EMPLOYEE
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
@ -174,7 +155,7 @@ swp-current-time {
|
||||||
padding: var(--spacing-4) var(--spacing-6);
|
padding: var(--spacing-4) var(--spacing-6);
|
||||||
background: color-mix(in srgb, var(--color-teal) 10%, transparent);
|
background: color-mix(in srgb, var(--color-teal) 10%, transparent);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
margin: 0 var(--card-body-padding) var(--spacing-4) var(--card-body-padding);
|
margin-bottom: var(--spacing-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-current-time i {
|
swp-current-time i {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,36 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
BASE DRAWER
|
BASE DRAWER (Generic)
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
[data-drawer] {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: var(--drawer-width, 320px);
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: var(--z-drawer);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-drawer].active,
|
||||||
|
[data-drawer].open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drawer width variants */
|
||||||
|
[data-drawer="sm"] { --drawer-width: 280px; }
|
||||||
|
[data-drawer="md"] { --drawer-width: 360px; }
|
||||||
|
[data-drawer="lg"] { --drawer-width: 420px; }
|
||||||
|
[data-drawer="xl"] { --drawer-width: 480px; }
|
||||||
|
|
||||||
|
/* Legacy support for existing drawers */
|
||||||
swp-profile-drawer,
|
swp-profile-drawer,
|
||||||
swp-notification-drawer,
|
swp-notification-drawer,
|
||||||
swp-todo-drawer {
|
swp-todo-drawer {
|
||||||
|
|
@ -38,17 +66,26 @@ swp-drawer-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: var(--spacing-4) var(--spacing-5);
|
padding: var(--spacing-10) var(--spacing-12);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-drawer-title {
|
swp-drawer-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
font-weight: 600;
|
font-weight: var(--font-weight-semibold);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swp-drawer-title swp-count {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
swp-drawer-close {
|
swp-drawer-close {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
@ -56,8 +93,8 @@ swp-drawer-close {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: var(--color-background-alt);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--radius-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
|
|
@ -73,12 +110,13 @@ swp-drawer-close i {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
DRAWER CONTENT
|
DRAWER CONTENT / BODY
|
||||||
=========================================== */
|
=========================================== */
|
||||||
swp-drawer-content {
|
swp-drawer-content,
|
||||||
|
swp-drawer-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--spacing-5);
|
padding: var(--spacing-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-drawer-divider {
|
swp-drawer-divider {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,7 @@
|
||||||
NOTIFICATION LIST
|
NOTIFICATION LIST
|
||||||
=========================================== */
|
=========================================== */
|
||||||
swp-notification-list {
|
swp-notification-list {
|
||||||
display: grid;
|
display: contents;
|
||||||
grid-template-columns: 50px 1fr;
|
|
||||||
gap: var(--spacing-4) var(--spacing-6);
|
|
||||||
padding: 0 var(--card-body-padding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
|
|
@ -70,23 +67,3 @@ swp-notification-content {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-notification-text {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
color: var(--color-text);
|
|
||||||
line-height: var(--line-height-snug);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-notification-text strong {
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-notification-time {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
margin-top: var(--spacing-1);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ swp-card {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: var(--spacing-5);
|
||||||
margin-bottom: var(--spacing-5);
|
margin-bottom: var(--spacing-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,6 +57,7 @@ swp-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin: calc(-1 * var(--spacing-5)) calc(-1 * var(--spacing-5)) var(--spacing-4) calc(-1 * var(--spacing-5));
|
||||||
padding: var(--spacing-4) var(--spacing-5);
|
padding: var(--spacing-4) var(--spacing-5);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +88,44 @@ swp-card-action:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-card-content {
|
swp-card-content {
|
||||||
padding: var(--spacing-5);
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card content with grid lists - auto-detect via :has() */
|
||||||
|
swp-card-content:has(> swp-booking-list),
|
||||||
|
swp-card-content:has(> swp-notification-list),
|
||||||
|
swp-card-content:has(> swp-attention-list) {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card-content:has(> swp-booking-list) {
|
||||||
|
grid-template-columns: 50px 4px 1fr auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card-content:has(> swp-notification-list) {
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card-content:has(> swp-attention-list) {
|
||||||
|
grid-template-columns: 50px 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic list item title & description */
|
||||||
|
swp-item-title {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-item-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
|
|
@ -98,14 +137,11 @@ swp-dashboard-grid {
|
||||||
gap: var(--spacing-5);
|
gap: var(--spacing-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-main-column {
|
swp-main-column,
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-side-column {
|
swp-side-column {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
gap: var(--spacing-5);
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
|
|
|
||||||
38
PlanTempus.Application/wwwroot/css/quick-stats.css
Normal file
38
PlanTempus.Application/wwwroot/css/quick-stats.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Quick Stats CSS
|
||||||
|
*
|
||||||
|
* Styling for quick stats components in sidebar
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
QUICK STATS CONTAINER
|
||||||
|
=========================================== */
|
||||||
|
swp-quick-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
QUICK STAT ITEM
|
||||||
|
=========================================== */
|
||||||
|
swp-quick-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-1);
|
||||||
|
padding: var(--spacing-3);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-quick-stat swp-stat-value {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-quick-stat swp-stat-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
@ -182,33 +182,6 @@ swp-stat-card.highlight.filled swp-stat-change {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
|
||||||
QUICK STATS (Compact Variant)
|
|
||||||
=========================================== */
|
|
||||||
swp-quick-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: var(--spacing-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-quick-stat {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-3);
|
|
||||||
background: var(--color-background);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-quick-stat swp-stat-value {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-quick-stat swp-stat-label {
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
margin-top: var(--spacing-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
STAT ITEM (Inline Variant)
|
STAT ITEM (Inline Variant)
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
|
||||||
250
PlanTempus.Application/wwwroot/css/waitlist.css
Normal file
250
PlanTempus.Application/wwwroot/css/waitlist.css
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
/**
|
||||||
|
* Waitlist CSS
|
||||||
|
*
|
||||||
|
* Styling for waitlist mini card and drawer items
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
WAITLIST MINI CARD
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-card:hover {
|
||||||
|
border-color: var(--color-teal);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-icon {
|
||||||
|
position: relative;
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 var(--spacing-2);
|
||||||
|
background: var(--color-teal);
|
||||||
|
color: white;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-label {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
WAITLIST LIST
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
WAITLIST ITEM
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-6);
|
||||||
|
padding: var(--spacing-8);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
CUSTOMER SECTION
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-customer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-customer swp-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-teal);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-customer-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-name {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-phone {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
SERVICE TAG
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-service {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-teal);
|
||||||
|
padding: var(--spacing-2) var(--spacing-3);
|
||||||
|
background: color-mix(in srgb, var(--color-teal) 10%, transparent);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
META SECTION (Preferences & Dates)
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-periods {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-periods swp-label {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-period-tag {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
padding: var(--spacing-1) var(--spacing-2);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-dates {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-date {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-date i {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-date.expires.soon {
|
||||||
|
color: var(--color-amber);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
ACTION BUTTONS
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
padding-top: var(--spacing-4);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-actions swp-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
padding: var(--spacing-4) var(--spacing-6);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-actions swp-btn.secondary {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-actions swp-btn.secondary:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-actions swp-btn.primary {
|
||||||
|
background: var(--color-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-actions swp-btn.primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
EMPTY STATE
|
||||||
|
=========================================== */
|
||||||
|
swp-waitlist-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-10) var(--spacing-6);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-empty i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: var(--color-border);
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-waitlist-empty span {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
7
PlanTempus.Application/wwwroot/js/app.js.map
Normal file
7
PlanTempus.Application/wwwroot/js/app.js.map
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -13,6 +13,7 @@ export class DrawerController {
|
||||||
private newTodoDrawer: HTMLElement | null = null;
|
private newTodoDrawer: HTMLElement | null = null;
|
||||||
private overlay: HTMLElement | null = null;
|
private overlay: HTMLElement | null = null;
|
||||||
private activeDrawer: DrawerName | null = null;
|
private activeDrawer: DrawerName | null = null;
|
||||||
|
private activeGenericDrawer: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.profileDrawer = document.getElementById('profileDrawer');
|
this.profileDrawer = document.getElementById('profileDrawer');
|
||||||
|
|
@ -22,6 +23,7 @@ export class DrawerController {
|
||||||
this.overlay = document.getElementById('drawerOverlay');
|
this.overlay = document.getElementById('drawerOverlay');
|
||||||
|
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
|
this.setupGenericDrawers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -71,11 +73,37 @@ export class DrawerController {
|
||||||
[this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer]
|
[this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer]
|
||||||
.forEach(drawer => drawer?.classList.remove('active'));
|
.forEach(drawer => drawer?.classList.remove('active'));
|
||||||
|
|
||||||
|
// Close any generic drawers
|
||||||
|
this.closeGenericDrawer();
|
||||||
|
|
||||||
this.overlay?.classList.remove('active');
|
this.overlay?.classList.remove('active');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
this.activeDrawer = null;
|
this.activeDrawer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a generic drawer by ID
|
||||||
|
*/
|
||||||
|
openGenericDrawer(drawerId: string): void {
|
||||||
|
this.closeAll();
|
||||||
|
|
||||||
|
const drawer = document.getElementById(drawerId);
|
||||||
|
if (drawer && this.overlay) {
|
||||||
|
drawer.classList.add('open');
|
||||||
|
this.overlay.classList.add('active');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
this.activeGenericDrawer = drawer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the currently open generic drawer
|
||||||
|
*/
|
||||||
|
closeGenericDrawer(): void {
|
||||||
|
this.activeGenericDrawer?.classList.remove('open');
|
||||||
|
this.activeGenericDrawer = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open profile drawer
|
* Open profile drawer
|
||||||
*/
|
*/
|
||||||
|
|
@ -223,4 +251,35 @@ export class DrawerController {
|
||||||
option.classList.add('active');
|
option.classList.add('active');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup generic drawer triggers and close buttons
|
||||||
|
* Uses data-drawer-trigger="drawer-id" and data-drawer-close attributes
|
||||||
|
*/
|
||||||
|
private setupGenericDrawers(): void {
|
||||||
|
// Handle drawer triggers
|
||||||
|
document.addEventListener('click', (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const trigger = target.closest<HTMLElement>('[data-drawer-trigger]');
|
||||||
|
|
||||||
|
if (trigger) {
|
||||||
|
const drawerId = trigger.dataset.drawerTrigger;
|
||||||
|
if (drawerId) {
|
||||||
|
this.openGenericDrawer(drawerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle drawer close buttons
|
||||||
|
document.addEventListener('click', (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const closeBtn = target.closest<HTMLElement>('[data-drawer-close]');
|
||||||
|
|
||||||
|
if (closeBtn) {
|
||||||
|
this.closeGenericDrawer();
|
||||||
|
this.overlay?.classList.remove('active');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue