diff --git a/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml b/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml new file mode 100644 index 0000000..9fd16d3 --- /dev/null +++ b/PlanTempus.Application/Features/Account/Components/InvoiceHistory/Default.cshtml @@ -0,0 +1,108 @@ + + + Faktura-historik + + + + + + Dato + Fakturanr. + Beløb + Status + + + + + + 1. jan 2026 + INV-2026-0001 + 599,00 kr + + + + + + + PDF + + + + + + 1. dec 2025 + INV-2025-0012 + 599,00 kr + + + + + + + PDF + + + + + + 1. nov 2025 + INV-2025-0011 + 599,00 kr + + + + + + + PDF + + + + + + 1. okt 2025 + INV-2025-0010 + 599,00 kr + + + + + + + PDF + + + + + + 1. sep 2025 + INV-2025-0009 + 599,00 kr + + + + + + + PDF + + + + + + diff --git a/PlanTempus.Application/Features/Account/Components/InvoiceHistory/InvoiceHistoryViewComponent.cs b/PlanTempus.Application/Features/Account/Components/InvoiceHistory/InvoiceHistoryViewComponent.cs new file mode 100644 index 0000000..93bcc7c --- /dev/null +++ b/PlanTempus.Application/Features/Account/Components/InvoiceHistory/InvoiceHistoryViewComponent.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PlanTempus.Application.Features.Account.Components; + +/// +/// ViewComponent for the invoice history table. +/// Shows past invoices with status and download options. +/// +public class InvoiceHistoryViewComponent : ViewComponent +{ + public IViewComponentResult Invoke() + { + return View(); + } +} diff --git a/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml b/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml new file mode 100644 index 0000000..d748aa1 --- /dev/null +++ b/PlanTempus.Application/Features/Account/Components/PaymentMethod/Default.cshtml @@ -0,0 +1,40 @@ + + + + + + + Visa + **** **** **** 4582 + + + + Skift + + + + + Betalingsfrekvens + Månedlig + + + + Næste betaling + 1. februar 2026 + + + + Beløb + 599,00 kr + + + + Kortudløb + 08/2027 + + + + + Skift til årlig betaling (spar 15%) + + diff --git a/PlanTempus.Application/Features/Account/Components/PaymentMethod/PaymentMethodViewComponent.cs b/PlanTempus.Application/Features/Account/Components/PaymentMethod/PaymentMethodViewComponent.cs new file mode 100644 index 0000000..ef0005a --- /dev/null +++ b/PlanTempus.Application/Features/Account/Components/PaymentMethod/PaymentMethodViewComponent.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace PlanTempus.Application.Features.Account.Components; + +/// +/// ViewComponent for the payment method display. +/// Shows current card info, payment frequency, and next payment date. +/// +public class PaymentMethodViewComponent : ViewComponent +{ + public IViewComponentResult Invoke() + { + return View(); + } +} diff --git a/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml new file mode 100644 index 0000000..7464bb3 --- /dev/null +++ b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/Default.cshtml @@ -0,0 +1,66 @@ +@using PlanTempus.Application.Features.Accounts.Models +@model IEnumerable + +@{ + var currentPlanKey = (string)ViewBag.CurrentPlanKey; +} + + + @foreach (var plan in Model) + { + var isCurrent = plan.Key == currentPlanKey; + var cardClass = plan.Key switch + { + "enterprise" => isCurrent ? "enterprise current" : "enterprise", + _ => isCurrent ? "current" : "" + }; + + var badgeClass = isCurrent ? "current" : plan.BadgeClass; + var badgeText = isCurrent ? "Nuværende plan" : plan.BadgeText; + var badgeIcon = isCurrent ? "ph-check" : plan.BadgeIcon; + + var buttonText = isCurrent + ? "Nuværende plan" + : plan.IsContactSales + ? "Kontakt salg" + : $"Skift til {plan.Name}"; + + var buttonClass = isCurrent + ? "secondary" + : plan.IsContactSales + ? "outline" + : "secondary"; + + + + + @badgeText + + @plan.Name + @plan.UserRange + + @if (plan.PricePerMonth.HasValue) + { + @plan.PriceDisplay + kr/md + } + else + { + Kontakt os + } + + + @foreach (var feature in plan.Features) + { + + + @feature + + } + + + @buttonText + + + } + diff --git a/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs new file mode 100644 index 0000000..c0ad730 --- /dev/null +++ b/PlanTempus.Application/Features/Account/Components/SubscriptionPlans/SubscriptionPlansViewComponent.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Accounts.Models; + +namespace PlanTempus.Application.Features.Account.Components; + +/// +/// ViewComponent for the subscription plan selection grid. +/// Shows all available plans with the current plan highlighted. +/// +public class SubscriptionPlansViewComponent : ViewComponent +{ + public IViewComponentResult Invoke() + { + var plans = PlanCatalog.GetAllPlans(); + // Mock: current plan is "pro" + var currentPlanKey = "pro"; + + ViewBag.CurrentPlanKey = currentPlanKey; + return View(plans); + } +} diff --git a/PlanTempus.Application/Features/Account/Pages/Index.cshtml b/PlanTempus.Application/Features/Account/Pages/Index.cshtml new file mode 100644 index 0000000..13b2920 --- /dev/null +++ b/PlanTempus.Application/Features/Account/Pages/Index.cshtml @@ -0,0 +1,45 @@ +@page "/konto" +@using PlanTempus.Application.Features.Account.Pages +@model PlanTempus.Application.Features.Account.Pages.IndexModel +@{ + ViewData["Title"] = "Abonnement & Konto"; +} + + + + + + +

Abonnement & Konto

+

Administrer dit abonnement og betalingsinfo

+
+
+ + + + + + + Dit abonnement + + + + @await Component.InvokeAsync("SubscriptionPlans") + + + + + + + + Betaling & Fakturaer + + + + + @await Component.InvokeAsync("PaymentMethod") + @await Component.InvokeAsync("InvoiceHistory") + + + +
diff --git a/PlanTempus.Application/Features/Account/Pages/Index.cshtml.cs b/PlanTempus.Application/Features/Account/Pages/Index.cshtml.cs new file mode 100644 index 0000000..56b60ca --- /dev/null +++ b/PlanTempus.Application/Features/Account/Pages/Index.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PlanTempus.Application.Features.Account.Pages; + +public class IndexModel : PageModel +{ + public void OnGet() + { + } +} diff --git a/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs b/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs index 5d8221a..a728e0b 100644 --- a/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs +++ b/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs @@ -176,7 +176,7 @@ public class MockMenuService : IMenuService Id = "account", Label = "Abonnement & Konto", Icon = "ph-credit-card", - Url = "/poc-konto.html", + Url = "/konto", MinimumRole = UserRole.Admin, SortOrder = 2 } diff --git a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml index 9b72be5..7ce5429 100644 --- a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml +++ b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml @@ -25,6 +25,8 @@ + + @await RenderSectionAsync("Styles", required: false) diff --git a/PlanTempus.Application/wwwroot/css/account.css b/PlanTempus.Application/wwwroot/css/account.css new file mode 100644 index 0000000..908ab38 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/account.css @@ -0,0 +1,401 @@ +/** + * Account Styles - Subscription & Billing Management + * + * For logged-in users to manage their subscription plan, + * payment method, and view invoice history. + */ + +/* =========================================== + SECTION STYLING + =========================================== */ +swp-account-section { + display: block; + margin-bottom: var(--spacing-8); +} + +swp-account-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-4); +} + +swp-account-section-title { + display: flex; + align-items: center; + gap: var(--spacing-3); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} + +swp-account-section-title i { + font-size: 22px; + color: var(--color-teal); +} + +/* =========================================== + PLAN GRID + =========================================== */ +swp-plan-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-5); +} + +@media (max-width: 1024px) { + swp-plan-grid { + grid-template-columns: 1fr; + } +} + +/* Plan card current state (extends auth.css) */ +swp-plan-card.current { + border-color: var(--color-teal); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-teal) 15%, transparent); +} + +swp-plan-badge.current { + background: color-mix(in srgb, var(--color-teal) 15%, transparent); + color: var(--color-teal); +} + +/* Disabled button for current plan */ +swp-plan-card.current swp-btn.secondary { + background: var(--color-background-alt); + color: var(--color-text-secondary); + cursor: default; + pointer-events: none; +} + +/* Enterprise plan styling */ +swp-plan-card.enterprise { + background: linear-gradient(135deg, var(--color-surface) 0%, color-mix(in srgb, var(--color-purple) 5%, var(--color-surface)) 100%); + border-color: var(--color-purple); +} + +swp-plan-badge.popular { + background: color-mix(in srgb, var(--color-amber) 15%, transparent); + color: var(--color-amber); +} + +/* Plan action buttons */ +swp-plan-action { + margin-top: auto; + padding-top: var(--spacing-5); +} + +swp-plan-action swp-btn { + width: 100%; + justify-content: center; +} + +/* =========================================== + BILLING GRID (2 columns) + =========================================== */ +swp-billing-grid { + display: grid; + grid-template-columns: 380px 1fr; + gap: var(--spacing-6); +} + +@media (max-width: 1024px) { + swp-billing-grid { + grid-template-columns: 1fr; + } +} + +/* =========================================== + PAYMENT CARD + =========================================== */ +swp-payment-card { + display: flex; + flex-direction: column; + gap: var(--spacing-5); + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + padding: var(--spacing-6); +} + +swp-payment-method { + display: flex; + align-items: center; + gap: var(--spacing-4); + padding: var(--spacing-4); + background: var(--color-background-alt); + border-radius: var(--radius-md); +} + +swp-payment-icon { + width: 48px; + height: 32px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +swp-payment-icon i { + font-size: 24px; + color: var(--color-blue); +} + +swp-payment-info { + flex: 1; + min-width: 0; +} + +swp-payment-type { + display: block; + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + margin-bottom: 2px; +} + +swp-payment-number { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + font-family: var(--font-mono); +} + +swp-payment-detail { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-3) 0; + border-bottom: 1px solid var(--color-border); +} + +swp-payment-detail:last-of-type { + border-bottom: none; +} + +swp-payment-label { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +swp-payment-value { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--color-text); +} + +swp-payment-value.highlight { + color: var(--color-teal); +} + +/* =========================================== + INVOICES CARD + =========================================== */ +swp-invoices-card { + display: block; + background: var(--color-surface); + border-radius: var(--radius-lg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + overflow: hidden; +} + +swp-invoices-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-4) var(--spacing-5); + border-bottom: 1px solid var(--color-border); +} + +swp-invoices-title { + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} + +/* =========================================== + INVOICE TABLE (Grid + Subgrid) + =========================================== */ +swp-invoice-table { + display: grid; + grid-template-columns: 100px minmax(120px, 1fr) 100px 100px 80px; +} + +swp-invoice-table-header { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + background: var(--color-background-alt); +} + +swp-invoice-table-body { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; +} + +swp-invoice-row { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + align-items: center; + border-bottom: 1px solid var(--color-border); + transition: background var(--transition-fast); +} + +swp-invoice-table-body swp-invoice-row:hover { + background: var(--color-background-hover); +} + +swp-invoice-row:last-child { + border-bottom: none; +} + +swp-invoice-cell { + padding: var(--spacing-3) var(--spacing-4); + font-size: var(--font-size-sm); + color: var(--color-text); +} + +swp-invoice-cell:first-child { + padding-left: var(--spacing-5); +} + +swp-invoice-cell:last-child { + padding-right: var(--spacing-5); +} + +/* Header cells */ +swp-invoice-table-header swp-invoice-cell { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); + padding-top: var(--spacing-3); + padding-bottom: var(--spacing-3); +} + +/* Invoice number mono font */ +swp-invoice-cell.mono { + font-family: var(--font-mono); + font-size: var(--font-size-xs); +} + +/* =========================================== + INVOICE STATUS BADGES + =========================================== */ +swp-invoice-status { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-1) var(--spacing-3); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-sm); +} + +swp-invoice-status.paid { + background: color-mix(in srgb, var(--color-green) 15%, transparent); + color: var(--color-green); +} + +swp-invoice-status.pending { + background: color-mix(in srgb, var(--color-amber) 15%, transparent); + color: var(--color-amber); +} + +swp-invoice-status.overdue { + background: color-mix(in srgb, var(--color-red) 15%, transparent); + color: var(--color-red); +} + +swp-invoice-status i { + font-size: 14px; +} + +/* =========================================== + DOWNLOAD BUTTON + =========================================== */ +swp-download-btn { + display: inline-flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-3); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-teal); + background: transparent; + border: 1px solid var(--color-teal); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-download-btn:hover { + background: color-mix(in srgb, var(--color-teal) 10%, transparent); +} + +swp-download-btn i { + font-size: 14px; +} + +/* =========================================== + BUTTONS (account-specific) + =========================================== */ +swp-btn.secondary { + background: var(--color-surface); + border: 1px solid var(--color-border); + color: var(--color-text); +} + +swp-btn.secondary:hover { + background: var(--color-background-hover); +} + +swp-btn.outline { + background: transparent; + border: 1px solid var(--color-teal); + color: var(--color-teal); +} + +swp-btn.outline:hover { + background: color-mix(in srgb, var(--color-teal) 10%, transparent); +} + +swp-btn.sm { + padding: var(--spacing-2) var(--spacing-3); + font-size: var(--font-size-xs); +} + +swp-btn.sm i { + font-size: 14px; +} + +/* =========================================== + RESPONSIVE + =========================================== */ +@media (max-width: 768px) { + swp-invoice-table { + grid-template-columns: 80px minmax(100px, 1fr) 80px 80px 70px; + } + + swp-invoice-cell { + padding: var(--spacing-2) var(--spacing-3); + } + + swp-invoice-cell:first-child { + padding-left: var(--spacing-4); + } + + swp-invoice-cell:last-child { + padding-right: var(--spacing-4); + } + + swp-payment-card { + padding: var(--spacing-4); + } +}