Adds comprehensive customer detail view components

Implements full customer detail page with multiple feature-rich components including overview, economy, statistics, journal, appointments, giftcards, and activity sections

Creates reusable ViewComponents for different customer detail aspects with robust data modeling and presentation logic
This commit is contained in:
Janus C. H. Knudsen 2026-01-25 01:55:41 +01:00
parent 38e9243bcd
commit 1b25978d9b
26 changed files with 3792 additions and 956 deletions

View file

@ -0,0 +1,93 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
using System.Globalization;
namespace PlanTempus.Application.Features.Customers.Components;
public class CustomerDetailGiftcardsViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
public CustomerDetailGiftcardsViewComponent(ILocalizationService localization)
{
_localization = localization;
}
public IViewComponentResult Invoke(string customerId)
{
var customer = CustomerDetailCatalog.Get(customerId);
var culture = new CultureInfo("da-DK");
// Separate giftcards and punchcards
var giftcards = customer.Giftcards.Active.Where(g => g.Type == "giftcard").ToList();
var punchcards = customer.Giftcards.Active.Where(g => g.Type == "punchcard").ToList();
var model = new CustomerDetailGiftcardsViewModel
{
GiftcardsTitle = "Aktive gavekort",
PunchcardsTitle = "Klippekort",
ExpiredTitle = "Udlobne / Brugte",
NoExpiredText = "Ingen udlobne eller brugte kort",
Giftcards = giftcards.Select(g =>
{
var expiresText = "Udlober aldrig";
if (!string.IsNullOrEmpty(g.ExpiresAt) && DateTime.TryParse(g.ExpiresAt, out var expires))
{
expiresText = $"Udlober: {expires.ToString("d. MMMM yyyy", culture)}";
}
var percentage = g.OriginalValue > 0 ? (int)((g.CurrentBalance ?? 0) / g.OriginalValue * 100) : 0;
return new GiftcardItemViewModel
{
Label = g.Label,
BalanceText = $"Saldo: <strong>{g.CurrentBalance:N0} kr</strong> (af {g.OriginalValue:N0} kr)".Replace(",", "."),
ExpiresText = expiresText,
Percentage = percentage
};
}).ToList(),
Punchcards = punchcards.Select(p =>
{
var expiresText = "Udlober aldrig";
if (!string.IsNullOrEmpty(p.ExpiresAt) && DateTime.TryParse(p.ExpiresAt, out var expires))
{
expiresText = $"Udlober: {expires.ToString("d. MMMM yyyy", culture)}";
}
var percentage = p.TotalPunches > 0 ? (int)((p.UsedPunches ?? 0) * 100 / p.TotalPunches) : 0;
return new GiftcardItemViewModel
{
Label = p.Label,
BalanceText = $"Brugt: <strong>{p.UsedPunches} af {p.TotalPunches}</strong> klip",
ExpiresText = expiresText,
Percentage = percentage
};
}).ToList(),
ExpiredCards = customer.Giftcards.Expired.Select(g => new GiftcardItemViewModel
{
Label = g.Label,
BalanceText = "",
ExpiresText = "",
Percentage = 0
}).ToList()
};
return View(model);
}
}
public class CustomerDetailGiftcardsViewModel
{
public required string GiftcardsTitle { get; init; }
public required string PunchcardsTitle { get; init; }
public required string ExpiredTitle { get; init; }
public required string NoExpiredText { get; init; }
public List<GiftcardItemViewModel> Giftcards { get; init; } = new();
public List<GiftcardItemViewModel> Punchcards { get; init; } = new();
public List<GiftcardItemViewModel> ExpiredCards { get; init; } = new();
}
public class GiftcardItemViewModel
{
public required string Label { get; init; }
public required string BalanceText { get; init; }
public required string ExpiresText { get; init; }
public int Percentage { get; init; }
}

View file

@ -0,0 +1,81 @@
@model PlanTempus.Application.Features.Customers.Components.CustomerDetailGiftcardsViewModel
<swp-detail-grid>
<!-- Left Column -->
<swp-card-column>
<!-- Aktive gavekort -->
@if (Model.Giftcards.Any())
{
<swp-card>
<swp-card-header>
<swp-card-title>@Model.GiftcardsTitle</swp-card-title>
</swp-card-header>
@foreach (var giftcard in Model.Giftcards)
{
<swp-giftcard>
<swp-giftcard-header>
@giftcard.Label
</swp-giftcard-header>
<swp-giftcard-balance>
@Html.Raw(giftcard.BalanceText)
</swp-giftcard-balance>
<swp-progress-bar>
<swp-progress-fill style="width: @giftcard.Percentage%;"></swp-progress-fill>
</swp-progress-bar>
<swp-giftcard-expires>@giftcard.ExpiresText</swp-giftcard-expires>
</swp-giftcard>
}
</swp-card>
}
<!-- Klippekort -->
@if (Model.Punchcards.Any())
{
<swp-card>
<swp-card-header>
<swp-card-title>@Model.PunchcardsTitle</swp-card-title>
</swp-card-header>
@foreach (var punchcard in Model.Punchcards)
{
<swp-giftcard>
<swp-giftcard-header>
@punchcard.Label
</swp-giftcard-header>
<swp-giftcard-balance>
@Html.Raw(punchcard.BalanceText)
</swp-giftcard-balance>
<swp-progress-bar>
<swp-progress-fill style="width: @punchcard.Percentage%;"></swp-progress-fill>
</swp-progress-bar>
<swp-giftcard-expires>@punchcard.ExpiresText</swp-giftcard-expires>
</swp-giftcard>
}
</swp-card>
}
</swp-card-column>
<!-- Right Column -->
<swp-card-column>
<!-- Udlobne / Brugte -->
<swp-card>
<swp-card-header>
<swp-card-title>@Model.ExpiredTitle</swp-card-title>
</swp-card-header>
@if (Model.ExpiredCards.Any())
{
@foreach (var card in Model.ExpiredCards)
{
<swp-giftcard class="expired">
<swp-giftcard-header>@card.Label</swp-giftcard-header>
</swp-giftcard>
}
}
else
{
<swp-empty-state>
<p>@Model.NoExpiredText</p>
</swp-empty-state>
}
</swp-card>
</swp-card-column>
</swp-detail-grid>