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:
parent
38e9243bcd
commit
1b25978d9b
26 changed files with 3792 additions and 956 deletions
|
|
@ -0,0 +1,145 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Customers.Components;
|
||||
|
||||
public class CustomerDetailEconomyViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
public CustomerDetailEconomyViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string customerId)
|
||||
{
|
||||
var customer = CustomerDetailCatalog.Get(customerId);
|
||||
var economy = customer.Economy;
|
||||
|
||||
var model = new CustomerDetailEconomyViewModel
|
||||
{
|
||||
HasData = economy != null,
|
||||
|
||||
// Stat cards
|
||||
CurrentYearValue = economy != null ? $"{economy.CurrentYear.Total:N0} kr".Replace(",", ".") : "-",
|
||||
CurrentYearLabel = string.Format(_localization.Get("customers.detail.economy.thisYear"), economy?.CurrentYear.Year ?? DateTime.Now.Year),
|
||||
LastYearValue = economy != null ? $"{economy.LastYear.Total:N0} kr".Replace(",", ".") : "-",
|
||||
LastYearLabel = _localization.Get("customers.detail.economy.lastYear"),
|
||||
AvgPerVisitValue = economy != null ? $"{economy.AvgPerVisit:N0} kr".Replace(",", ".") : "-",
|
||||
AvgPerVisitLabel = _localization.Get("customers.detail.economy.avgPerVisit"),
|
||||
AvgPerMonthValue = economy != null ? $"{economy.AvgPerMonth:N0} kr".Replace(",", ".") : "-",
|
||||
AvgPerMonthLabel = _localization.Get("customers.detail.economy.avgPerMonth"),
|
||||
|
||||
// Chart card
|
||||
RevenueOverTimeTitle = _localization.Get("customers.detail.economy.revenueOverTime"),
|
||||
ServicesLabel = _localization.Get("customers.detail.economy.services"),
|
||||
ProductsLabel = _localization.Get("customers.detail.economy.products"),
|
||||
ChartData = economy?.ChartData != null ? new CustomerChartDataViewModel
|
||||
{
|
||||
Categories = economy.ChartData.Categories,
|
||||
Series = economy.ChartData.Series.Select(s => new CustomerChartSeriesViewModel
|
||||
{
|
||||
Name = s.Name,
|
||||
Color = s.Color,
|
||||
Data = s.Data.Select(d => new CustomerChartDataPointViewModel
|
||||
{
|
||||
X = d.X,
|
||||
Y = d.Y
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
} : null,
|
||||
|
||||
// Purchase history
|
||||
PurchaseHistoryTitle = _localization.Get("customers.detail.economy.purchaseHistory"),
|
||||
Purchases = economy?.Purchases.Take(5).Select(p => new CustomerPurchaseViewModel
|
||||
{
|
||||
Invoice = p.Invoice,
|
||||
Date = FormatDate(p.Date),
|
||||
Time = p.Time,
|
||||
Employee = p.Employee,
|
||||
Services = p.Services,
|
||||
Type = p.Type,
|
||||
TypeLabel = p.Type == "service"
|
||||
? _localization.Get("customers.detail.economy.services")
|
||||
: _localization.Get("customers.detail.economy.products"),
|
||||
Amount = $"{p.Amount:N0} kr".Replace(",", ".")
|
||||
}).ToList() ?? new List<CustomerPurchaseViewModel>(),
|
||||
SeeAllText = _localization.Get("customers.detail.economy.seeAll"),
|
||||
|
||||
// Empty state
|
||||
EmptyStateText = _localization.Get("customers.detail.economy.noData")
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private static string FormatDate(string dateStr)
|
||||
{
|
||||
if (DateTime.TryParse(dateStr, out var date))
|
||||
{
|
||||
return date.ToString("d. MMM yyyy", new System.Globalization.CultureInfo("da-DK"));
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomerDetailEconomyViewModel
|
||||
{
|
||||
public bool HasData { get; init; }
|
||||
|
||||
// Stat cards
|
||||
public required string CurrentYearValue { get; init; }
|
||||
public required string CurrentYearLabel { get; init; }
|
||||
public required string LastYearValue { get; init; }
|
||||
public required string LastYearLabel { get; init; }
|
||||
public required string AvgPerVisitValue { get; init; }
|
||||
public required string AvgPerVisitLabel { get; init; }
|
||||
public required string AvgPerMonthValue { get; init; }
|
||||
public required string AvgPerMonthLabel { get; init; }
|
||||
|
||||
// Chart card
|
||||
public required string RevenueOverTimeTitle { get; init; }
|
||||
public required string ServicesLabel { get; init; }
|
||||
public required string ProductsLabel { get; init; }
|
||||
public CustomerChartDataViewModel? ChartData { get; init; }
|
||||
|
||||
// Purchase history
|
||||
public required string PurchaseHistoryTitle { get; init; }
|
||||
public List<CustomerPurchaseViewModel> Purchases { get; init; } = new();
|
||||
public required string SeeAllText { get; init; }
|
||||
|
||||
// Empty state
|
||||
public required string EmptyStateText { get; init; }
|
||||
}
|
||||
|
||||
public class CustomerPurchaseViewModel
|
||||
{
|
||||
public required string Invoice { get; init; }
|
||||
public required string Date { get; init; }
|
||||
public required string Time { get; init; }
|
||||
public required string Employee { get; init; }
|
||||
public required string Services { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string TypeLabel { get; init; }
|
||||
public required string Amount { get; init; }
|
||||
}
|
||||
|
||||
public class CustomerChartDataViewModel
|
||||
{
|
||||
public List<string> Categories { get; init; } = new();
|
||||
public List<CustomerChartSeriesViewModel> Series { get; init; } = new();
|
||||
}
|
||||
|
||||
public class CustomerChartSeriesViewModel
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string Color { get; init; }
|
||||
public List<CustomerChartDataPointViewModel> Data { get; init; } = new();
|
||||
}
|
||||
|
||||
public class CustomerChartDataPointViewModel
|
||||
{
|
||||
public required string X { get; init; }
|
||||
public decimal Y { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
@using System.Text.Json
|
||||
@model PlanTempus.Application.Features.Customers.Components.CustomerDetailEconomyViewModel
|
||||
|
||||
@if (!Model.HasData)
|
||||
{
|
||||
<swp-empty-state>
|
||||
<i class="ph ph-currency-circle-dollar"></i>
|
||||
<span>@Model.EmptyStateText</span>
|
||||
</swp-empty-state>
|
||||
}
|
||||
else
|
||||
{
|
||||
<swp-detail-grid>
|
||||
<!-- Stat Cards -->
|
||||
<swp-stats-row class="cols-4 full-width">
|
||||
<swp-stat-card class="highlight">
|
||||
<swp-stat-value>@Model.CurrentYearValue</swp-stat-value>
|
||||
<swp-stat-label>@Model.CurrentYearLabel</swp-stat-label>
|
||||
</swp-stat-card>
|
||||
<swp-stat-card>
|
||||
<swp-stat-value>@Model.LastYearValue</swp-stat-value>
|
||||
<swp-stat-label>@Model.LastYearLabel</swp-stat-label>
|
||||
</swp-stat-card>
|
||||
<swp-stat-card>
|
||||
<swp-stat-value>@Model.AvgPerVisitValue</swp-stat-value>
|
||||
<swp-stat-label>@Model.AvgPerVisitLabel</swp-stat-label>
|
||||
</swp-stat-card>
|
||||
<swp-stat-card>
|
||||
<swp-stat-value>@Model.AvgPerMonthValue</swp-stat-value>
|
||||
<swp-stat-label>@Model.AvgPerMonthLabel</swp-stat-label>
|
||||
</swp-stat-card>
|
||||
</swp-stats-row>
|
||||
|
||||
<!-- Revenue Chart Card -->
|
||||
<swp-card class="full-width">
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.RevenueOverTimeTitle</swp-card-title>
|
||||
<swp-chart-legend>
|
||||
<swp-chart-legend-item>
|
||||
<swp-chart-legend-dot class="services"></swp-chart-legend-dot>
|
||||
<span>@Model.ServicesLabel</span>
|
||||
</swp-chart-legend-item>
|
||||
<swp-chart-legend-item>
|
||||
<swp-chart-legend-dot class="products"></swp-chart-legend-dot>
|
||||
<span>@Model.ProductsLabel</span>
|
||||
</swp-chart-legend-item>
|
||||
</swp-chart-legend>
|
||||
</swp-card-header>
|
||||
@if (Model.ChartData != null)
|
||||
{
|
||||
<swp-chart-container id="customerRevenueChart"></swp-chart-container>
|
||||
<script id="customerRevenueChartData" type="application/json">@Html.Raw(JsonSerializer.Serialize(Model.ChartData, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }))</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<swp-chart-container>
|
||||
<!-- No chart data available -->
|
||||
</swp-chart-container>
|
||||
}
|
||||
</swp-card>
|
||||
|
||||
<!-- Purchase History Card -->
|
||||
<swp-card class="customer-economy full-width">
|
||||
<swp-card-header>
|
||||
<swp-card-title>@Model.PurchaseHistoryTitle</swp-card-title>
|
||||
</swp-card-header>
|
||||
<swp-data-table>
|
||||
<swp-data-table-header>
|
||||
<swp-data-table-cell>Faktura</swp-data-table-cell>
|
||||
<swp-data-table-cell>Dato/tid</swp-data-table-cell>
|
||||
<swp-data-table-cell>Medarbejder</swp-data-table-cell>
|
||||
<swp-data-table-cell>Ydelser</swp-data-table-cell>
|
||||
<swp-data-table-cell>Type</swp-data-table-cell>
|
||||
<swp-data-table-cell>Beløb</swp-data-table-cell>
|
||||
</swp-data-table-header>
|
||||
@foreach (var purchase in Model.Purchases)
|
||||
{
|
||||
<swp-data-table-row>
|
||||
<swp-data-table-cell>
|
||||
<swp-invoice-cell>@purchase.Invoice</swp-invoice-cell>
|
||||
</swp-data-table-cell>
|
||||
<swp-data-table-cell>
|
||||
<swp-datetime-cell>
|
||||
<span class="date">@purchase.Date</span>
|
||||
<span class="time">@purchase.Time</span>
|
||||
</swp-datetime-cell>
|
||||
</swp-data-table-cell>
|
||||
<swp-data-table-cell class="muted">@purchase.Employee</swp-data-table-cell>
|
||||
<swp-data-table-cell>@purchase.Services</swp-data-table-cell>
|
||||
<swp-data-table-cell>
|
||||
<swp-tag class="@purchase.Type">@purchase.TypeLabel</swp-tag>
|
||||
</swp-data-table-cell>
|
||||
<swp-data-table-cell>
|
||||
<swp-amount-cell>@purchase.Amount</swp-amount-cell>
|
||||
</swp-data-table-cell>
|
||||
</swp-data-table-row>
|
||||
}
|
||||
</swp-data-table>
|
||||
<swp-see-all>@Model.SeeAllText</swp-see-all>
|
||||
</swp-card>
|
||||
</swp-detail-grid>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue