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,169 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Customers.Components;
public class CustomerDetailOverviewViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
public CustomerDetailOverviewViewComponent(ILocalizationService localization)
{
_localization = localization;
}
public IViewComponentResult Invoke(string customerId)
{
var customer = CustomerDetailCatalog.Get(customerId);
var model = new CustomerDetailOverviewViewModel
{
// Contact
ContactTitle = _localization.Get("customers.detail.contactInfo"),
Phone = customer.Contact.Phone,
PhoneLabel = _localization.Get("customers.detail.phone"),
Email = customer.Contact.Email,
EmailLabel = _localization.Get("customers.detail.email"),
Address = customer.Contact.Address,
AddressLabel = _localization.Get("customers.detail.address"),
ZipCity = $"{customer.Contact.Zip} {customer.Contact.City}",
ZipCityLabel = _localization.Get("customers.detail.zipCity"),
// Profile
ProfileTitle = _localization.Get("customers.detail.profile"),
ProfileItems = customer.Profile.Select(p => new ProfileItemViewModel
{
Title = p.Title,
Value = p.Value
}).ToList(),
// Marketing
MarketingTitle = _localization.Get("customers.detail.marketing"),
EmailMarketingLabel = _localization.Get("customers.detail.emailMarketing"),
EmailOptIn = customer.Marketing.EmailOptIn,
SmsMarketingLabel = _localization.Get("customers.detail.smsMarketing"),
SmsOptIn = customer.Marketing.SmsOptIn,
YesLabel = _localization.Get("common.yes"),
NoLabel = _localization.Get("common.no"),
// Payment
PaymentTitle = _localization.Get("customers.detail.paymentSettings"),
RequirePrepaymentLabel = _localization.Get("customers.detail.requirePrepayment"),
RequirePrepaymentDesc = "Kunden skal betale fuldt beløb ved booking",
RequirePrepayment = customer.Payment.RequirePrepayment,
AllowPartialPaymentLabel = _localization.Get("customers.detail.allowPartialPayment"),
AllowPartialPaymentDesc = "Kunden kan vælge at betale et depositum",
AllowPartialPayment = customer.Payment.AllowPartialPayment,
// Preferences
PreferencesTitle = _localization.Get("customers.detail.preferences"),
PreferredHairdresser = customer.Preferences.PreferredHairdresser,
PreferredHairdresserLabel = _localization.Get("customers.detail.preferredHairdresser"),
PreferredDays = customer.Preferences.PreferredDays,
PreferredDaysLabel = _localization.Get("customers.detail.preferredDay"),
SpecialRequests = customer.Preferences.SpecialRequests,
SpecialRequestsLabel = _localization.Get("customers.detail.specialRequests"),
// Warnings
WarningsTitle = _localization.Get("customers.detail.warnings"),
Warnings = customer.Warnings.Select(w => new WarningItemViewModel
{
Title = w.Title,
Value = w.Value
}).ToList(),
// Group & Relations
GroupRelationsTitle = _localization.Get("customers.detail.groupAndRelations"),
GroupLabel = "Kundegruppe:",
GroupId = customer.Group.GroupId,
GroupName = customer.Group.GroupName,
Relations = customer.Relations.Select(r => new RelationItemViewModel
{
Id = r.Id,
Name = r.Name,
Initials = r.Initials,
Type = r.Type
}).ToList(),
AddRelationText = "Tilføj relation"
};
return View(model);
}
}
public class CustomerDetailOverviewViewModel
{
// Contact
public required string ContactTitle { get; init; }
public required string Phone { get; init; }
public required string PhoneLabel { get; init; }
public required string Email { get; init; }
public required string EmailLabel { get; init; }
public required string Address { get; init; }
public required string AddressLabel { get; init; }
public required string ZipCity { get; init; }
public required string ZipCityLabel { get; init; }
// Profile
public required string ProfileTitle { get; init; }
public List<ProfileItemViewModel> ProfileItems { get; init; } = new();
// Marketing
public required string MarketingTitle { get; init; }
public required string EmailMarketingLabel { get; init; }
public bool EmailOptIn { get; init; }
public required string SmsMarketingLabel { get; init; }
public bool SmsOptIn { get; init; }
public required string YesLabel { get; init; }
public required string NoLabel { get; init; }
// Payment
public required string PaymentTitle { get; init; }
public required string RequirePrepaymentLabel { get; init; }
public required string RequirePrepaymentDesc { get; init; }
public bool RequirePrepayment { get; init; }
public required string AllowPartialPaymentLabel { get; init; }
public required string AllowPartialPaymentDesc { get; init; }
public bool AllowPartialPayment { get; init; }
// Preferences
public required string PreferencesTitle { get; init; }
public required string PreferredHairdresser { get; init; }
public required string PreferredHairdresserLabel { get; init; }
public required string PreferredDays { get; init; }
public required string PreferredDaysLabel { get; init; }
public required string SpecialRequests { get; init; }
public required string SpecialRequestsLabel { get; init; }
// Warnings
public required string WarningsTitle { get; init; }
public List<WarningItemViewModel> Warnings { get; init; } = new();
// Group & Relations
public required string GroupRelationsTitle { get; init; }
public required string GroupLabel { get; init; }
public required string GroupId { get; init; }
public required string GroupName { get; init; }
public List<RelationItemViewModel> Relations { get; init; } = new();
public required string AddRelationText { get; init; }
}
public class ProfileItemViewModel
{
public required string Title { get; init; }
public required string Value { get; init; }
}
public class WarningItemViewModel
{
public required string Title { get; init; }
public required string Value { get; init; }
}
public class RelationItemViewModel
{
public required string Id { get; init; }
public required string Name { get; init; }
public required string Initials { get; init; }
public required string Type { get; init; }
}

View file

@ -0,0 +1,201 @@
@model PlanTempus.Application.Features.Customers.Components.CustomerDetailOverviewViewModel
<swp-detail-grid>
<!-- Left Column -->
<swp-card-column>
<!-- Kontaktoplysninger -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-address-book"></i>
<span>@Model.ContactTitle</span>
</swp-card-title>
</swp-card-header>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.PhoneLabel</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Phone</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.EmailLabel</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Email</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.AddressLabel</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.Address</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.ZipCityLabel</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.ZipCity</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<!-- Profil -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-user-circle"></i>
<span>@Model.ProfileTitle</span>
</swp-card-title>
</swp-card-header>
<swp-profile-boxes>
@foreach (var item in Model.ProfileItems)
{
<swp-profile-box>
<swp-profile-box-label>@item.Title</swp-profile-box-label>
<swp-profile-box-value>@item.Value</swp-profile-box-value>
</swp-profile-box>
}
</swp-profile-boxes>
</swp-card>
</swp-card-column>
<!-- Right Column -->
<swp-card-column>
<!-- Marketing -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-megaphone"></i>
<span>@Model.MarketingTitle</span>
</swp-card-title>
</swp-card-header>
<swp-toggle-row>
<swp-toggle-label>@Model.EmailMarketingLabel</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.EmailOptIn ? "yes" : "no")">
<swp-toggle-option>@Model.YesLabel</swp-toggle-option>
<swp-toggle-option>@Model.NoLabel</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.SmsMarketingLabel</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.SmsOptIn ? "yes" : "no")">
<swp-toggle-option>@Model.YesLabel</swp-toggle-option>
<swp-toggle-option>@Model.NoLabel</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card>
<!-- Betalingsindstillinger -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-credit-card"></i>
<span>@Model.PaymentTitle</span>
</swp-card-title>
</swp-card-header>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>@Model.RequirePrepaymentLabel</swp-toggle-label>
<swp-toggle-desc>@Model.RequirePrepaymentDesc</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="@(Model.RequirePrepayment ? "yes" : "no")">
<swp-toggle-option>@Model.YesLabel</swp-toggle-option>
<swp-toggle-option>@Model.NoLabel</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label>@Model.AllowPartialPaymentLabel</swp-toggle-label>
<swp-toggle-desc>@Model.AllowPartialPaymentDesc</swp-toggle-desc>
</swp-toggle-info>
<swp-toggle-slider data-value="@(Model.AllowPartialPayment ? "yes" : "no")">
<swp-toggle-option>@Model.YesLabel</swp-toggle-option>
<swp-toggle-option>@Model.NoLabel</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card>
<!-- Praeferencer -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-sliders-horizontal"></i>
<span>@Model.PreferencesTitle</span>
</swp-card-title>
</swp-card-header>
<swp-profile-boxes>
<swp-profile-box>
<swp-profile-box-label>@Model.PreferredHairdresserLabel</swp-profile-box-label>
<swp-profile-box-value>@Model.PreferredHairdresser</swp-profile-box-value>
</swp-profile-box>
<swp-profile-box>
<swp-profile-box-label>@Model.PreferredDaysLabel</swp-profile-box-label>
<swp-profile-box-value>@Model.PreferredDays</swp-profile-box-value>
</swp-profile-box>
<swp-profile-box class="full-width">
<swp-profile-box-label>@Model.SpecialRequestsLabel</swp-profile-box-label>
<swp-profile-box-value>@Model.SpecialRequests</swp-profile-box-value>
</swp-profile-box>
</swp-profile-boxes>
</swp-card>
<!-- Advarsler -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-warning"></i>
<span>@Model.WarningsTitle</span>
</swp-card-title>
</swp-card-header>
<swp-profile-boxes>
@foreach (var warning in Model.Warnings)
{
<swp-profile-box class="warning full-width">
<swp-profile-box-label>@warning.Title</swp-profile-box-label>
<swp-profile-box-value>@warning.Value</swp-profile-box-value>
</swp-profile-box>
}
</swp-profile-boxes>
</swp-card>
<!-- Kundegruppe & Relationer -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-users-three"></i>
<span>@Model.GroupRelationsTitle</span>
</swp-card-title>
</swp-card-header>
<swp-customer-group-row>
<swp-customer-group-label>@Model.GroupLabel</swp-customer-group-label>
<swp-select>
<button type="button">
<swp-select-value>@Model.GroupName</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option class="@(Model.GroupId == "standard" ? "selected" : "")" data-value="standard">Standard</swp-select-option>
<swp-select-option class="@(Model.GroupId == "premium" ? "selected" : "")" data-value="premium">Premium</swp-select-option>
<swp-select-option class="@(Model.GroupId == "erhverv" ? "selected" : "")" data-value="erhverv">Erhverv</swp-select-option>
<swp-select-option class="@(Model.GroupId == "medarbejder" ? "selected" : "")" data-value="medarbejder">Medarbejder</swp-select-option>
<swp-select-option class="@(Model.GroupId == "familie" ? "selected" : "")" data-value="familie">Familie & Venner</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-customer-group-row>
<swp-relations-list>
@foreach (var relation in Model.Relations)
{
<swp-relation-item>
<swp-relation-avatar>@relation.Initials</swp-relation-avatar>
<swp-relation-info>
<swp-relation-name>@relation.Name</swp-relation-name>
<swp-relation-type>@relation.Type</swp-relation-type>
</swp-relation-info>
<swp-relation-actions>
<swp-relation-link>Abn</swp-relation-link>
<swp-relation-remove>&times;</swp-relation-remove>
</swp-relation-actions>
</swp-relation-item>
}
<swp-add-relation>
<i class="ph ph-plus"></i>
<span>@Model.AddRelationText</span>
</swp-add-relation>
</swp-relations-list>
</swp-card>
</swp-card-column>
</swp-detail-grid>