Adds comprehensive customers list and management components

Introduces customer-related view components for table and row display
Implements mock data loading and customer list rendering
Adds localization support for customer-related text
Enhances UI with detailed customer information and interactions
This commit is contained in:
Janus C. H. Knudsen 2026-01-21 18:00:53 +01:00
parent cd7acaf490
commit 6ef001e35f
11 changed files with 869 additions and 675 deletions

View file

@ -0,0 +1,139 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Customers.Components;
public class CustomerTableViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
private readonly IWebHostEnvironment _env;
public CustomerTableViewComponent(ILocalizationService localization, IWebHostEnvironment env)
{
_localization = localization;
_env = env;
}
public IViewComponentResult Invoke()
{
var data = LoadCustomerData();
var model = new CustomerTableViewModel
{
SearchPlaceholder = _localization.Get("customers.searchPlaceholder"),
ExportButtonText = _localization.Get("customers.export"),
CreateButtonText = _localization.Get("customers.create"),
ColumnName = _localization.Get("customers.column.name"),
ColumnPhone = _localization.Get("customers.column.phone"),
ColumnEmail = _localization.Get("customers.column.email"),
ColumnVisits = _localization.Get("customers.column.visits"),
ColumnLastVisit = _localization.Get("customers.column.lastVisit"),
ColumnHairdresser = _localization.Get("customers.column.hairdresser"),
ColumnCreated = _localization.Get("customers.column.created"),
ColumnTags = _localization.Get("customers.column.tags"),
EmptySearchText = _localization.Get("customers.emptySearch"),
Customers = data.Customers
.OrderBy(c => c.FirstName)
.ThenBy(c => c.LastName)
.Select(c => new CustomerItemViewModel
{
Id = c.Id,
FullName = $"{c.FirstName} {c.LastName}",
Initials = c.Initials,
Phone = c.Phone,
Email = c.Email,
Visits = c.Visits,
LastVisit = FormatLastVisit(c.LastVisit),
PreferredHairdresser = c.PreferredHairdresser,
CreatedAt = FormatCreatedAt(c.CreatedAt),
Tags = c.Tags,
AvatarColor = c.AvatarColor
})
.ToList()
};
return View(model);
}
private CustomerMockData LoadCustomerData()
{
var jsonPath = Path.Combine(_env.ContentRootPath, "Features", "Customers", "Data", "customersMock.json");
var json = System.IO.File.ReadAllText(jsonPath);
return JsonSerializer.Deserialize<CustomerMockData>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new CustomerMockData();
}
private static string FormatLastVisit(string dateStr)
{
if (DateTime.TryParse(dateStr, out var date))
{
return date.ToString("d. MMM", new CultureInfo("da-DK")).TrimEnd('.');
}
return dateStr;
}
private static string FormatCreatedAt(string dateStr)
{
if (DateTime.TryParse(dateStr, out var date))
{
return date.ToString("MMM yyyy", new CultureInfo("da-DK"));
}
return dateStr;
}
}
public class CustomerTableViewModel
{
public required string SearchPlaceholder { get; init; }
public required string ExportButtonText { get; init; }
public required string CreateButtonText { get; init; }
public required string ColumnName { get; init; }
public required string ColumnPhone { get; init; }
public required string ColumnEmail { get; init; }
public required string ColumnVisits { get; init; }
public required string ColumnLastVisit { get; init; }
public required string ColumnHairdresser { get; init; }
public required string ColumnCreated { get; init; }
public required string ColumnTags { get; init; }
public required string EmptySearchText { get; init; }
public required IReadOnlyList<CustomerItemViewModel> Customers { get; init; }
}
public class CustomerItemViewModel
{
public required string Id { get; init; }
public required string FullName { get; init; }
public required string Initials { get; init; }
public required string Phone { get; init; }
public required string Email { get; init; }
public int Visits { get; init; }
public required string LastVisit { get; init; }
public required string PreferredHairdresser { get; init; }
public required string CreatedAt { get; init; }
public required IReadOnlyList<string> Tags { get; init; }
public string? AvatarColor { get; init; }
}
internal class CustomerMockData
{
public List<CustomerData> Customers { get; set; } = new();
}
internal class CustomerData
{
public string Id { get; set; } = "";
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public string Initials { get; set; } = "";
public string Phone { get; set; } = "";
public string Email { get; set; } = "";
public int Visits { get; set; }
public string LastVisit { get; set; } = "";
public string PreferredHairdresser { get; set; } = "";
public string CreatedAt { get; set; } = "";
public List<string> Tags { get; set; } = new();
public string? AvatarColor { get; set; }
}

View file

@ -0,0 +1,42 @@
@model PlanTempus.Application.Features.Customers.Components.CustomerTableViewModel
<swp-action-bar>
<swp-search-input>
<i class="ph ph-magnifying-glass"></i>
<input type="text" id="searchInput" placeholder="@Model.SearchPlaceholder" />
</swp-search-input>
<swp-btn-group>
<swp-btn class="secondary">
<i class="ph ph-export"></i>
<span>@Model.ExportButtonText</span>
</swp-btn>
<swp-btn class="primary">
<i class="ph ph-plus"></i>
<span>@Model.CreateButtonText</span>
</swp-btn>
</swp-btn-group>
</swp-action-bar>
<swp-card class="customers-list">
<swp-data-table>
<swp-data-table-header>
<swp-data-table-cell>@Model.ColumnName</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnPhone</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnEmail</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnVisits</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnLastVisit</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnHairdresser</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnCreated</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnTags</swp-data-table-cell>
</swp-data-table-header>
@foreach (var customer in Model.Customers)
{
@await Component.InvokeAsync("CustomerRow", customer)
}
</swp-data-table>
<swp-empty-state id="emptyState" style="display: none;">
<i class="ph ph-users"></i>
<span>@Model.EmptySearchText</span>
</swp-empty-state>
</swp-card>