Adds comprehensive service detail views and localization

Implements new service detail tabs for prices, duration, and rules

Extends localization support for Danish and English translations
Adds dynamic view components for managing service-specific configurations
Introduces flexible pricing, duration, and booking rule management

Enhances service management with granular configuration options
This commit is contained in:
Janus C. H. Knudsen 2026-01-17 01:21:00 +01:00
parent e9f3639c7c
commit 5e3811347c
11 changed files with 1018 additions and 13 deletions

View file

@ -0,0 +1,112 @@
@model PlanTempus.Application.Features.Services.Components.ServiceDetailPricesViewModel
<swp-card>
<swp-section-label>@Model.LabelPriceStructure</swp-section-label>
<swp-price-mode>
<swp-price-mode-btn data-mode="simple" class="@(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Simple ? "active" : "")">@Model.LabelSimplePrice</swp-price-mode-btn>
<swp-price-mode-btn data-mode="matrix" class="@(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Matrix ? "active" : "")">@Model.LabelMatrixPrice</swp-price-mode-btn>
</swp-price-mode>
<!-- Simple price view -->
<swp-price-simple style="display: @(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Simple ? "block" : "none");">
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelPrice</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.SimplePrice</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-price-simple>
<!-- Matrix price view -->
<swp-price-matrix style="display: @(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Matrix ? "block" : "none");">
<swp-data-table class="price-matrix">
<swp-data-table-header>
<swp-data-table-cell>@Model.LabelLevel</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelShortHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelMediumHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelLongHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelExtraLongHair</swp-data-table-cell>
</swp-data-table-header>
@foreach (var row in Model.PriceMatrix)
{
<swp-data-table-row>
<swp-data-table-cell>@row.Level</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.ShortHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.MediumHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.LongHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.ExtraLongHair</span> kr</swp-data-table-cell>
</swp-data-table-row>
}
</swp-data-table>
<swp-add-button>
<i class="ph ph-plus"></i>
@Model.LabelAddLevel
</swp-add-button>
</swp-price-matrix>
</swp-card>
<swp-detail-grid>
<swp-card>
<swp-section-label>@Model.LabelEconomy</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelVatRate</swp-edit-label>
<swp-select data-value="@Model.VatRate">
<button type="button" aria-expanded="false">
<swp-select-value>@(Model.VatRate == "25" ? "25% (standard)" : "0% (momsfri)")</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option data-value="25" class="@(Model.VatRate == "25" ? "selected" : "")">25% (standard)</swp-select-option>
<swp-select-option data-value="0" class="@(Model.VatRate == "0" ? "selected" : "")">0% (momsfri)</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelProductCost</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.ProductCost</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelCommission</swp-edit-label>
<swp-select data-value="@Model.Commission">
<button type="button" aria-expanded="false">
<swp-select-value>@(Model.Commission == "standard" ? "Standard (fra lønsats)" : Model.Commission == "fixed" ? "Fast beløb" : "Procent af pris")</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option data-value="standard" class="@(Model.Commission == "standard" ? "selected" : "")">Standard (fra lønsats)</swp-select-option>
<swp-select-option data-value="fixed" class="@(Model.Commission == "fixed" ? "selected" : "")">Fast beløb</swp-select-option>
<swp-select-option data-value="percent" class="@(Model.Commission == "percent" ? "selected" : "")">Procent af pris</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>@Model.LabelDiscounts</swp-section-label>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelMemberDiscount</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.MemberDiscount ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelGiftCardPayment</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.GiftCardPayment ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelLoyaltyPoints</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.LoyaltyPoints ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card>
</swp-detail-grid>

View file

@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Services.Components;
public class ServiceDetailPricesViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
public ServiceDetailPricesViewComponent(ILocalizationService localization)
{
_localization = localization;
}
public IViewComponentResult Invoke(string key)
{
var service = ServiceDetailCatalog.Get(key);
var model = new ServiceDetailPricesViewModel
{
// Data
PriceMode = service.PriceMode,
SimplePrice = service.SimplePrice,
PriceMatrix = service.PriceMatrix,
VatRate = service.VatRate,
ProductCost = service.ProductCost,
Commission = service.Commission,
MemberDiscount = service.MemberDiscount,
GiftCardPayment = service.GiftCardPayment,
LoyaltyPoints = service.LoyaltyPoints,
// Labels - Price structure
LabelPriceStructure = _localization.Get("services.detail.prices.priceStructure"),
LabelSimplePrice = _localization.Get("services.detail.prices.simplePrice"),
LabelMatrixPrice = _localization.Get("services.detail.prices.matrixPrice"),
LabelPrice = _localization.Get("services.detail.prices.price"),
LabelLevel = _localization.Get("services.detail.prices.level"),
LabelShortHair = _localization.Get("services.detail.prices.shortHair"),
LabelMediumHair = _localization.Get("services.detail.prices.mediumHair"),
LabelLongHair = _localization.Get("services.detail.prices.longHair"),
LabelExtraLongHair = _localization.Get("services.detail.prices.extraLongHair"),
LabelAddLevel = _localization.Get("services.detail.prices.addLevel"),
// Labels - Economy
LabelEconomy = _localization.Get("services.detail.prices.economy"),
LabelVatRate = _localization.Get("services.detail.prices.vatRate"),
LabelProductCost = _localization.Get("services.detail.prices.productCost"),
LabelCommission = _localization.Get("services.detail.prices.commission"),
// Labels - Discounts
LabelDiscounts = _localization.Get("services.detail.prices.discounts"),
LabelMemberDiscount = _localization.Get("services.detail.prices.memberDiscount"),
LabelGiftCardPayment = _localization.Get("services.detail.prices.giftCardPayment"),
LabelLoyaltyPoints = _localization.Get("services.detail.prices.loyaltyPoints"),
// Toggle labels
ToggleYes = _localization.Get("common.yes"),
ToggleNo = _localization.Get("common.no")
};
return View(model);
}
}
public class ServiceDetailPricesViewModel
{
// Data
public PriceMode PriceMode { get; init; }
public required string SimplePrice { get; init; }
public required List<PriceMatrixRow> PriceMatrix { get; init; }
public required string VatRate { get; init; }
public required string ProductCost { get; init; }
public required string Commission { get; init; }
public bool MemberDiscount { get; init; }
public bool GiftCardPayment { get; init; }
public bool LoyaltyPoints { get; init; }
// Labels - Price structure
public required string LabelPriceStructure { get; init; }
public required string LabelSimplePrice { get; init; }
public required string LabelMatrixPrice { get; init; }
public required string LabelPrice { get; init; }
public required string LabelLevel { get; init; }
public required string LabelShortHair { get; init; }
public required string LabelMediumHair { get; init; }
public required string LabelLongHair { get; init; }
public required string LabelExtraLongHair { get; init; }
public required string LabelAddLevel { get; init; }
// Labels - Economy
public required string LabelEconomy { get; init; }
public required string LabelVatRate { get; init; }
public required string LabelProductCost { get; init; }
public required string LabelCommission { get; init; }
// Labels - Discounts
public required string LabelDiscounts { get; init; }
public required string LabelMemberDiscount { get; init; }
public required string LabelGiftCardPayment { get; init; }
public required string LabelLoyaltyPoints { get; init; }
// Toggle labels
public required string ToggleYes { get; init; }
public required string ToggleNo { get; init; }
}