diff --git a/.workbench/image.png b/.workbench/image.png new file mode 100644 index 0000000..e1594c1 Binary files /dev/null and b/.workbench/image.png differ diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingItem/Default.cshtml b/PlanTempus.Application/Features/Dashboard/Components/BookingItem/Default.cshtml new file mode 100644 index 0000000..44857f0 --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingItem/Default.cshtml @@ -0,0 +1,17 @@ +@model PlanTempus.Application.Features.Dashboard.Components.BookingItemViewModel + + + + @Model.TimeStart + @Model.TimeEnd + + + + @Model.Service + @Model.CustomerName + + + @Model.EmployeeInitials + + @Model.StatusText + diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingItemViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/BookingItemViewComponent.cs new file mode 100644 index 0000000..4e172f3 --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingItemViewComponent.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PlanTempus.Application.Features.Dashboard.Components; + +public class BookingItemViewComponent : ViewComponent +{ + public IViewComponentResult Invoke(string key) + { + var model = BookingItemCatalog.Get(key); + return View(model); + } +} + +public class BookingItemViewModel +{ + public required string Key { get; init; } + public required string TimeStart { get; init; } + public required string TimeEnd { get; init; } + public required string Service { get; init; } + public required string CustomerName { get; init; } + public required string EmployeeInitials { get; init; } + public required string EmployeeName { get; init; } + public required string Status { get; init; } + public string? IndicatorColor { get; init; } + + public string StatusText => Status switch + { + "completed" => "Gennemført", + "inprogress" => "I gang", + "confirmed" => "Bekræftet", + "pending" => "Afventer", + _ => Status + }; +} + +public static class BookingItemCatalog +{ + private static readonly Dictionary Bookings = new() + { + ["booking-1"] = new BookingItemViewModel + { + Key = "booking-1", + TimeStart = "08:00", + TimeEnd = "08:30", + Service = "Herreklip", + CustomerName = "Thomas Berg", + EmployeeInitials = "MH", + EmployeeName = "Maria Hansen", + Status = "completed" + }, + ["booking-2"] = new BookingItemViewModel + { + Key = "booking-2", + TimeStart = "08:30", + TimeEnd = "09:00", + Service = "Dameklip", + CustomerName = "Katrine Holm", + EmployeeInitials = "AS", + EmployeeName = "Anna Sørensen", + Status = "completed" + }, + ["booking-3"] = new BookingItemViewModel + { + Key = "booking-3", + TimeStart = "09:00", + TimeEnd = "09:30", + Service = "Skægtrimning", + CustomerName = "Mikkel Skov", + EmployeeInitials = "PK", + EmployeeName = "Peter Kristensen", + Status = "completed" + }, + ["booking-4"] = new BookingItemViewModel + { + Key = "booking-4", + TimeStart = "10:30", + TimeEnd = "11:00", + Service = "Herreklip", + CustomerName = "Jonas Petersen", + EmployeeInitials = "MH", + EmployeeName = "Maria Hansen", + Status = "inprogress", + IndicatorColor = "blue" + }, + ["booking-5"] = new BookingItemViewModel + { + Key = "booking-5", + TimeStart = "10:00", + TimeEnd = "11:00", + Service = "Føn + Styling", + CustomerName = "Rikke Dam", + EmployeeInitials = "LJ", + EmployeeName = "Louise Jensen", + Status = "inprogress", + IndicatorColor = "purple" + }, + ["booking-6"] = new BookingItemViewModel + { + Key = "booking-6", + TimeStart = "11:00", + TimeEnd = "12:00", + Service = "Balayage", + CustomerName = "Emma Christensen", + EmployeeInitials = "AS", + EmployeeName = "Anna Sørensen", + Status = "confirmed", + IndicatorColor = "teal" + } + }; + + public static BookingItemViewModel Get(string key) + { + if (Bookings.TryGetValue(key, out var booking)) + return booking; + + throw new KeyNotFoundException($"BookingItem with key '{key}' not found"); + } + + public static IEnumerable AllKeys => Bookings.Keys; +} diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml b/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml new file mode 100644 index 0000000..878b7fd --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml @@ -0,0 +1,23 @@ +@model PlanTempus.Application.Features.Dashboard.Components.BookingListViewModel + + + + + + @Model.Title + + Se alle + + + + Nu: @Model.CurrentTime + + + + @foreach (var bookingKey in Model.BookingKeys) + { + @await Component.InvokeAsync("BookingItem", bookingKey) + } + + + diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingListViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/BookingListViewComponent.cs new file mode 100644 index 0000000..7c99560 --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingListViewComponent.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PlanTempus.Application.Features.Dashboard.Components; + +public class BookingListViewComponent : ViewComponent +{ + public IViewComponentResult Invoke(string key) + { + var model = BookingListCatalog.Get(key); + return View(model); + } +} + +public class BookingListViewModel +{ + public required string Key { get; init; } + public required string Title { get; init; } + public required string CurrentTime { get; init; } + public required IReadOnlyList BookingKeys { get; init; } +} + +public static class BookingListCatalog +{ + private static readonly Dictionary Lists = new() + { + ["todays-bookings"] = new BookingListViewModel + { + Key = "todays-bookings", + Title = "Dagens bookinger", + CurrentTime = "10:45", + BookingKeys = ["booking-1", "booking-2", "booking-3", "booking-4", "booking-5", "booking-6"] + } + }; + + public static BookingListViewModel Get(string key) + { + if (Lists.TryGetValue(key, out var list)) + return list; + + throw new KeyNotFoundException($"BookingList with key '{key}' not found"); + } +} diff --git a/PlanTempus.Application/Features/Dashboard/Components/NotificationItem/Default.cshtml b/PlanTempus.Application/Features/Dashboard/Components/NotificationItem/Default.cshtml new file mode 100644 index 0000000..6372dfd --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/NotificationItem/Default.cshtml @@ -0,0 +1,13 @@ +@model PlanTempus.Application.Features.Dashboard.Components.NotificationItemViewModel + + + + + + + + @Model.Title @Model.Text + + @Model.Time + + diff --git a/PlanTempus.Application/Features/Dashboard/Components/NotificationItemViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/NotificationItemViewComponent.cs new file mode 100644 index 0000000..5372aa9 --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/NotificationItemViewComponent.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PlanTempus.Application.Features.Dashboard.Components; + +public class NotificationItemViewComponent : ViewComponent +{ + public IViewComponentResult Invoke(string key) + { + var model = NotificationItemCatalog.Get(key); + return View(model); + } +} + +public class NotificationItemViewModel +{ + public required string Key { get; init; } + public required string Icon { get; init; } + public required string Title { get; init; } + public required string Text { get; init; } + public required string Time { get; init; } + public bool IsUnread { get; init; } +} + +public static class NotificationItemCatalog +{ + private static readonly Dictionary Notifications = new() + { + ["notif-1"] = new NotificationItemViewModel + { + Key = "notif-1", + Icon = "calendar-plus", + Title = "Ny booking", + Text = "fra Emma Christensen til Balayage", + Time = "For 15 min. siden", + IsUnread = true + }, + ["notif-2"] = new NotificationItemViewModel + { + Key = "notif-2", + Icon = "star", + Title = "Ny anmeldelse", + Text = "5 stjerner fra Sofie Nielsen", + Time = "For 1 time siden", + IsUnread = true + }, + ["notif-3"] = new NotificationItemViewModel + { + Key = "notif-3", + Icon = "x", + Title = "Aflysning", + Text = "Mette Hansen aflyste kl. 15:00", + Time = "For 2 timer siden", + IsUnread = false + }, + ["notif-4"] = new NotificationItemViewModel + { + Key = "notif-4", + Icon = "check", + Title = "Bekræftet", + Text = "Louise Andersen bekræftede kl. 13:00", + Time = "I går kl. 18:30", + IsUnread = false + } + }; + + public static NotificationItemViewModel Get(string key) + { + if (Notifications.TryGetValue(key, out var notification)) + return notification; + + throw new KeyNotFoundException($"NotificationItem with key '{key}' not found"); + } + + public static IEnumerable AllKeys => Notifications.Keys; +} diff --git a/PlanTempus.Application/Features/Dashboard/Components/NotificationList/Default.cshtml b/PlanTempus.Application/Features/Dashboard/Components/NotificationList/Default.cshtml new file mode 100644 index 0000000..9b5b5ab --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/NotificationList/Default.cshtml @@ -0,0 +1,19 @@ +@model PlanTempus.Application.Features.Dashboard.Components.NotificationListViewModel + + + + + + @Model.Title + + @Model.ActionText + + + + @foreach (var notificationKey in Model.NotificationKeys) + { + @await Component.InvokeAsync("NotificationItem", notificationKey) + } + + + diff --git a/PlanTempus.Application/Features/Dashboard/Components/NotificationListViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/NotificationListViewComponent.cs new file mode 100644 index 0000000..b2596f7 --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Components/NotificationListViewComponent.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PlanTempus.Application.Features.Dashboard.Components; + +public class NotificationListViewComponent : ViewComponent +{ + public IViewComponentResult Invoke(string key) + { + var model = NotificationListCatalog.Get(key); + return View(model); + } +} + +public class NotificationListViewModel +{ + public required string Key { get; init; } + public required string Title { get; init; } + public required string ActionText { get; init; } + public required IReadOnlyList NotificationKeys { get; init; } +} + +public static class NotificationListCatalog +{ + private static readonly Dictionary Lists = new() + { + ["recent-notifications"] = new NotificationListViewModel + { + Key = "recent-notifications", + Title = "Notifikationer", + ActionText = "Marker alle som læst", + NotificationKeys = ["notif-1", "notif-2", "notif-3", "notif-4"] + } + }; + + public static NotificationListViewModel Get(string key) + { + if (Lists.TryGetValue(key, out var list)) + return list; + + throw new KeyNotFoundException($"NotificationList with key '{key}' not found"); + } +} diff --git a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml index 92c24f0..5c78b61 100644 --- a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml +++ b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml @@ -31,22 +31,14 @@ - - - - - - Dagens bookinger - - Se alle - - -

Booking oversigt kommer her...

-
-
+ + @await Component.InvokeAsync("BookingList", "todays-bookings") + + @await Component.InvokeAsync("NotificationList", "recent-notifications") + diff --git a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml index bc1b7d0..36be511 100644 --- a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml +++ b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml @@ -18,6 +18,8 @@ + + @await RenderSectionAsync("Styles", required: false) diff --git a/PlanTempus.Application/wwwroot/css/bookings.css b/PlanTempus.Application/wwwroot/css/bookings.css new file mode 100644 index 0000000..695efd9 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/bookings.css @@ -0,0 +1,194 @@ +/** + * Bookings - Booking List & Items + * + * Styling for booking components on dashboard + */ + +/* =========================================== + BOOKING LIST + =========================================== */ +swp-booking-list { + display: grid; + grid-template-columns: 50px 4px 1fr auto auto; + gap: var(--spacing-4); + padding: 0 var(--card-body-padding); +} + +/* =========================================== + BOOKING ITEM + =========================================== */ +swp-booking-item { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; + padding: var(--spacing-4); + background: var(--color-background-alt); + border-radius: var(--radius-lg); + cursor: pointer; + transition: background var(--transition-fast); +} + +swp-booking-item:hover { + background: var(--color-background-hover); +} + +swp-booking-item.completed { + opacity: 0.6; +} + +swp-booking-item.completed swp-booking-indicator { + background: var(--color-border); +} + +swp-booking-item.inprogress { + background: color-mix(in srgb, var(--color-teal) 8%, var(--color-background-alt)); +} + +/* =========================================== + BOOKING TIME + =========================================== */ +swp-booking-time { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +swp-booking-time swp-time-start { + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + font-family: var(--font-mono); + color: var(--color-text); +} + +swp-booking-time swp-time-end { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + font-family: var(--font-mono); +} + +/* =========================================== + BOOKING INDICATOR + =========================================== */ +swp-booking-indicator { + width: 4px; + height: 40px; + border-radius: 2px; +} + +swp-booking-indicator.teal { background: var(--color-teal); } +swp-booking-indicator.blue { background: var(--color-blue); } +swp-booking-indicator.purple { background: var(--color-purple); } +swp-booking-indicator.amber { background: var(--color-amber); } +swp-booking-indicator.green { background: var(--color-green); } + +/* =========================================== + BOOKING DETAILS + =========================================== */ +swp-booking-details { + min-width: 0; + 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 + =========================================== */ +swp-booking-employee { + display: flex; + align-items: center; + gap: var(--spacing-3); +} + +swp-booking-employee swp-avatar-small { + width: 24px; + height: 24px; + border-radius: var(--radius-full); + background: var(--color-teal); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: var(--font-weight-semibold); +} + +swp-booking-employee span { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +/* =========================================== + BOOKING STATUS + =========================================== */ +swp-booking-status { + padding: var(--spacing-2) var(--spacing-4); + border-radius: var(--radius-md); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); +} + +swp-booking-status.confirmed { + background: color-mix(in srgb, var(--color-green) 15%, transparent); + color: var(--color-green); +} + +swp-booking-status.pending { + background: color-mix(in srgb, var(--color-amber) 15%, transparent); + color: var(--color-amber); +} + +swp-booking-status.inprogress, +swp-booking-status.in-progress { + background: color-mix(in srgb, var(--color-blue) 15%, transparent); + color: var(--color-blue); +} + +swp-booking-status.completed { + background: var(--color-background-hover); + color: var(--color-text-secondary); +} + +/* =========================================== + CURRENT TIME INDICATOR + =========================================== */ +swp-current-time { + display: flex; + align-items: center; + gap: var(--spacing-4); + padding: var(--spacing-4) var(--spacing-6); + background: color-mix(in srgb, var(--color-teal) 10%, transparent); + border-radius: var(--radius-lg); + margin: 0 var(--card-body-padding) var(--spacing-4) var(--card-body-padding); +} + +swp-current-time i { + font-size: var(--font-size-lg); + color: var(--color-teal); +} + +swp-current-time span { + font-size: var(--font-size-md); + color: var(--color-teal); + font-weight: var(--font-weight-medium); +} + +swp-current-time swp-time { + font-family: var(--font-mono); + font-weight: var(--font-weight-semibold); +} diff --git a/PlanTempus.Application/wwwroot/css/design-tokens.css b/PlanTempus.Application/wwwroot/css/design-tokens.css index 346e5a9..6718a60 100644 --- a/PlanTempus.Application/wwwroot/css/design-tokens.css +++ b/PlanTempus.Application/wwwroot/css/design-tokens.css @@ -189,6 +189,9 @@ --container-max-width-md: 1200px; --container-max-width-lg: 1400px; + /* -------- Card Spacing -------- */ + --card-body-padding: var(--spacing-5); + /* -------- Calendar Grid -------- */ --hour-height: 64px; --time-axis-width: 60px; diff --git a/PlanTempus.Application/wwwroot/css/notifications.css b/PlanTempus.Application/wwwroot/css/notifications.css new file mode 100644 index 0000000..f16bb76 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/notifications.css @@ -0,0 +1,92 @@ +/** + * Notifications CSS + * + * Styling for notification components on dashboard + */ + +/* =========================================== + NOTIFICATION LIST + =========================================== */ +swp-notification-list { + display: grid; + grid-template-columns: 50px 1fr; + gap: var(--spacing-4) var(--spacing-6); + padding: 0 var(--card-body-padding); +} + +/* =========================================== + NOTIFICATION ITEM + =========================================== */ +swp-notification-item { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; + padding: var(--spacing-5) var(--spacing-6); + background: var(--color-background-alt); + border-radius: var(--radius-xl); + cursor: pointer; + transition: background var(--transition-fast); +} + +swp-notification-item:hover { + background: var(--color-background-hover); +} + +swp-notification-item.unread { + background: color-mix(in srgb, var(--color-teal) 5%, var(--color-background-alt)); +} + +swp-notification-item.unread:hover { + background: var(--color-background-hover); +} + +/* =========================================== + NOTIFICATION ICON + =========================================== */ +swp-notification-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); +} + +swp-notification-item.unread swp-notification-icon { + background: color-mix(in srgb, var(--color-teal) 15%, transparent); + color: var(--color-teal); +} + +/* =========================================== + NOTIFICATION CONTENT + =========================================== */ +swp-notification-content { + display: flex; + flex-direction: column; + 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); +} diff --git a/PlanTempus.Application/wwwroot/css/page.css b/PlanTempus.Application/wwwroot/css/page.css index dceab74..4372d4f 100644 --- a/PlanTempus.Application/wwwroot/css/page.css +++ b/PlanTempus.Application/wwwroot/css/page.css @@ -94,7 +94,7 @@ swp-card-content { =========================================== */ swp-dashboard-grid { display: grid; - grid-template-columns: 1fr 350px; + grid-template-columns: 1fr 380px; gap: var(--spacing-5); }