Adds account management and subscription features

Introduces new account pages for managing subscriptions, payment methods, and invoice history

Includes:
- Subscription plan selection view
- Payment method display component
- Invoice history table
- Account page layout and styling

Updates main layout to include new CSS files for account management
This commit is contained in:
Janus C. H. Knudsen 2026-01-11 22:33:21 +01:00
parent 5e0bd9db74
commit 1f400dcc6e
11 changed files with 724 additions and 1 deletions

View file

@ -0,0 +1,108 @@
<swp-invoices-card>
<swp-invoices-header>
<swp-invoices-title>Faktura-historik</swp-invoices-title>
</swp-invoices-header>
<swp-invoice-table>
<swp-invoice-table-header>
<swp-invoice-row>
<swp-invoice-cell>Dato</swp-invoice-cell>
<swp-invoice-cell>Fakturanr.</swp-invoice-cell>
<swp-invoice-cell>Beløb</swp-invoice-cell>
<swp-invoice-cell>Status</swp-invoice-cell>
<swp-invoice-cell></swp-invoice-cell>
</swp-invoice-row>
</swp-invoice-table-header>
<swp-invoice-table-body>
<swp-invoice-row>
<swp-invoice-cell>1. jan 2026</swp-invoice-cell>
<swp-invoice-cell class="mono">INV-2026-0001</swp-invoice-cell>
<swp-invoice-cell>599,00 kr</swp-invoice-cell>
<swp-invoice-cell>
<swp-invoice-status class="paid">
<i class="ph ph-check-circle"></i>
Betalt
</swp-invoice-status>
</swp-invoice-cell>
<swp-invoice-cell>
<swp-download-btn>
<i class="ph ph-download"></i>
PDF
</swp-download-btn>
</swp-invoice-cell>
</swp-invoice-row>
<swp-invoice-row>
<swp-invoice-cell>1. dec 2025</swp-invoice-cell>
<swp-invoice-cell class="mono">INV-2025-0012</swp-invoice-cell>
<swp-invoice-cell>599,00 kr</swp-invoice-cell>
<swp-invoice-cell>
<swp-invoice-status class="paid">
<i class="ph ph-check-circle"></i>
Betalt
</swp-invoice-status>
</swp-invoice-cell>
<swp-invoice-cell>
<swp-download-btn>
<i class="ph ph-download"></i>
PDF
</swp-download-btn>
</swp-invoice-cell>
</swp-invoice-row>
<swp-invoice-row>
<swp-invoice-cell>1. nov 2025</swp-invoice-cell>
<swp-invoice-cell class="mono">INV-2025-0011</swp-invoice-cell>
<swp-invoice-cell>599,00 kr</swp-invoice-cell>
<swp-invoice-cell>
<swp-invoice-status class="paid">
<i class="ph ph-check-circle"></i>
Betalt
</swp-invoice-status>
</swp-invoice-cell>
<swp-invoice-cell>
<swp-download-btn>
<i class="ph ph-download"></i>
PDF
</swp-download-btn>
</swp-invoice-cell>
</swp-invoice-row>
<swp-invoice-row>
<swp-invoice-cell>1. okt 2025</swp-invoice-cell>
<swp-invoice-cell class="mono">INV-2025-0010</swp-invoice-cell>
<swp-invoice-cell>599,00 kr</swp-invoice-cell>
<swp-invoice-cell>
<swp-invoice-status class="paid">
<i class="ph ph-check-circle"></i>
Betalt
</swp-invoice-status>
</swp-invoice-cell>
<swp-invoice-cell>
<swp-download-btn>
<i class="ph ph-download"></i>
PDF
</swp-download-btn>
</swp-invoice-cell>
</swp-invoice-row>
<swp-invoice-row>
<swp-invoice-cell>1. sep 2025</swp-invoice-cell>
<swp-invoice-cell class="mono">INV-2025-0009</swp-invoice-cell>
<swp-invoice-cell>599,00 kr</swp-invoice-cell>
<swp-invoice-cell>
<swp-invoice-status class="paid">
<i class="ph ph-check-circle"></i>
Betalt
</swp-invoice-status>
</swp-invoice-cell>
<swp-invoice-cell>
<swp-download-btn>
<i class="ph ph-download"></i>
PDF
</swp-download-btn>
</swp-invoice-cell>
</swp-invoice-row>
</swp-invoice-table-body>
</swp-invoice-table>
</swp-invoices-card>

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Account.Components;
/// <summary>
/// ViewComponent for the invoice history table.
/// Shows past invoices with status and download options.
/// </summary>
public class InvoiceHistoryViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,40 @@
<swp-payment-card>
<swp-payment-method>
<swp-payment-icon>
<i class="ph ph-credit-card"></i>
</swp-payment-icon>
<swp-payment-info>
<swp-payment-type>Visa</swp-payment-type>
<swp-payment-number>**** **** **** 4582</swp-payment-number>
</swp-payment-info>
<swp-btn class="secondary sm">
<i class="ph ph-pencil"></i>
Skift
</swp-btn>
</swp-payment-method>
<swp-payment-detail>
<swp-payment-label>Betalingsfrekvens</swp-payment-label>
<swp-payment-value>Månedlig</swp-payment-value>
</swp-payment-detail>
<swp-payment-detail>
<swp-payment-label>Næste betaling</swp-payment-label>
<swp-payment-value class="highlight">1. februar 2026</swp-payment-value>
</swp-payment-detail>
<swp-payment-detail>
<swp-payment-label>Beløb</swp-payment-label>
<swp-payment-value>599,00 kr</swp-payment-value>
</swp-payment-detail>
<swp-payment-detail>
<swp-payment-label>Kortudløb</swp-payment-label>
<swp-payment-value>08/2027</swp-payment-value>
</swp-payment-detail>
<swp-btn class="outline" style="margin-top: var(--spacing-2);">
<i class="ph ph-arrows-clockwise"></i>
Skift til årlig betaling (spar 15%)
</swp-btn>
</swp-payment-card>

View file

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Account.Components;
/// <summary>
/// ViewComponent for the payment method display.
/// Shows current card info, payment frequency, and next payment date.
/// </summary>
public class PaymentMethodViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,66 @@
@using PlanTempus.Application.Features.Accounts.Models
@model IEnumerable<PlanInfo>
@{
var currentPlanKey = (string)ViewBag.CurrentPlanKey;
}
<swp-plan-grid>
@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";
<swp-plan-card class="@cardClass">
<swp-plan-badge class="@badgeClass">
<i class="ph @badgeIcon"></i>
@badgeText
</swp-plan-badge>
<swp-plan-name>@plan.Name</swp-plan-name>
<swp-plan-users>@plan.UserRange</swp-plan-users>
<swp-plan-price>
@if (plan.PricePerMonth.HasValue)
{
<swp-plan-price-amount>@plan.PriceDisplay</swp-plan-price-amount>
<swp-plan-price-period>kr/md</swp-plan-price-period>
}
else
{
<swp-plan-price-amount>Kontakt os</swp-plan-price-amount>
}
</swp-plan-price>
<swp-plan-features>
@foreach (var feature in plan.Features)
{
<swp-plan-feature>
<i class="ph ph-check-circle"></i>
@feature
</swp-plan-feature>
}
</swp-plan-features>
<swp-plan-action>
<swp-btn class="@buttonClass">@buttonText</swp-btn>
</swp-plan-action>
</swp-plan-card>
}
</swp-plan-grid>

View file

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Accounts.Models;
namespace PlanTempus.Application.Features.Account.Components;
/// <summary>
/// ViewComponent for the subscription plan selection grid.
/// Shows all available plans with the current plan highlighted.
/// </summary>
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);
}
}

View file

@ -0,0 +1,45 @@
@page "/konto"
@using PlanTempus.Application.Features.Account.Pages
@model PlanTempus.Application.Features.Account.Pages.IndexModel
@{
ViewData["Title"] = "Abonnement & Konto";
}
<swp-page-container>
<!-- Page Header -->
<swp-page-header>
<swp-page-title>
<h1>Abonnement & Konto</h1>
<p>Administrer dit abonnement og betalingsinfo</p>
</swp-page-title>
</swp-page-header>
<!-- Subscription Section -->
<swp-account-section>
<swp-account-section-header>
<swp-account-section-title>
<i class="ph ph-crown"></i>
Dit abonnement
</swp-account-section-title>
</swp-account-section-header>
@await Component.InvokeAsync("SubscriptionPlans")
</swp-account-section>
<!-- Billing Section -->
<swp-account-section>
<swp-account-section-header>
<swp-account-section-title>
<i class="ph ph-credit-card"></i>
Betaling & Fakturaer
</swp-account-section-title>
</swp-account-section-header>
<swp-billing-grid>
@await Component.InvokeAsync("PaymentMethod")
@await Component.InvokeAsync("InvoiceHistory")
</swp-billing-grid>
</swp-account-section>
</swp-page-container>

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PlanTempus.Application.Features.Account.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}

View file

@ -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
}

View file

@ -25,6 +25,8 @@
<link rel="stylesheet" href="~/css/waitlist.css">
<link rel="stylesheet" href="~/css/tabs.css">
<link rel="stylesheet" href="~/css/cash.css">
<link rel="stylesheet" href="~/css/auth.css">
<link rel="stylesheet" href="~/css/account.css">
@await RenderSectionAsync("Styles", required: false)
</head>
<body class="has-demo-banner">

View file

@ -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);
}
}