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,117 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
using System.Globalization;
|
||||
|
||||
namespace PlanTempus.Application.Features.Customers.Components;
|
||||
|
||||
public class CustomerDetailActivityViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
public CustomerDetailActivityViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string customerId)
|
||||
{
|
||||
var customer = CustomerDetailCatalog.Get(customerId);
|
||||
var culture = new CultureInfo("da-DK");
|
||||
var today = DateTime.Today;
|
||||
|
||||
// Group activities by date
|
||||
var groupedActivities = customer.Activity
|
||||
.OrderByDescending(a => DateTime.Parse(a.Date))
|
||||
.ThenByDescending(a => a.Time)
|
||||
.GroupBy(a => a.Date)
|
||||
.Select(g =>
|
||||
{
|
||||
var date = DateTime.Parse(g.Key);
|
||||
string dateHeader;
|
||||
if (date.Date == today)
|
||||
{
|
||||
dateHeader = "I dag";
|
||||
}
|
||||
else if (date.Date == today.AddDays(-1))
|
||||
{
|
||||
dateHeader = "I gar";
|
||||
}
|
||||
else
|
||||
{
|
||||
dateHeader = date.ToString("d. MMMM yyyy", culture);
|
||||
}
|
||||
|
||||
return new ActivityDateGroupViewModel
|
||||
{
|
||||
DateHeader = dateHeader,
|
||||
Items = g.Select(a => new ActivityItemViewModel
|
||||
{
|
||||
Time = a.Time,
|
||||
Type = a.Type,
|
||||
Icon = a.Icon,
|
||||
Title = a.Title,
|
||||
Actor = a.Actor,
|
||||
Badges = a.Badges.Select(b => new ActivityBadgeViewModel
|
||||
{
|
||||
Text = char.ToUpper(b[0]) + b[1..],
|
||||
CssClass = b.ToLowerInvariant()
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var model = new CustomerDetailActivityViewModel
|
||||
{
|
||||
DateGroups = groupedActivities,
|
||||
Filters = new List<ActivityFilterViewModel>
|
||||
{
|
||||
new() { Label = "Alle", Type = null, Icon = null, IsActive = true },
|
||||
new() { Label = "Bookinger", Type = "booking", Icon = "calendar" },
|
||||
new() { Label = "Kommunikation", Type = "communication", Icon = "envelope" },
|
||||
new() { Label = "Ændringer", Type = "edit", Icon = "pencil-simple" },
|
||||
new() { Label = "Betalinger", Type = "payment", Icon = "credit-card" },
|
||||
new() { Label = "Advarsler", Type = "warning", Icon = "warning" },
|
||||
new() { Label = "Kunde", Type = "customer", Icon = "user" }
|
||||
}
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomerDetailActivityViewModel
|
||||
{
|
||||
public List<ActivityDateGroupViewModel> DateGroups { get; init; } = new();
|
||||
public List<ActivityFilterViewModel> Filters { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ActivityFilterViewModel
|
||||
{
|
||||
public required string Label { get; init; }
|
||||
public string? Type { get; init; }
|
||||
public string? Icon { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
}
|
||||
|
||||
public class ActivityDateGroupViewModel
|
||||
{
|
||||
public required string DateHeader { get; init; }
|
||||
public List<ActivityItemViewModel> Items { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ActivityItemViewModel
|
||||
{
|
||||
public required string Time { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Icon { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string? Actor { get; init; }
|
||||
public List<ActivityBadgeViewModel> Badges { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ActivityBadgeViewModel
|
||||
{
|
||||
public required string Text { get; init; }
|
||||
public required string CssClass { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
@model PlanTempus.Application.Features.Customers.Components.CustomerDetailActivityViewModel
|
||||
|
||||
<!-- Filters -->
|
||||
<swp-activity-filters>
|
||||
@foreach (var filter in Model.Filters)
|
||||
{
|
||||
<swp-activity-filter class="@(filter.IsActive ? "active" : "")" data-type="@(filter.Type ?? "all")">
|
||||
@if (!string.IsNullOrEmpty(filter.Icon))
|
||||
{
|
||||
<i class="ph ph-@filter.Icon"></i>
|
||||
}
|
||||
@filter.Label
|
||||
</swp-activity-filter>
|
||||
}
|
||||
</swp-activity-filters>
|
||||
|
||||
<swp-card>
|
||||
<swp-activity-timeline>
|
||||
@foreach (var dateGroup in Model.DateGroups)
|
||||
{
|
||||
<swp-activity-date-group>
|
||||
<swp-activity-date-header>@dateGroup.DateHeader</swp-activity-date-header>
|
||||
|
||||
@foreach (var item in dateGroup.Items)
|
||||
{
|
||||
<swp-activity-item data-type="@item.Type">
|
||||
<swp-activity-icon class="@item.Type"><i class="ph ph-@item.Icon"></i></swp-activity-icon>
|
||||
<swp-activity-content>
|
||||
<swp-activity-title>
|
||||
@Html.Raw(item.Title)
|
||||
@foreach (var badge in item.Badges)
|
||||
{
|
||||
<swp-activity-badge class="@badge.CssClass">@badge.Text</swp-activity-badge>
|
||||
}
|
||||
</swp-activity-title>
|
||||
<swp-activity-meta>
|
||||
<swp-activity-time>@item.Time</swp-activity-time>
|
||||
@if (!string.IsNullOrEmpty(item.Actor))
|
||||
{
|
||||
<swp-activity-actor>@item.Actor</swp-activity-actor>
|
||||
}
|
||||
</swp-activity-meta>
|
||||
</swp-activity-content>
|
||||
</swp-activity-item>
|
||||
}
|
||||
</swp-activity-date-group>
|
||||
}
|
||||
</swp-activity-timeline>
|
||||
</swp-card>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const filters = document.querySelectorAll('swp-activity-filter');
|
||||
const timeline = document.querySelector('swp-activity-timeline');
|
||||
|
||||
filters.forEach(filter => {
|
||||
filter.addEventListener('click', () => {
|
||||
// Update active state
|
||||
filters.forEach(f => f.classList.remove('active'));
|
||||
filter.classList.add('active');
|
||||
|
||||
const filterType = filter.dataset.type;
|
||||
|
||||
// Filter items
|
||||
const items = timeline.querySelectorAll('swp-activity-item');
|
||||
items.forEach(item => {
|
||||
if (filterType === 'all' || item.dataset.type === filterType) {
|
||||
item.style.display = '';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Hide date groups with no visible items
|
||||
const dateGroups = timeline.querySelectorAll('swp-activity-date-group');
|
||||
dateGroups.forEach(group => {
|
||||
const visibleItems = group.querySelectorAll('swp-activity-item:not([style*="display: none"])');
|
||||
group.style.display = visibleItems.length > 0 ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue