From ef174af0e1fc910ca82aecb5b154bda7446cdcee Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Mon, 12 Jan 2026 15:42:18 +0100 Subject: [PATCH] Adds localization support across application views Implements localization for dashboard, cash register, account, and profile sections Adds localization keys for various UI elements, improving internationalization support Refactors view components to use ILocalizationService for dynamic text rendering Prepares ground for multi-language support with translation-ready markup --- .../Components/InvoiceHistory/Default.cshtml | 30 +-- .../Components/PaymentMethod/Default.cshtml | 14 +- .../SubscriptionPlans/Default.cshtml | 14 +- .../SubscriptionPlansViewComponent.cs | 9 + .../Features/Account/Pages/Index.cshtml | 8 +- .../Components/CashApproval/Default.cshtml | 18 +- .../Components/CashBalance/Default.cshtml | 20 +- .../Components/CashDifference/Default.cshtml | 2 +- .../Components/PeriodInfo/Default.cshtml | 9 +- .../Components/PeriodRevenue/Default.cshtml | 12 +- .../ReconciliationNote/Default.cshtml | 6 +- .../ReconciliationTable/Default.cshtml | 54 ++--- .../RegisterFilterBar/Default.cshtml | 18 +- .../Features/CashRegister/Pages/Index.cshtml | 22 +- .../AttentionListViewComponent.cs | 36 +++- .../BookingItem/BookingItemViewComponent.cs | 77 +++++-- .../BookingList/BookingListViewComponent.cs | 38 +++- .../Components/BookingList/Default.cshtml | 4 +- .../NotificationListViewComponent.cs | 40 +++- .../QuickStat/QuickStatViewComponent.cs | 48 +++-- .../QuickStatListViewComponent.cs | 38 +++- .../StatCard/StatCardViewComponent.cs | 67 +++++-- .../Features/Dashboard/Pages/Index.cshtml | 8 +- .../Localization/Translations/da.json | 189 +++++++++++++++++- .../Localization/Translations/en.json | 189 +++++++++++++++++- .../Features/Shared/_WaitlistDrawer.cshtml | 2 +- .../_Shared/Pages/_ProfileDrawer.cshtml | 10 +- .../wwwroot/css/account.css | 38 ++-- .../wwwroot/css/attentions.css | 2 +- .../wwwroot/css/bookings.css | 2 +- .../wwwroot/css/design-tokens.css | 5 +- .../wwwroot/css/notifications.css | 2 +- PlanTempus.Application/wwwroot/css/page.css | 27 ++- .../wwwroot/css/quick-stats.css | 6 +- PlanTempus.Application/wwwroot/css/stats.css | 16 +- .../wwwroot/css/waitlist.css | 4 +- 36 files changed, 821 insertions(+), 263 deletions(-) diff --git a/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml b/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml index 9fd16d3..915919b 100644 --- a/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml +++ b/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml @@ -1,15 +1,15 @@ - Faktura-historik + Faktura-historik - Dato - Fakturanr. - Beløb - Status + Dato + Fakturanr. + Beløb + Status @@ -21,13 +21,13 @@ - PDF + PDF @@ -39,13 +39,13 @@ - PDF + PDF @@ -57,13 +57,13 @@ - PDF + PDF @@ -75,13 +75,13 @@ - PDF + PDF @@ -93,13 +93,13 @@ - PDF + PDF diff --git a/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml b/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml index d748aa1..1336b2d 100644 --- a/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml +++ b/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml @@ -9,32 +9,32 @@ - Skift + Skift - Betalingsfrekvens - Månedlig + Betalingsfrekvens + Månedlig - Næste betaling + Næste betaling 1. februar 2026 - Beløb + Beløb 599,00 kr - Kortudløb + Kortudløb 08/2027 - Skift til årlig betaling (spar 15%) + Skift til årlig betaling (spar 15%) diff --git a/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml index 7464bb3..fc0c4be 100644 --- a/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml +++ b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml @@ -1,8 +1,10 @@ @using PlanTempus.Application.Features.Accounts.Models +@using PlanTempus.Application.Features.Localization.Services @model IEnumerable @{ var currentPlanKey = (string)ViewBag.CurrentPlanKey; + var L = (ILocalizationService)ViewBag.Localization; } @@ -16,14 +18,14 @@ }; var badgeClass = isCurrent ? "current" : plan.BadgeClass; - var badgeText = isCurrent ? "Nuværende plan" : plan.BadgeText; + var badgeText = isCurrent ? L.Get("account.subscription.currentPlan") : plan.BadgeText; var badgeIcon = isCurrent ? "ph-check" : plan.BadgeIcon; var buttonText = isCurrent - ? "Nuværende plan" + ? L.Get("account.subscription.currentPlan") : plan.IsContactSales - ? "Kontakt salg" - : $"Skift til {plan.Name}"; + ? L.Get("account.subscription.contactSales") + : L.Get("account.subscription.switchTo").Replace("{plan}", plan.Name); var buttonClass = isCurrent ? "secondary" @@ -42,11 +44,11 @@ @if (plan.PricePerMonth.HasValue) { @plan.PriceDisplay - kr/md + kr/md } else { - Kontakt os + Kontakt os } diff --git a/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs index c0ad730..1b09297 100644 --- a/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs +++ b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using PlanTempus.Application.Features.Accounts.Models; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Account.Components; @@ -9,6 +10,13 @@ namespace PlanTempus.Application.Features.Account.Components; /// public class SubscriptionPlansViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public SubscriptionPlansViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke() { var plans = PlanCatalog.GetAllPlans(); @@ -16,6 +24,7 @@ public class SubscriptionPlansViewComponent : ViewComponent var currentPlanKey = "pro"; ViewBag.CurrentPlanKey = currentPlanKey; + ViewBag.Localization = _localization; return View(plans); } } diff --git a/PlanTempus.Application/Features/Account/Pages/Index.cshtml b/PlanTempus.Application/Features/Account/Pages/Index.cshtml index 13b2920..e48143b 100644 --- a/PlanTempus.Application/Features/Account/Pages/Index.cshtml +++ b/PlanTempus.Application/Features/Account/Pages/Index.cshtml @@ -10,8 +10,8 @@ -

Abonnement & Konto

-

Administrer dit abonnement og betalingsinfo

+

Abonnement & Konto

+

Administrer dit abonnement og betalingsinfo

@@ -20,7 +20,7 @@ - Dit abonnement + Dit abonnement @@ -32,7 +32,7 @@ - Betaling & Fakturaer + Betaling & Fakturaer diff --git a/PlanTempus.Application/Features/CashRegister/Components/CashApproval/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/CashApproval/Default.cshtml index 4f8ca39..1be609a 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/CashApproval/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/CashApproval/Default.cshtml @@ -1,21 +1,21 @@ - Afslut dagen + Afslut dagen - Status + Status - Kladde + Kladde - Godkendt af (valgfrit) + Godkendt af (valgfrit) @@ -24,15 +24,15 @@ - + - Gem som kladde + Gem som kladde - Fortryd - Godkend & lås + Fortryd + Godkend & lås diff --git a/PlanTempus.Application/Features/CashRegister/Components/CashBalance/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/CashBalance/Default.cshtml index 9b339fc..d6f3cc1 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/CashBalance/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/CashBalance/Default.cshtml @@ -1,20 +1,20 @@ - Kontanter i kassen + Kontanter i kassen - Startbeholdning - Overført fra sidste afstemning + Startbeholdning + Overført fra sidste afstemning 2.000,00 - Udbetalinger / Bilag - Sammentæl bilag betalt kontant + Udbetalinger / Bilag + Sammentæl bilag betalt kontant @@ -23,8 +23,8 @@ - Udtaget til bank - Kontanter lagt til side + Udtaget til bank + Kontanter lagt til side @@ -33,15 +33,15 @@ - Forventet kontantbeholdning + Forventet kontantbeholdning 5.220,00 - Optalt kontantbeholdning * - Hvad ligger der faktisk i kassen? + Optalt kontantbeholdning * + Hvad ligger der faktisk i kassen? diff --git a/PlanTempus.Application/Features/CashRegister/Components/CashDifference/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/CashDifference/Default.cshtml index b0a8b4d..c2520f9 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/CashDifference/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/CashDifference/Default.cshtml @@ -1,6 +1,6 @@ - Kontant difference + Kassedifference Optalt minus forventet – kr diff --git a/PlanTempus.Application/Features/CashRegister/Components/PeriodInfo/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/PeriodInfo/Default.cshtml index 94feb27..8956a97 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/PeriodInfo/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/PeriodInfo/Default.cshtml @@ -1,11 +1,10 @@ - Periodeoplysninger - Identificér afstemningen + Periodeoplysninger - Periode + Periode 28. dec 2025 kl. 18:00 @@ -15,7 +14,7 @@ - Kassepunkt + Kassepunkt diff --git a/PlanTempus.Application/Features/CashRegister/Components/PeriodRevenue/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/PeriodRevenue/Default.cshtml index 77e2348..aac8bde 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/PeriodRevenue/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/PeriodRevenue/Default.cshtml @@ -1,7 +1,7 @@ - Periodens omsætning - Systemtal vs. kontrol + Periodens omsætning + Systemtal vs. kontrol @@ -12,7 +12,7 @@ - Kortbetalinger + Kortbetalinger 12.875,50 @@ -20,7 +20,7 @@ - MobilePay / Online + MobilePay / Online 2.450,00 @@ -28,12 +28,12 @@ - Kontantsalg + Kontantsalg 3.540,00 .. - Kort og MobilePay afstemmes mod bank/indløser. Kontanter tælles op nedenfor. + Kort og MobilePay afstemmes mod bank/indløser. Kontanter tælles op nedenfor. diff --git a/PlanTempus.Application/Features/CashRegister/Components/ReconciliationNote/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/ReconciliationNote/Default.cshtml index 5d63a14..bda7473 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/ReconciliationNote/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/ReconciliationNote/Default.cshtml @@ -1,12 +1,10 @@ - Note til difference - Valgfrit + Note til afstemning - + - Kan gøres obligatorisk ved difference over 100 kr. diff --git a/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml index 2675ca8..e75ddf8 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/ReconciliationTable/Default.cshtml @@ -1,11 +1,11 @@ - 0 valgt + 0 valgt - Eksporter SAF-T + Eksporter SAF-T @@ -13,14 +13,14 @@ - Dato - ID - Periode - Kassepunkt - Afsluttet af - Omsætning - Difference - Status + Dato + ID + Periode + Kassepunkt + Afsluttet af + Omsætning + Difference + Status @@ -39,7 +39,7 @@ 4.250 kr - Kladde + Kladde @@ -56,7 +56,7 @@ Anna Jensen 18.865 kr 0 kr - Godkendt + Godkendt @@ -64,15 +64,15 @@ - Download CSV + Download CSV - Download PDF + Download PDF - Se transaktioner + Se transaktioner @@ -91,7 +91,7 @@ Karina Knudsen 12.450 kr -25 kr - Godkendt + Godkendt @@ -99,15 +99,15 @@ - Download CSV + Download CSV - Download PDF + Download PDF - Se transaktioner + Se transaktioner @@ -126,7 +126,7 @@ Martin Nielsen 21.340 kr 0 kr - Godkendt + Godkendt @@ -134,15 +134,15 @@ - Download CSV + Download CSV - Download PDF + Download PDF - Se transaktioner + Se transaktioner @@ -161,7 +161,7 @@ Anna Jensen 28.750 kr -50 kr - Godkendt + Godkendt @@ -169,15 +169,15 @@ - Download CSV + Download CSV - Download PDF + Download PDF - Se transaktioner + Se transaktioner diff --git a/PlanTempus.Application/Features/CashRegister/Components/RegisterFilterBar/Default.cshtml b/PlanTempus.Application/Features/CashRegister/Components/RegisterFilterBar/Default.cshtml index 2371b88..ec7d328 100644 --- a/PlanTempus.Application/Features/CashRegister/Components/RegisterFilterBar/Default.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Components/RegisterFilterBar/Default.cshtml @@ -1,28 +1,28 @@ - Fra + Fra - Til + Til - Kassepunkt + Kassepunkt - Status + Status - Nulstil + Nulstil diff --git a/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml b/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml index cec4339..4a0e869 100644 --- a/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml +++ b/PlanTempus.Application/Features/CashRegister/Pages/Index.cshtml @@ -13,19 +13,19 @@ 12 - Afstemninger i periode + Afstemninger i periode 186.450 kr - Total omsætning + Total omsætning 42.340 kr - Kontantsalg + Kontantsalg -75 kr - Samlet difference + Samlet difference @@ -33,19 +33,19 @@ 47 - Transaktioner i dag + Transaktioner i dag 18.865 kr - Omsætning i dag + Omsætning i dag 29. dec 17:45 - Sidste afstemning + Sidste afstemning Anna J. - Åbnede kassen 29. dec 09:05 + Åbnede kassen 29. dec 09:05 @@ -54,11 +54,11 @@ - Oversigt + Oversigt - Kasseafstemning + Kasseafstemning @@ -88,6 +88,6 @@ - Systemet gemmer hvornår og af hvem der er godkendt – enkelt kontrolspor. + Systemet gemmer hvornår og af hvem der er godkendt – enkelt kontrolspor. diff --git a/PlanTempus.Application/Features/Dashboard/Components/AttentionList/AttentionListViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/AttentionList/AttentionListViewComponent.cs index 2892d22..e62bff8 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/AttentionList/AttentionListViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/AttentionList/AttentionListViewComponent.cs @@ -1,12 +1,20 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; public class AttentionListViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public AttentionListViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = AttentionListCatalog.Get(key); + var model = AttentionListCatalog.Get(key, _localization); return View(model); } } @@ -18,23 +26,35 @@ public class AttentionListViewModel public required IReadOnlyList AttentionKeys { get; init; } } +internal class AttentionListData +{ + public required string Key { get; init; } + public required string TitleKey { get; init; } + public required IReadOnlyList AttentionKeys { get; init; } +} + public static class AttentionListCatalog { - private static readonly Dictionary Lists = new() + private static readonly Dictionary Lists = new() { - ["current-attentions"] = new AttentionListViewModel + ["current-attentions"] = new AttentionListData { Key = "current-attentions", - Title = "Opmærksomheder", + TitleKey = "dashboard.attentions.title", AttentionKeys = ["attention-1", "attention-2", "attention-3"] } }; - public static AttentionListViewModel Get(string key) + public static AttentionListViewModel Get(string key, ILocalizationService localization) { - if (Lists.TryGetValue(key, out var list)) - return list; + if (!Lists.TryGetValue(key, out var list)) + throw new KeyNotFoundException($"AttentionList with key '{key}' not found"); - throw new KeyNotFoundException($"AttentionList with key '{key}' not found"); + return new AttentionListViewModel + { + Key = list.Key, + Title = localization.Get(list.TitleKey), + AttentionKeys = list.AttentionKeys + }; } } diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingItem/BookingItemViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/BookingItem/BookingItemViewComponent.cs index 4e172f3..3770e60 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/BookingItem/BookingItemViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingItem/BookingItemViewComponent.cs @@ -1,12 +1,20 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; public class BookingItemViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public BookingItemViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = BookingItemCatalog.Get(key); + var model = BookingItemCatalog.Get(key, _localization); return View(model); } } @@ -21,23 +29,36 @@ public class BookingItemViewModel public required string EmployeeInitials { get; init; } public required string EmployeeName { get; init; } public required string Status { get; init; } + public required string StatusText { get; init; } public string? IndicatorColor { get; init; } +} - public string StatusText => Status switch - { - "completed" => "Gennemført", - "inprogress" => "I gang", - "confirmed" => "Bekræftet", - "pending" => "Afventer", - _ => Status - }; +internal class BookingItemData +{ + 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 static class BookingItemCatalog { - private static readonly Dictionary Bookings = new() + private static readonly Dictionary StatusKeys = new() { - ["booking-1"] = new BookingItemViewModel + ["completed"] = "dashboard.bookings.status.completed", + ["inprogress"] = "dashboard.bookings.status.inProgress", + ["confirmed"] = "dashboard.bookings.status.confirmed", + ["pending"] = "dashboard.bookings.status.pending" + }; + + private static readonly Dictionary Bookings = new() + { + ["booking-1"] = new BookingItemData { Key = "booking-1", TimeStart = "08:00", @@ -48,7 +69,7 @@ public static class BookingItemCatalog EmployeeName = "Maria Hansen", Status = "completed" }, - ["booking-2"] = new BookingItemViewModel + ["booking-2"] = new BookingItemData { Key = "booking-2", TimeStart = "08:30", @@ -59,7 +80,7 @@ public static class BookingItemCatalog EmployeeName = "Anna Sørensen", Status = "completed" }, - ["booking-3"] = new BookingItemViewModel + ["booking-3"] = new BookingItemData { Key = "booking-3", TimeStart = "09:00", @@ -70,7 +91,7 @@ public static class BookingItemCatalog EmployeeName = "Peter Kristensen", Status = "completed" }, - ["booking-4"] = new BookingItemViewModel + ["booking-4"] = new BookingItemData { Key = "booking-4", TimeStart = "10:30", @@ -82,7 +103,7 @@ public static class BookingItemCatalog Status = "inprogress", IndicatorColor = "blue" }, - ["booking-5"] = new BookingItemViewModel + ["booking-5"] = new BookingItemData { Key = "booking-5", TimeStart = "10:00", @@ -94,7 +115,7 @@ public static class BookingItemCatalog Status = "inprogress", IndicatorColor = "purple" }, - ["booking-6"] = new BookingItemViewModel + ["booking-6"] = new BookingItemData { Key = "booking-6", TimeStart = "11:00", @@ -108,12 +129,28 @@ public static class BookingItemCatalog } }; - public static BookingItemViewModel Get(string key) + public static BookingItemViewModel Get(string key, ILocalizationService localization) { - if (Bookings.TryGetValue(key, out var booking)) - return booking; + if (!Bookings.TryGetValue(key, out var booking)) + throw new KeyNotFoundException($"BookingItem with key '{key}' not found"); - throw new KeyNotFoundException($"BookingItem with key '{key}' not found"); + var statusText = StatusKeys.TryGetValue(booking.Status, out var statusKey) + ? localization.Get(statusKey) + : booking.Status; + + return new BookingItemViewModel + { + Key = booking.Key, + TimeStart = booking.TimeStart, + TimeEnd = booking.TimeEnd, + Service = booking.Service, + CustomerName = booking.CustomerName, + EmployeeInitials = booking.EmployeeInitials, + EmployeeName = booking.EmployeeName, + Status = booking.Status, + StatusText = statusText, + IndicatorColor = booking.IndicatorColor + }; } public static IEnumerable AllKeys => Bookings.Keys; diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingList/BookingListViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/BookingList/BookingListViewComponent.cs index 7c99560..2e6305a 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/BookingList/BookingListViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingList/BookingListViewComponent.cs @@ -1,12 +1,20 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; public class BookingListViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public BookingListViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = BookingListCatalog.Get(key); + var model = BookingListCatalog.Get(key, _localization); return View(model); } } @@ -19,24 +27,38 @@ public class BookingListViewModel public required IReadOnlyList BookingKeys { get; init; } } +internal class BookingListData +{ + public required string Key { get; init; } + public required string TitleKey { get; init; } + public required string CurrentTime { get; init; } + public required IReadOnlyList BookingKeys { get; init; } +} + public static class BookingListCatalog { - private static readonly Dictionary Lists = new() + private static readonly Dictionary Lists = new() { - ["todays-bookings"] = new BookingListViewModel + ["todays-bookings"] = new BookingListData { Key = "todays-bookings", - Title = "Dagens bookinger", + TitleKey = "dashboard.bookings.title", CurrentTime = "10:45", BookingKeys = ["booking-1", "booking-2", "booking-3", "booking-4", "booking-5", "booking-6"] } }; - public static BookingListViewModel Get(string key) + public static BookingListViewModel Get(string key, ILocalizationService localization) { - if (Lists.TryGetValue(key, out var list)) - return list; + if (!Lists.TryGetValue(key, out var list)) + throw new KeyNotFoundException($"BookingList with key '{key}' not found"); - throw new KeyNotFoundException($"BookingList with key '{key}' not found"); + return new BookingListViewModel + { + Key = list.Key, + Title = localization.Get(list.TitleKey), + CurrentTime = list.CurrentTime, + BookingKeys = list.BookingKeys + }; } } diff --git a/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml b/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml index 878b7fd..71acecf 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml +++ b/PlanTempus.Application/Features/Dashboard/Components/BookingList/Default.cshtml @@ -6,11 +6,11 @@ @Model.Title - Se alle + Se alle - Nu: @Model.CurrentTime + Nu: @Model.CurrentTime diff --git a/PlanTempus.Application/Features/Dashboard/Components/NotificationList/NotificationListViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/NotificationList/NotificationListViewComponent.cs index b2596f7..ba7912f 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/NotificationList/NotificationListViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/NotificationList/NotificationListViewComponent.cs @@ -1,12 +1,20 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; public class NotificationListViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public NotificationListViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = NotificationListCatalog.Get(key); + var model = NotificationListCatalog.Get(key, _localization); return View(model); } } @@ -19,24 +27,38 @@ public class NotificationListViewModel public required IReadOnlyList NotificationKeys { get; init; } } +internal class NotificationListData +{ + public required string Key { get; init; } + public required string TitleKey { get; init; } + public required string ActionTextKey { get; init; } + public required IReadOnlyList NotificationKeys { get; init; } +} + public static class NotificationListCatalog { - private static readonly Dictionary Lists = new() + private static readonly Dictionary Lists = new() { - ["recent-notifications"] = new NotificationListViewModel + ["recent-notifications"] = new NotificationListData { Key = "recent-notifications", - Title = "Notifikationer", - ActionText = "Marker alle som læst", + TitleKey = "dashboard.notifications.title", + ActionTextKey = "dashboard.notifications.markAllRead", NotificationKeys = ["notif-1", "notif-2", "notif-3", "notif-4"] } }; - public static NotificationListViewModel Get(string key) + public static NotificationListViewModel Get(string key, ILocalizationService localization) { - if (Lists.TryGetValue(key, out var list)) - return list; + if (!Lists.TryGetValue(key, out var list)) + throw new KeyNotFoundException($"NotificationList with key '{key}' not found"); - throw new KeyNotFoundException($"NotificationList with key '{key}' not found"); + return new NotificationListViewModel + { + Key = list.Key, + Title = localization.Get(list.TitleKey), + ActionText = localization.Get(list.ActionTextKey), + NotificationKeys = list.NotificationKeys + }; } } diff --git a/PlanTempus.Application/Features/Dashboard/Components/QuickStat/QuickStatViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/QuickStat/QuickStatViewComponent.cs index eb16392..4d8c0fd 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/QuickStat/QuickStatViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/QuickStat/QuickStatViewComponent.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; @@ -7,9 +8,16 @@ namespace PlanTempus.Application.Features.Dashboard.Components; /// public class QuickStatViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public QuickStatViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = QuickStatCatalog.Get(key); + var model = QuickStatCatalog.Get(key, _localization); return View(model); } } @@ -24,45 +32,57 @@ public class QuickStatViewModel public required string Label { get; init; } } +internal class QuickStatData +{ + public required string Key { get; init; } + public required string Value { get; init; } + public required string LabelKey { get; init; } +} + /// /// Catalog of available quick stats with their data. /// public static class QuickStatCatalog { - private static readonly Dictionary Stats = new() + private static readonly Dictionary Stats = new() { - ["bookings-week"] = new QuickStatViewModel + ["bookings-week"] = new QuickStatData { Key = "bookings-week", Value = "47", - Label = "Bookinger" + LabelKey = "dashboard.quickStats.bookings" }, - ["revenue-week"] = new QuickStatViewModel + ["revenue-week"] = new QuickStatData { Key = "revenue-week", Value = "38.200 kr", - Label = "Omsætning" + LabelKey = "dashboard.quickStats.revenue" }, - ["new-customers"] = new QuickStatViewModel + ["new-customers"] = new QuickStatData { Key = "new-customers", Value = "8", - Label = "Nye kunder" + LabelKey = "dashboard.quickStats.newCustomers" }, - ["avg-occupancy"] = new QuickStatViewModel + ["avg-occupancy"] = new QuickStatData { Key = "avg-occupancy", Value = "72%", - Label = "Gns. belægning" + LabelKey = "dashboard.quickStats.avgOccupancy" } }; - public static QuickStatViewModel Get(string key) + public static QuickStatViewModel Get(string key, ILocalizationService localization) { - if (Stats.TryGetValue(key, out var stat)) - return stat; + if (!Stats.TryGetValue(key, out var stat)) + throw new KeyNotFoundException($"QuickStat with key '{key}' not found"); - throw new KeyNotFoundException($"QuickStat with key '{key}' not found"); + return new QuickStatViewModel + { + Key = stat.Key, + Value = stat.Value, + Label = localization.Get(stat.LabelKey) + }; } public static IEnumerable AllKeys => Stats.Keys; diff --git a/PlanTempus.Application/Features/Dashboard/Components/QuickStatList/QuickStatListViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/QuickStatList/QuickStatListViewComponent.cs index cef1f5e..53e39bd 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/QuickStatList/QuickStatListViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/QuickStatList/QuickStatListViewComponent.cs @@ -1,12 +1,20 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; public class QuickStatListViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public QuickStatListViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = QuickStatListCatalog.Get(key); + var model = QuickStatListCatalog.Get(key, _localization); return View(model); } } @@ -19,24 +27,38 @@ public class QuickStatListViewModel public required IReadOnlyList StatKeys { get; init; } } +internal class QuickStatListData +{ + public required string Key { get; init; } + public required string TitleKey { get; init; } + public required string Icon { get; init; } + public required IReadOnlyList StatKeys { get; init; } +} + public static class QuickStatListCatalog { - private static readonly Dictionary Lists = new() + private static readonly Dictionary Lists = new() { - ["this-week"] = new QuickStatListViewModel + ["this-week"] = new QuickStatListData { Key = "this-week", - Title = "Denne uge", + TitleKey = "dashboard.quickStats.title", Icon = "chart-line-up", StatKeys = ["bookings-week", "revenue-week", "new-customers", "avg-occupancy"] } }; - public static QuickStatListViewModel Get(string key) + public static QuickStatListViewModel Get(string key, ILocalizationService localization) { - if (Lists.TryGetValue(key, out var list)) - return list; + if (!Lists.TryGetValue(key, out var list)) + throw new KeyNotFoundException($"QuickStatList with key '{key}' not found"); - throw new KeyNotFoundException($"QuickStatList with key '{key}' not found"); + return new QuickStatListViewModel + { + Key = list.Key, + Title = localization.Get(list.TitleKey), + Icon = list.Icon, + StatKeys = list.StatKeys + }; } } diff --git a/PlanTempus.Application/Features/Dashboard/Components/StatCard/StatCardViewComponent.cs b/PlanTempus.Application/Features/Dashboard/Components/StatCard/StatCardViewComponent.cs index ccac82f..53583e5 100644 --- a/PlanTempus.Application/Features/Dashboard/Components/StatCard/StatCardViewComponent.cs +++ b/PlanTempus.Application/Features/Dashboard/Components/StatCard/StatCardViewComponent.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Localization.Services; namespace PlanTempus.Application.Features.Dashboard.Components; @@ -7,9 +8,16 @@ namespace PlanTempus.Application.Features.Dashboard.Components; /// public class StatCardViewComponent : ViewComponent { + private readonly ILocalizationService _localization; + + public StatCardViewComponent(ILocalizationService localization) + { + _localization = localization; + } + public IViewComponentResult Invoke(string key) { - var model = StatCardCatalog.Get(key); + var model = StatCardCatalog.Get(key, _localization); return View(model); } } @@ -29,61 +37,84 @@ public class StatCardViewModel public bool HasTrend => !string.IsNullOrEmpty(TrendText); } +/// +/// Internal data for stat cards (uses localization keys). +/// +internal class StatCardData +{ + public required string Key { get; init; } + public required string Value { get; init; } + public required string LabelKey { get; init; } + public string? TrendTextKey { get; init; } + public string? TrendIcon { get; init; } + public string? TrendDirection { get; init; } + public string? Variant { get; init; } +} + /// /// Catalog of available stat cards with their data. /// public static class StatCardCatalog { - private static readonly Dictionary Cards = new() + private static readonly Dictionary Cards = new() { - ["bookings-today"] = new StatCardViewModel + ["bookings-today"] = new StatCardData { Key = "bookings-today", Value = "12", - Label = "Bookinger i dag", - TrendText = "4 gennemført, 2 i gang", + LabelKey = "dashboard.stats.bookingsToday", + TrendTextKey = "dashboard.stats.bookingsTrend", TrendIcon = "ph-check-circle", TrendDirection = "up", Variant = "highlight" }, - ["expected-revenue"] = new StatCardViewModel + ["expected-revenue"] = new StatCardData { Key = "expected-revenue", Value = "8.450 kr", - Label = "Forventet omsætning", - TrendText = "+12% vs. gennemsnit", + LabelKey = "dashboard.stats.expectedRevenue", + TrendTextKey = "dashboard.stats.revenueTrend", TrendIcon = "ph-trend-up", TrendDirection = "up", Variant = "success" }, - ["occupancy-rate"] = new StatCardViewModel + ["occupancy-rate"] = new StatCardData { Key = "occupancy-rate", Value = "78%", - Label = "Belægningsgrad", - TrendText = "God kapacitet", + LabelKey = "dashboard.stats.occupancyRate", + TrendTextKey = "dashboard.stats.occupancyTrend", TrendIcon = "ph-trend-up", TrendDirection = "up", Variant = null }, - ["needs-attention"] = new StatCardViewModel + ["needs-attention"] = new StatCardData { Key = "needs-attention", Value = "4", - Label = "Kræver opmærksomhed", - TrendText = null, + LabelKey = "dashboard.stats.needsAttention", + TrendTextKey = null, TrendIcon = null, TrendDirection = null, Variant = "warning" } }; - public static StatCardViewModel Get(string key) + public static StatCardViewModel Get(string key, ILocalizationService localization) { - if (Cards.TryGetValue(key, out var card)) - return card; + if (!Cards.TryGetValue(key, out var card)) + throw new KeyNotFoundException($"StatCard with key '{key}' not found"); - throw new KeyNotFoundException($"StatCard with key '{key}' not found"); + return new StatCardViewModel + { + Key = card.Key, + Value = card.Value, + Label = localization.Get(card.LabelKey), + TrendText = card.TrendTextKey != null ? localization.Get(card.TrendTextKey) : null, + TrendIcon = card.TrendIcon, + TrendDirection = card.TrendDirection, + Variant = card.Variant + }; } public static IEnumerable AllKeys => Cards.Keys; diff --git a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml index 1a746d3..ad4c940 100644 --- a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml +++ b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml @@ -22,7 +22,7 @@ - AI Analyse + AI Analyse Godt i gang! 4 af 12 bookinger er gennemført. 2 er i gang nu, og 6 venter. @@ -51,17 +51,17 @@ - Hurtige handlinger + Hurtige handlinger - Ny booking + Ny booking - Ny kunde + Ny kunde diff --git a/PlanTempus.Application/Features/Localization/Translations/da.json b/PlanTempus.Application/Features/Localization/Translations/da.json index c569ae6..2589a9c 100644 --- a/PlanTempus.Application/Features/Localization/Translations/da.json +++ b/PlanTempus.Application/Features/Localization/Translations/da.json @@ -24,10 +24,197 @@ "close": "Luk", "delete": "Slet", "edit": "Rediger", - "add": "Tilføj" + "add": "Tilføj", + "from": "Fra", + "to": "Til", + "all": "Alle", + "reset": "Nulstil", + "status": "Status" }, "sidebar": { "lockScreen": "Lås skærm", "appName": "Salon OS" + }, + "dashboard": { + "title": "Dashboard", + "subtitle": "Overblik over dagens aktivitet", + "stats": { + "bookingsToday": "Bookinger i dag", + "bookingsTrend": "4 gennemført, 2 i gang", + "expectedRevenue": "Forventet omsætning", + "revenueTrend": "+12% vs. gennemsnit", + "occupancyRate": "Belægningsgrad", + "occupancyTrend": "God kapacitet", + "needsAttention": "Kræver handling" + }, + "ai": { + "header": "AI Analyse" + }, + "bookings": { + "title": "Dagens bookinger", + "viewAll": "Se alle", + "currentTime": "Nu:", + "status": { + "confirmed": "Bekræftet", + "pending": "Afventer", + "inProgress": "I gang", + "completed": "Gennemført" + } + }, + "notifications": { + "title": "Notifikationer", + "viewAll": "Se alle", + "markAllRead": "Marker alle som læst" + }, + "attentions": { + "title": "Kræver opmærksomhed", + "viewAll": "Se alle" + }, + "quickStats": { + "title": "Denne uge", + "bookings": "Bookinger", + "revenue": "Omsætning", + "newCustomers": "Nye kunder", + "cancellations": "Aflysninger", + "avgOccupancy": "Gns. belægning" + }, + "quickActions": { + "title": "Hurtige handlinger", + "newBooking": "Ny booking", + "newCustomer": "Ny kunde" + }, + "waitlist": { + "title": "Venteliste", + "count": "{count} venter" + } + }, + "cash": { + "title": "Kasse", + "tabs": { + "overview": "Oversigt", + "reconciliation": "Kasseafstemning" + }, + "stats": { + "reconciliationsInPeriod": "Afstemninger i periode", + "totalRevenue": "Total omsætning", + "cashSales": "Kontantsalg", + "totalDifference": "Samlet difference", + "transactionsToday": "Transaktioner i dag", + "revenueToday": "Omsætning i dag", + "lastReconciliation": "Sidste afstemning", + "openedRegister": "Åbnede kassen" + }, + "filter": { + "register": "Kassepunkt", + "approved": "Godkendt", + "draft": "Kladde" + }, + "table": { + "date": "Dato", + "id": "ID", + "period": "Periode", + "register": "Kassepunkt", + "closedBy": "Afsluttet af", + "revenue": "Omsætning", + "difference": "Difference", + "selected": "{count} valgt", + "noneSelected": "0 valgt", + "showingCount": "Viser {count} afstemninger", + "exportSaft": "Eksporter SAF-T", + "downloadCsv": "Download CSV", + "downloadPdf": "Download PDF", + "viewTransactions": "Se transaktioner" + }, + "revenue": { + "title": "Periodens omsætning", + "subtitle": "Systemtal vs. kontrol", + "cardPayments": "Kortbetalinger", + "mobilePay": "MobilePay / Online", + "cashSales": "Kontantsalg", + "hint": "Kort og MobilePay afstemmes mod bank/indløser. Kontanter tælles op nedenfor." + }, + "balance": { + "title": "Kontanter i kassen", + "startBalance": "Startbeholdning", + "startHint": "Overført fra sidste afstemning", + "payouts": "Udbetalinger / Bilag", + "payoutsHint": "Sammentæl bilag betalt kontant", + "toBank": "Udtaget til bank", + "toBankHint": "Kontanter lagt til side", + "expected": "Forventet kontantbeholdning", + "counted": "Optalt kontantbeholdning", + "countedHint": "Hvad ligger der faktisk i kassen?" + }, + "difference": { + "title": "Kassedifference", + "match": "Kassen stemmer", + "over": "Overskud", + "under": "Underskud" + }, + "period": { + "title": "Periodeoplysninger", + "dateRange": "Periode", + "register": "Kassepunkt", + "employee": "Medarbejder" + }, + "note": { + "title": "Note til afstemning", + "placeholder": "Beskriv evt. årsag til difference..." + }, + "approval": { + "title": "Afslut dagen", + "approvedBy": "Godkendt af (valgfrit)", + "selectPlaceholder": "Vælg...", + "confirmation": "Jeg bekræfter, at kassen er talt op, og at tallene er indtastet efter bedste evne.", + "saveDraft": "Gem som kladde", + "approve": "Godkend & lås" + }, + "status": { + "draft": "Kladde", + "approved": "Godkendt" + }, + "systemNote": "Systemet gemmer hvornår og af hvem der er godkendt – enkelt kontrolspor." + }, + "profile": { + "title": "Profil", + "myProfile": "Min profil", + "settings": "Indstillinger", + "darkMode": "Mørk tilstand", + "logout": "Log ud" + }, + "account": { + "title": "Abonnement & Konto", + "subtitle": "Administrer dit abonnement og betalingsinfo", + "subscription": { + "title": "Dit abonnement", + "currentPlan": "Nuværende plan", + "switchTo": "Skift til {plan}", + "contactSales": "Kontakt salg", + "contactUs": "Kontakt os", + "pricePerMonth": "kr/md" + }, + "billing": { + "title": "Betaling & Fakturaer" + }, + "payment": { + "frequency": "Betalingsfrekvens", + "monthly": "Månedlig", + "yearly": "Årlig", + "nextPayment": "Næste betaling", + "amount": "Beløb", + "cardExpiry": "Kortudløb", + "change": "Skift", + "switchToYearly": "Skift til årlig betaling (spar 15%)" + }, + "invoices": { + "title": "Faktura-historik", + "date": "Dato", + "invoiceNumber": "Fakturanr.", + "amount": "Beløb", + "download": "PDF", + "paid": "Betalt", + "pending": "Afventer", + "overdue": "Forfalden" + } } } diff --git a/PlanTempus.Application/Features/Localization/Translations/en.json b/PlanTempus.Application/Features/Localization/Translations/en.json index a66a7b7..9738bcc 100644 --- a/PlanTempus.Application/Features/Localization/Translations/en.json +++ b/PlanTempus.Application/Features/Localization/Translations/en.json @@ -24,10 +24,197 @@ "close": "Close", "delete": "Delete", "edit": "Edit", - "add": "Add" + "add": "Add", + "from": "From", + "to": "To", + "all": "All", + "reset": "Reset", + "status": "Status" }, "sidebar": { "lockScreen": "Lock screen", "appName": "Salon OS" + }, + "dashboard": { + "title": "Dashboard", + "subtitle": "Overview of today's activity", + "stats": { + "bookingsToday": "Bookings today", + "bookingsTrend": "4 completed, 2 in progress", + "expectedRevenue": "Expected revenue", + "revenueTrend": "+12% vs. average", + "occupancyRate": "Occupancy rate", + "occupancyTrend": "Good capacity", + "needsAttention": "Needs attention" + }, + "ai": { + "header": "AI Analysis" + }, + "bookings": { + "title": "Today's bookings", + "viewAll": "View all", + "currentTime": "Now:", + "status": { + "confirmed": "Confirmed", + "pending": "Pending", + "inProgress": "In progress", + "completed": "Completed" + } + }, + "notifications": { + "title": "Notifications", + "viewAll": "View all", + "markAllRead": "Mark all as read" + }, + "attentions": { + "title": "Needs attention", + "viewAll": "View all" + }, + "quickStats": { + "title": "This week", + "bookings": "Bookings", + "revenue": "Revenue", + "newCustomers": "New customers", + "cancellations": "Cancellations", + "avgOccupancy": "Avg. occupancy" + }, + "quickActions": { + "title": "Quick actions", + "newBooking": "New booking", + "newCustomer": "New customer" + }, + "waitlist": { + "title": "Waitlist", + "count": "{count} waiting" + } + }, + "cash": { + "title": "Cash Register", + "tabs": { + "overview": "Overview", + "reconciliation": "Reconciliation" + }, + "stats": { + "reconciliationsInPeriod": "Reconciliations in period", + "totalRevenue": "Total revenue", + "cashSales": "Cash sales", + "totalDifference": "Total difference", + "transactionsToday": "Transactions today", + "revenueToday": "Revenue today", + "lastReconciliation": "Last reconciliation", + "openedRegister": "Opened register" + }, + "filter": { + "register": "Register", + "approved": "Approved", + "draft": "Draft" + }, + "table": { + "date": "Date", + "id": "ID", + "period": "Period", + "register": "Register", + "closedBy": "Closed by", + "revenue": "Revenue", + "difference": "Difference", + "selected": "{count} selected", + "noneSelected": "0 selected", + "showingCount": "Showing {count} reconciliations", + "exportSaft": "Export SAF-T", + "downloadCsv": "Download CSV", + "downloadPdf": "Download PDF", + "viewTransactions": "View transactions" + }, + "revenue": { + "title": "Period revenue", + "subtitle": "System vs. control", + "cardPayments": "Card payments", + "mobilePay": "MobilePay / Online", + "cashSales": "Cash sales", + "hint": "Card and MobilePay are reconciled against bank/acquirer. Cash is counted below." + }, + "balance": { + "title": "Cash in register", + "startBalance": "Starting balance", + "startHint": "Carried over from last reconciliation", + "payouts": "Payouts / Receipts", + "payoutsHint": "Total receipts paid in cash", + "toBank": "Withdrawn to bank", + "toBankHint": "Cash set aside", + "expected": "Expected cash balance", + "counted": "Counted cash balance", + "countedHint": "What is actually in the register?" + }, + "difference": { + "title": "Cash difference", + "match": "Register balanced", + "over": "Overage", + "under": "Shortage" + }, + "period": { + "title": "Period information", + "dateRange": "Period", + "register": "Register", + "employee": "Employee" + }, + "note": { + "title": "Reconciliation note", + "placeholder": "Describe reason for difference..." + }, + "approval": { + "title": "Close the day", + "approvedBy": "Approved by (optional)", + "selectPlaceholder": "Select...", + "confirmation": "I confirm that the register has been counted and the figures have been entered to the best of my ability.", + "saveDraft": "Save as draft", + "approve": "Approve & lock" + }, + "status": { + "draft": "Draft", + "approved": "Approved" + }, + "systemNote": "The system records when and by whom approval was made – simple audit trail." + }, + "profile": { + "title": "Profile", + "myProfile": "My profile", + "settings": "Settings", + "darkMode": "Dark mode", + "logout": "Log out" + }, + "account": { + "title": "Subscription & Account", + "subtitle": "Manage your subscription and payment info", + "subscription": { + "title": "Your subscription", + "currentPlan": "Current plan", + "switchTo": "Switch to {plan}", + "contactSales": "Contact sales", + "contactUs": "Contact us", + "pricePerMonth": "/mo" + }, + "billing": { + "title": "Payment & Invoices" + }, + "payment": { + "frequency": "Payment frequency", + "monthly": "Monthly", + "yearly": "Yearly", + "nextPayment": "Next payment", + "amount": "Amount", + "cardExpiry": "Card expiry", + "change": "Change", + "switchToYearly": "Switch to yearly billing (save 15%)" + }, + "invoices": { + "title": "Invoice history", + "date": "Date", + "invoiceNumber": "Invoice no.", + "amount": "Amount", + "download": "PDF", + "paid": "Paid", + "pending": "Pending", + "overdue": "Overdue" + } } } diff --git a/PlanTempus.Application/Features/Shared/_WaitlistDrawer.cshtml b/PlanTempus.Application/Features/Shared/_WaitlistDrawer.cshtml index bee4ac3..2ffc20c 100644 --- a/PlanTempus.Application/Features/Shared/_WaitlistDrawer.cshtml +++ b/PlanTempus.Application/Features/Shared/_WaitlistDrawer.cshtml @@ -3,7 +3,7 @@ - Venteliste (@WaitlistItemCatalog.AllKeys.Count()) + Venteliste (@WaitlistItemCatalog.AllKeys.Count()) diff --git a/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml index 9e42620..68f1020 100644 --- a/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml +++ b/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml @@ -1,6 +1,6 @@ - Profil + Profil @@ -18,11 +18,11 @@ - Min profil + Min profil - Indstillinger + Indstillinger @@ -31,7 +31,7 @@ - Mørk tilstand + Mørk tilstand @@ -43,7 +43,7 @@ - Log ud + Log ud diff --git a/PlanTempus.Application/wwwroot/css/account.css b/PlanTempus.Application/wwwroot/css/account.css index 908ab38..980e89c 100644 --- a/PlanTempus.Application/wwwroot/css/account.css +++ b/PlanTempus.Application/wwwroot/css/account.css @@ -10,14 +10,14 @@ =========================================== */ swp-account-section { display: block; - margin-bottom: var(--spacing-8); + margin-bottom: var(--section-gap); } swp-account-section-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: var(--spacing-4); + margin-bottom: var(--section-gap); } swp-account-section-title { @@ -40,7 +40,7 @@ swp-account-section-title i { swp-plan-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-5); + gap: var(--card-gap); } @media (max-width: 1024px) { @@ -82,7 +82,7 @@ swp-plan-badge.popular { /* Plan action buttons */ swp-plan-action { margin-top: auto; - padding-top: var(--spacing-5); + padding-top: var(--card-padding); } swp-plan-action swp-btn { @@ -96,7 +96,7 @@ swp-plan-action swp-btn { swp-billing-grid { display: grid; grid-template-columns: 380px 1fr; - gap: var(--spacing-6); + gap: var(--card-gap); } @media (max-width: 1024px) { @@ -111,18 +111,18 @@ swp-billing-grid { swp-payment-card { display: flex; flex-direction: column; - gap: var(--spacing-5); + gap: var(--card-padding); background: var(--color-surface); border-radius: var(--radius-lg); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); - padding: var(--spacing-6); + padding: var(--card-padding); } swp-payment-method { display: flex; align-items: center; gap: var(--spacing-4); - padding: var(--spacing-4); + padding: var(--card-padding); background: var(--color-background-alt); border-radius: var(--radius-md); } @@ -168,7 +168,7 @@ swp-payment-detail { display: flex; justify-content: space-between; align-items: center; - padding: var(--spacing-3) 0; + padding: var(--spacing-4) 0; border-bottom: 1px solid var(--color-border); } @@ -206,7 +206,7 @@ swp-invoices-header { display: flex; align-items: center; justify-content: space-between; - padding: var(--spacing-4) var(--spacing-5); + padding: var(--card-padding); border-bottom: 1px solid var(--color-border); } @@ -255,17 +255,17 @@ swp-invoice-row:last-child { } swp-invoice-cell { - padding: var(--spacing-3) var(--spacing-4); + padding: var(--spacing-4) var(--spacing-4); font-size: var(--font-size-sm); color: var(--color-text); } swp-invoice-cell:first-child { - padding-left: var(--spacing-5); + padding-left: var(--card-padding); } swp-invoice-cell:last-child { - padding-right: var(--spacing-5); + padding-right: var(--card-padding); } /* Header cells */ @@ -275,8 +275,8 @@ swp-invoice-table-header swp-invoice-cell { text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-text-secondary); - padding-top: var(--spacing-3); - padding-bottom: var(--spacing-3); + padding-top: var(--spacing-4); + padding-bottom: var(--spacing-4); } /* Invoice number mono font */ @@ -384,18 +384,18 @@ swp-btn.sm i { } swp-invoice-cell { - padding: var(--spacing-2) var(--spacing-3); + padding: var(--spacing-3) var(--spacing-3); } swp-invoice-cell:first-child { - padding-left: var(--spacing-4); + padding-left: var(--spacing-5); } swp-invoice-cell:last-child { - padding-right: var(--spacing-4); + padding-right: var(--spacing-5); } swp-payment-card { - padding: var(--spacing-4); + padding: var(--spacing-5); } } diff --git a/PlanTempus.Application/wwwroot/css/attentions.css b/PlanTempus.Application/wwwroot/css/attentions.css index 47d4738..b51693a 100644 --- a/PlanTempus.Application/wwwroot/css/attentions.css +++ b/PlanTempus.Application/wwwroot/css/attentions.css @@ -20,7 +20,7 @@ swp-attention-item { grid-template-columns: subgrid; align-items: center; gap: var(--spacing-8); - padding: var(--spacing-5) var(--spacing-6); + padding: var(--card-padding); background: var(--color-background-alt); border-radius: var(--radius-xl); border-left: 3px solid var(--color-border); diff --git a/PlanTempus.Application/wwwroot/css/bookings.css b/PlanTempus.Application/wwwroot/css/bookings.css index b04e98e..5c867f2 100644 --- a/PlanTempus.Application/wwwroot/css/bookings.css +++ b/PlanTempus.Application/wwwroot/css/bookings.css @@ -19,7 +19,7 @@ swp-booking-item { grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; - padding: var(--spacing-4); + padding: var(--card-padding); background: var(--color-background-alt); border-radius: var(--radius-lg); cursor: pointer; diff --git a/PlanTempus.Application/wwwroot/css/design-tokens.css b/PlanTempus.Application/wwwroot/css/design-tokens.css index 6718a60..8b3ff94 100644 --- a/PlanTempus.Application/wwwroot/css/design-tokens.css +++ b/PlanTempus.Application/wwwroot/css/design-tokens.css @@ -190,7 +190,10 @@ --container-max-width-lg: 1400px; /* -------- Card Spacing -------- */ - --card-body-padding: var(--spacing-5); + --card-padding: 12px; + --card-gap: 24px; + --section-gap: 24px; + --page-padding: 24px; /* -------- Calendar Grid -------- */ --hour-height: 64px; diff --git a/PlanTempus.Application/wwwroot/css/notifications.css b/PlanTempus.Application/wwwroot/css/notifications.css index 1f589f9..31fc996 100644 --- a/PlanTempus.Application/wwwroot/css/notifications.css +++ b/PlanTempus.Application/wwwroot/css/notifications.css @@ -19,7 +19,7 @@ swp-notification-item { grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; - padding: var(--spacing-5) var(--spacing-6); + padding: var(--card-padding); background: var(--color-background-alt); border-radius: var(--radius-xl); cursor: pointer; diff --git a/PlanTempus.Application/wwwroot/css/page.css b/PlanTempus.Application/wwwroot/css/page.css index c8263aa..3afe199 100644 --- a/PlanTempus.Application/wwwroot/css/page.css +++ b/PlanTempus.Application/wwwroot/css/page.css @@ -11,7 +11,7 @@ swp-page-container { display: block; max-width: var(--page-max-width); margin: 0 auto; - padding: var(--spacing-6); + padding: var(--page-padding); } /* =========================================== @@ -21,7 +21,7 @@ swp-page-header { display: flex; align-items: flex-start; justify-content: space-between; - margin-bottom: var(--spacing-6); + margin-bottom: var(--section-gap); } swp-page-title h1 { @@ -49,16 +49,15 @@ swp-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--border-radius-lg); - padding: var(--spacing-5); - margin-bottom: var(--spacing-5); + padding: var(--card-padding); } swp-card-header { display: flex; align-items: center; 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); + margin: calc(-1 * var(--card-padding)) calc(-1 * var(--card-padding)) var(--spacing-5) calc(-1 * var(--card-padding)); + padding: var(--spacing-4) var(--card-padding); border-bottom: 1px solid var(--color-border); } @@ -96,7 +95,7 @@ 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); + gap: var(--spacing-5); } swp-card-content:has(> swp-booking-list) { @@ -134,13 +133,13 @@ swp-item-desc { swp-dashboard-grid { display: grid; grid-template-columns: 1fr 380px; - gap: var(--spacing-5); + gap: var(--card-gap); } swp-main-column, swp-side-column { display: grid; - gap: var(--spacing-5); + gap: var(--card-gap); align-content: start; } @@ -149,7 +148,7 @@ swp-side-column { =========================================== */ swp-ai-insight { display: block; - padding: var(--spacing-4) var(--spacing-5); + padding: var(--card-padding); background: linear-gradient(135deg, color-mix(in srgb, var(--color-purple) 8%, transparent), color-mix(in srgb, var(--color-teal) 8%, transparent) @@ -183,14 +182,14 @@ swp-ai-text { swp-quick-actions { display: flex; flex-direction: column; - gap: var(--spacing-2); + gap: var(--spacing-3); } swp-quick-action-btn { display: flex; align-items: center; - gap: var(--spacing-2); - padding: var(--spacing-3); + gap: var(--spacing-3); + padding: var(--card-padding); font-size: var(--font-size-base); font-family: var(--font-family); color: var(--color-text); @@ -226,7 +225,7 @@ swp-quick-action-btn i { @media (max-width: 768px) { swp-page-container { - padding: var(--spacing-4); + padding: var(--spacing-5); } swp-page-header { diff --git a/PlanTempus.Application/wwwroot/css/quick-stats.css b/PlanTempus.Application/wwwroot/css/quick-stats.css index ec1f626..b077e7d 100644 --- a/PlanTempus.Application/wwwroot/css/quick-stats.css +++ b/PlanTempus.Application/wwwroot/css/quick-stats.css @@ -10,7 +10,7 @@ swp-quick-stats { display: grid; grid-template-columns: repeat(2, 1fr); - gap: var(--spacing-3); + gap: var(--card-gap); } /* =========================================== @@ -19,8 +19,8 @@ swp-quick-stats { swp-quick-stat { display: flex; flex-direction: column; - gap: var(--spacing-1); - padding: var(--spacing-3); + gap: var(--spacing-2); + padding: var(--card-padding); background: var(--color-background-alt); border-radius: var(--radius-md); } diff --git a/PlanTempus.Application/wwwroot/css/stats.css b/PlanTempus.Application/wwwroot/css/stats.css index dd49a10..bf79a14 100644 --- a/PlanTempus.Application/wwwroot/css/stats.css +++ b/PlanTempus.Application/wwwroot/css/stats.css @@ -11,15 +11,15 @@ swp-stats-bar, swp-stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); - gap: var(--spacing-4); - margin-bottom: var(--spacing-5); + gap: var(--card-gap); + margin-bottom: var(--section-gap); } swp-stats-row { display: grid; grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-4); - margin-bottom: var(--spacing-5); + gap: var(--card-gap); + margin-bottom: var(--section-gap); } /* =========================================== @@ -30,15 +30,15 @@ swp-stat-card { flex-direction: column; background: var(--color-surface); border-radius: var(--border-radius-lg); - padding: var(--spacing-4) var(--spacing-5); + padding: var(--card-padding); border: 1px solid var(--color-border); } swp-stat-box { display: flex; flex-direction: column; - gap: var(--spacing-1); - padding: var(--spacing-4); + gap: var(--spacing-2); + padding: var(--card-padding); background: var(--color-surface); border-radius: var(--border-radius); border: 1px solid var(--color-border); @@ -72,7 +72,7 @@ swp-stat-label { display: block; font-size: var(--font-size-sm); color: var(--color-text-secondary); - margin-top: var(--spacing-1); + margin-top: var(--spacing-2); } swp-stat-box swp-stat-label { diff --git a/PlanTempus.Application/wwwroot/css/waitlist.css b/PlanTempus.Application/wwwroot/css/waitlist.css index 8500654..792f774 100644 --- a/PlanTempus.Application/wwwroot/css/waitlist.css +++ b/PlanTempus.Application/wwwroot/css/waitlist.css @@ -10,8 +10,8 @@ swp-waitlist-card { display: flex; align-items: center; - gap: var(--spacing-3); - padding: var(--spacing-4); + gap: var(--spacing-4); + padding: var(--card-padding); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg);