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:
parent
e9f3639c7c
commit
5e3811347c
11 changed files with 1018 additions and 13 deletions
|
|
@ -274,6 +274,54 @@
|
||||||
"image": "Billede",
|
"image": "Billede",
|
||||||
"uploadImage": "+ Upload billede"
|
"uploadImage": "+ Upload billede"
|
||||||
},
|
},
|
||||||
|
"prices": {
|
||||||
|
"priceStructure": "Prisstruktur",
|
||||||
|
"simplePrice": "Simpel pris",
|
||||||
|
"matrixPrice": "Matrix-pris",
|
||||||
|
"price": "Pris",
|
||||||
|
"level": "Niveau",
|
||||||
|
"shortHair": "Kort hår",
|
||||||
|
"mediumHair": "Mellem hår",
|
||||||
|
"longHair": "Langt hår",
|
||||||
|
"extraLongHair": "Ekstra langt",
|
||||||
|
"addLevel": "Tilføj niveau eller hårlængde",
|
||||||
|
"economy": "Økonomi",
|
||||||
|
"vatRate": "Momssats",
|
||||||
|
"productCost": "Produktomkostning",
|
||||||
|
"commission": "Provision",
|
||||||
|
"discounts": "Rabatter & Loyalitet",
|
||||||
|
"memberDiscount": "Medlemsrabat (10%)",
|
||||||
|
"giftCardPayment": "Kan betales med gavekort",
|
||||||
|
"loyaltyPoints": "Optjen loyalitetspoint"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"durationVariants": "Varighedsvarianter",
|
||||||
|
"addVariant": "Tilføj variant",
|
||||||
|
"bufferTimes": "Buffer-tider",
|
||||||
|
"bufferBefore": "Buffer før aftale",
|
||||||
|
"bufferAfter": "Buffer efter aftale",
|
||||||
|
"cleanupTime": "Oprydningstid",
|
||||||
|
"minutes": "min"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"bookingRules": "Booking-regler",
|
||||||
|
"minNotice": "Minimum varsel",
|
||||||
|
"maxAdvanceBooking": "Maks. forudbooking",
|
||||||
|
"cancellationDeadline": "Afbestillingsfrist",
|
||||||
|
"noShowFee": "No-show gebyr",
|
||||||
|
"requirements": "Krav & Forberedelse",
|
||||||
|
"requiresConsultation": "Konsultation påkrævet",
|
||||||
|
"requiresConsultationDesc": "Kunde skal have konsultation før første booking",
|
||||||
|
"requiresPatchTest": "Patch test påkrævet",
|
||||||
|
"requiresPatchTestDesc": "Allergitest 48 timer før farvebehandling (nye kunder)",
|
||||||
|
"ageRestriction": "Aldersbegrænsning",
|
||||||
|
"ageRestrictionDesc": "Minimum alder for booking af denne service",
|
||||||
|
"onlineBookingSettings": "Online booking indstillinger",
|
||||||
|
"showInOnlineBooking": "Vis i online booking",
|
||||||
|
"allowEmployeeSelection": "Tillad valg af medarbejder",
|
||||||
|
"showPrice": "Vis pris",
|
||||||
|
"showDuration": "Vis varighed"
|
||||||
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"duration": "min varighed",
|
"duration": "min varighed",
|
||||||
"fromPrice": "fra pris",
|
"fromPrice": "fra pris",
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,106 @@
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"price": "Price",
|
"price": "Price",
|
||||||
"serviceCount": "Service count"
|
"serviceCount": "Service count"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"back": "Back to services",
|
||||||
|
"save": "Save changes",
|
||||||
|
"tabs": {
|
||||||
|
"general": "General",
|
||||||
|
"prices": "Prices",
|
||||||
|
"duration": "Duration",
|
||||||
|
"employees": "Employees",
|
||||||
|
"addons": "Add-ons",
|
||||||
|
"rules": "Rules"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"basic": "Basic",
|
||||||
|
"serviceName": "Service name",
|
||||||
|
"category": "Category",
|
||||||
|
"calendarColor": "Calendar color",
|
||||||
|
"isActive": "Service active",
|
||||||
|
"internalNotes": "Internal notes",
|
||||||
|
"bookingType": "Booking type",
|
||||||
|
"canBookAsMain": "Can be booked as main service",
|
||||||
|
"canBookAsMainDesc": "Shown in service list and can be booked independently",
|
||||||
|
"canBookAsAddon": "Can be booked as add-on",
|
||||||
|
"canBookAsAddonDesc": "Can be added as extra service to other services",
|
||||||
|
"onlineBooking": "Online booking",
|
||||||
|
"showInOnlineBooking": "Show in online booking",
|
||||||
|
"showInOnlineBookingDesc": "Visible to customers in online booking",
|
||||||
|
"isFeatured": "Featured service",
|
||||||
|
"isFeaturedDesc": "Shown at top with featured styling",
|
||||||
|
"description": "Description",
|
||||||
|
"image": "Image",
|
||||||
|
"uploadImage": "+ Upload image"
|
||||||
|
},
|
||||||
|
"prices": {
|
||||||
|
"priceStructure": "Price structure",
|
||||||
|
"simplePrice": "Simple price",
|
||||||
|
"matrixPrice": "Matrix price",
|
||||||
|
"price": "Price",
|
||||||
|
"level": "Level",
|
||||||
|
"shortHair": "Short hair",
|
||||||
|
"mediumHair": "Medium hair",
|
||||||
|
"longHair": "Long hair",
|
||||||
|
"extraLongHair": "Extra long",
|
||||||
|
"addLevel": "Add level or hair length",
|
||||||
|
"economy": "Economy",
|
||||||
|
"vatRate": "VAT rate",
|
||||||
|
"productCost": "Product cost",
|
||||||
|
"commission": "Commission",
|
||||||
|
"discounts": "Discounts & Loyalty",
|
||||||
|
"memberDiscount": "Member discount (10%)",
|
||||||
|
"giftCardPayment": "Can be paid with gift card",
|
||||||
|
"loyaltyPoints": "Earn loyalty points"
|
||||||
|
},
|
||||||
|
"duration": {
|
||||||
|
"durationVariants": "Duration variants",
|
||||||
|
"addVariant": "Add variant",
|
||||||
|
"bufferTimes": "Buffer times",
|
||||||
|
"bufferBefore": "Buffer before appointment",
|
||||||
|
"bufferAfter": "Buffer after appointment",
|
||||||
|
"cleanupTime": "Cleanup time",
|
||||||
|
"minutes": "min"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"bookingRules": "Booking rules",
|
||||||
|
"minNotice": "Minimum notice",
|
||||||
|
"maxAdvanceBooking": "Max. advance booking",
|
||||||
|
"cancellationDeadline": "Cancellation deadline",
|
||||||
|
"noShowFee": "No-show fee",
|
||||||
|
"requirements": "Requirements & Preparation",
|
||||||
|
"requiresConsultation": "Consultation required",
|
||||||
|
"requiresConsultationDesc": "Customer must have consultation before first booking",
|
||||||
|
"requiresPatchTest": "Patch test required",
|
||||||
|
"requiresPatchTestDesc": "Allergy test 48 hours before color treatment (new customers)",
|
||||||
|
"ageRestriction": "Age restriction",
|
||||||
|
"ageRestrictionDesc": "Minimum age for booking this service",
|
||||||
|
"onlineBookingSettings": "Online booking settings",
|
||||||
|
"showInOnlineBooking": "Show in online booking",
|
||||||
|
"allowEmployeeSelection": "Allow employee selection",
|
||||||
|
"showPrice": "Show price",
|
||||||
|
"showDuration": "Show duration"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"duration": "min duration",
|
||||||
|
"fromPrice": "from price",
|
||||||
|
"employees": "employees",
|
||||||
|
"bookingsThisYear": "bookings this year",
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive"
|
||||||
|
},
|
||||||
|
"categoryDrawer": {
|
||||||
|
"title": "Create category",
|
||||||
|
"name": "Category name",
|
||||||
|
"description": "Description",
|
||||||
|
"visibilitySection": "Visibility",
|
||||||
|
"showInBooking": "Category should be shown in online booking",
|
||||||
|
"showInBookingDescription": "Category will still be visible here in the system",
|
||||||
|
"timePeriod": "Should only be visible in the following time period",
|
||||||
|
"timePeriodHint": "Leave fields blank for no time restriction",
|
||||||
|
"save": "Save category"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,44 @@ public static class ServiceDetailCatalog
|
||||||
CanBookAsAddon = false,
|
CanBookAsAddon = false,
|
||||||
ShowInOnlineBooking = true,
|
ShowInOnlineBooking = true,
|
||||||
IsFeatured = false,
|
IsFeatured = false,
|
||||||
Description = "Forkæl dig selv med en komplet forvandling! Vores Klip & Farve behandling inkluderer professionel farverådgivning, farvning tilpasset din hudtone, præcisionsklip og styling. Perfekt til dig der ønsker et helt nyt look."
|
Description = "Forkæl dig selv med en komplet forvandling! Vores Klip & Farve behandling inkluderer professionel farverådgivning, farvning tilpasset din hudtone, præcisionsklip og styling. Perfekt til dig der ønsker et helt nyt look.",
|
||||||
|
// Priser
|
||||||
|
PriceMode = PriceMode.Matrix,
|
||||||
|
PriceMatrix = new()
|
||||||
|
{
|
||||||
|
new("Junior", "795", "895", "995", "1.095"),
|
||||||
|
new("Senior", "895", "995", "1.095", "1.195"),
|
||||||
|
new("Master", "995", "1.095", "1.195", "1.295")
|
||||||
|
},
|
||||||
|
VatRate = "25",
|
||||||
|
ProductCost = "85 kr",
|
||||||
|
Commission = "standard",
|
||||||
|
MemberDiscount = true,
|
||||||
|
GiftCardPayment = true,
|
||||||
|
LoyaltyPoints = true,
|
||||||
|
// Varighed
|
||||||
|
DurationVariants = new()
|
||||||
|
{
|
||||||
|
new("Kort hår", 60),
|
||||||
|
new("Mellem hår", 90),
|
||||||
|
new("Langt hår", 120),
|
||||||
|
new("Ekstra langt hår", 150)
|
||||||
|
},
|
||||||
|
BufferBefore = "15",
|
||||||
|
BufferAfter = "10",
|
||||||
|
CleanupTime = "5",
|
||||||
|
// Regler
|
||||||
|
MinNotice = "24",
|
||||||
|
MaxAdvanceBooking = "3",
|
||||||
|
CancellationDeadline = "24",
|
||||||
|
NoShowFee = "50",
|
||||||
|
RequiresConsultation = false,
|
||||||
|
RequiresPatchTest = true,
|
||||||
|
AgeRestriction = false,
|
||||||
|
ShowInOnlineBookingRules = true,
|
||||||
|
AllowEmployeeSelection = true,
|
||||||
|
ShowPrice = true,
|
||||||
|
ShowDuration = true
|
||||||
},
|
},
|
||||||
["service-2"] = new ServiceDetailRecord
|
["service-2"] = new ServiceDetailRecord
|
||||||
{
|
{
|
||||||
|
|
@ -166,6 +203,42 @@ public record ServiceDetailRecord
|
||||||
public required bool ShowInOnlineBooking { get; init; }
|
public required bool ShowInOnlineBooking { get; init; }
|
||||||
public required bool IsFeatured { get; init; }
|
public required bool IsFeatured { get; init; }
|
||||||
public required string Description { get; init; }
|
public required string Description { get; init; }
|
||||||
|
|
||||||
|
// Priser tab
|
||||||
|
public PriceMode PriceMode { get; init; } = PriceMode.Simple;
|
||||||
|
public string SimplePrice { get; init; } = "995 kr";
|
||||||
|
public List<PriceMatrixRow> PriceMatrix { get; init; } = new();
|
||||||
|
public string VatRate { get; init; } = "25";
|
||||||
|
public string ProductCost { get; init; } = "85 kr";
|
||||||
|
public string Commission { get; init; } = "standard";
|
||||||
|
public bool MemberDiscount { get; init; } = true;
|
||||||
|
public bool GiftCardPayment { get; init; } = true;
|
||||||
|
public bool LoyaltyPoints { get; init; } = true;
|
||||||
|
|
||||||
|
// Varighed tab
|
||||||
|
public List<DurationVariant> DurationVariants { get; init; } = new();
|
||||||
|
public string BufferBefore { get; init; } = "15";
|
||||||
|
public string BufferAfter { get; init; } = "10";
|
||||||
|
public string CleanupTime { get; init; } = "5";
|
||||||
|
|
||||||
|
// Regler tab
|
||||||
|
public string MinNotice { get; init; } = "24";
|
||||||
|
public string MaxAdvanceBooking { get; init; } = "3";
|
||||||
|
public string CancellationDeadline { get; init; } = "24";
|
||||||
|
public string NoShowFee { get; init; } = "50";
|
||||||
|
public bool RequiresConsultation { get; init; } = false;
|
||||||
|
public bool RequiresPatchTest { get; init; } = false;
|
||||||
|
public bool AgeRestriction { get; init; } = false;
|
||||||
|
public bool ShowInOnlineBookingRules { get; init; } = true;
|
||||||
|
public bool AllowEmployeeSelection { get; init; } = true;
|
||||||
|
public bool ShowPrice { get; init; } = true;
|
||||||
|
public bool ShowDuration { get; init; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PriceMode { Simple, Matrix }
|
||||||
|
|
||||||
|
public record PriceMatrixRow(string Level, string ShortHair, string MediumHair, string LongHair, string ExtraLongHair);
|
||||||
|
|
||||||
|
public record DurationVariant(string Name, int Minutes);
|
||||||
|
|
||||||
public record ServiceTag(string Text, string CssClass);
|
public record ServiceTag(string Text, string CssClass);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
@model PlanTempus.Application.Features.Services.Components.ServiceDetailDurationViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelDurationVariants</swp-section-label>
|
||||||
|
|
||||||
|
<swp-duration-list>
|
||||||
|
@foreach (var variant in Model.DurationVariants)
|
||||||
|
{
|
||||||
|
<swp-duration-item>
|
||||||
|
<swp-duration-name>@variant.Name</swp-duration-name>
|
||||||
|
<swp-duration-value>
|
||||||
|
<span contenteditable="true">@variant.Minutes</span>
|
||||||
|
<swp-duration-unit>@Model.LabelMinutes</swp-duration-unit>
|
||||||
|
</swp-duration-value>
|
||||||
|
<swp-duration-delete>
|
||||||
|
<i class="ph ph-x"></i>
|
||||||
|
</swp-duration-delete>
|
||||||
|
</swp-duration-item>
|
||||||
|
}
|
||||||
|
</swp-duration-list>
|
||||||
|
|
||||||
|
<swp-add-button>
|
||||||
|
<i class="ph ph-plus"></i>
|
||||||
|
@Model.LabelAddVariant
|
||||||
|
</swp-add-button>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelBufferTimes</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelBufferBefore</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.BufferBefore">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@(Model.BufferBefore == "0" ? "Ingen" : Model.BufferBefore + " minutter")</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="0" class="@(Model.BufferBefore == "0" ? "selected" : "")">Ingen</swp-select-option>
|
||||||
|
<swp-select-option data-value="5" class="@(Model.BufferBefore == "5" ? "selected" : "")">5 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="10" class="@(Model.BufferBefore == "10" ? "selected" : "")">10 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="15" class="@(Model.BufferBefore == "15" ? "selected" : "")">15 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="30" class="@(Model.BufferBefore == "30" ? "selected" : "")">30 minutter</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelBufferAfter</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.BufferAfter">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@(Model.BufferAfter == "0" ? "Ingen" : Model.BufferAfter + " minutter")</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="0" class="@(Model.BufferAfter == "0" ? "selected" : "")">Ingen</swp-select-option>
|
||||||
|
<swp-select-option data-value="5" class="@(Model.BufferAfter == "5" ? "selected" : "")">5 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="10" class="@(Model.BufferAfter == "10" ? "selected" : "")">10 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="15" class="@(Model.BufferAfter == "15" ? "selected" : "")">15 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="30" class="@(Model.BufferAfter == "30" ? "selected" : "")">30 minutter</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelCleanupTime</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.CleanupTime">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@(Model.CleanupTime == "0" ? "Ingen" : Model.CleanupTime + " minutter")</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="0" class="@(Model.CleanupTime == "0" ? "selected" : "")">Ingen</swp-select-option>
|
||||||
|
<swp-select-option data-value="5" class="@(Model.CleanupTime == "5" ? "selected" : "")">5 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="10" class="@(Model.CleanupTime == "10" ? "selected" : "")">10 minutter</swp-select-option>
|
||||||
|
<swp-select-option data-value="15" class="@(Model.CleanupTime == "15" ? "selected" : "")">15 minutter</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
</swp-detail-grid>
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PlanTempus.Application.Features.Localization.Services;
|
||||||
|
|
||||||
|
namespace PlanTempus.Application.Features.Services.Components;
|
||||||
|
|
||||||
|
public class ServiceDetailDurationViewComponent : ViewComponent
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localization;
|
||||||
|
|
||||||
|
public ServiceDetailDurationViewComponent(ILocalizationService localization)
|
||||||
|
{
|
||||||
|
_localization = localization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IViewComponentResult Invoke(string key)
|
||||||
|
{
|
||||||
|
var service = ServiceDetailCatalog.Get(key);
|
||||||
|
|
||||||
|
var model = new ServiceDetailDurationViewModel
|
||||||
|
{
|
||||||
|
// Data
|
||||||
|
DurationVariants = service.DurationVariants,
|
||||||
|
BufferBefore = service.BufferBefore,
|
||||||
|
BufferAfter = service.BufferAfter,
|
||||||
|
CleanupTime = service.CleanupTime,
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
LabelDurationVariants = _localization.Get("services.detail.duration.durationVariants"),
|
||||||
|
LabelAddVariant = _localization.Get("services.detail.duration.addVariant"),
|
||||||
|
LabelBufferTimes = _localization.Get("services.detail.duration.bufferTimes"),
|
||||||
|
LabelBufferBefore = _localization.Get("services.detail.duration.bufferBefore"),
|
||||||
|
LabelBufferAfter = _localization.Get("services.detail.duration.bufferAfter"),
|
||||||
|
LabelCleanupTime = _localization.Get("services.detail.duration.cleanupTime"),
|
||||||
|
LabelMinutes = _localization.Get("services.detail.duration.minutes")
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServiceDetailDurationViewModel
|
||||||
|
{
|
||||||
|
// Data
|
||||||
|
public required List<DurationVariant> DurationVariants { get; init; }
|
||||||
|
public required string BufferBefore { get; init; }
|
||||||
|
public required string BufferAfter { get; init; }
|
||||||
|
public required string CleanupTime { get; init; }
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
public required string LabelDurationVariants { get; init; }
|
||||||
|
public required string LabelAddVariant { get; init; }
|
||||||
|
public required string LabelBufferTimes { get; init; }
|
||||||
|
public required string LabelBufferBefore { get; init; }
|
||||||
|
public required string LabelBufferAfter { get; init; }
|
||||||
|
public required string LabelCleanupTime { get; init; }
|
||||||
|
public required string LabelMinutes { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
@model PlanTempus.Application.Features.Services.Components.ServiceDetailRulesViewModel
|
||||||
|
|
||||||
|
<swp-detail-grid>
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelBookingRules</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelMinNotice</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.MinNotice">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@GetNoticeLabel(Model.MinNotice)</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="0" class="@(Model.MinNotice == "0" ? "selected" : "")">Ingen</swp-select-option>
|
||||||
|
<swp-select-option data-value="2" class="@(Model.MinNotice == "2" ? "selected" : "")">2 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="4" class="@(Model.MinNotice == "4" ? "selected" : "")">4 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="24" class="@(Model.MinNotice == "24" ? "selected" : "")">24 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="48" class="@(Model.MinNotice == "48" ? "selected" : "")">48 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="168" class="@(Model.MinNotice == "168" ? "selected" : "")">1 uge</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelMaxAdvanceBooking</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.MaxAdvanceBooking">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@GetAdvanceBookingLabel(Model.MaxAdvanceBooking)</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="1" class="@(Model.MaxAdvanceBooking == "1" ? "selected" : "")">1 måned</swp-select-option>
|
||||||
|
<swp-select-option data-value="2" class="@(Model.MaxAdvanceBooking == "2" ? "selected" : "")">2 måneder</swp-select-option>
|
||||||
|
<swp-select-option data-value="3" class="@(Model.MaxAdvanceBooking == "3" ? "selected" : "")">3 måneder</swp-select-option>
|
||||||
|
<swp-select-option data-value="6" class="@(Model.MaxAdvanceBooking == "6" ? "selected" : "")">6 måneder</swp-select-option>
|
||||||
|
<swp-select-option data-value="12" class="@(Model.MaxAdvanceBooking == "12" ? "selected" : "")">1 år</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelCancellationDeadline</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.CancellationDeadline">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@GetNoticeLabel(Model.CancellationDeadline)</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="0" class="@(Model.CancellationDeadline == "0" ? "selected" : "")">Ingen</swp-select-option>
|
||||||
|
<swp-select-option data-value="2" class="@(Model.CancellationDeadline == "2" ? "selected" : "")">2 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="4" class="@(Model.CancellationDeadline == "4" ? "selected" : "")">4 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="24" class="@(Model.CancellationDeadline == "24" ? "selected" : "")">24 timer</swp-select-option>
|
||||||
|
<swp-select-option data-value="48" class="@(Model.CancellationDeadline == "48" ? "selected" : "")">48 timer</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label>@Model.LabelNoShowFee</swp-edit-label>
|
||||||
|
<swp-select data-value="@Model.NoShowFee">
|
||||||
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-select-value>@GetNoShowFeeLabel(Model.NoShowFee)</swp-select-value>
|
||||||
|
<i class="ph ph-caret-down"></i>
|
||||||
|
</button>
|
||||||
|
<swp-select-dropdown>
|
||||||
|
<swp-select-option data-value="0" class="@(Model.NoShowFee == "0" ? "selected" : "")">Intet</swp-select-option>
|
||||||
|
<swp-select-option data-value="25" class="@(Model.NoShowFee == "25" ? "selected" : "")">25% af pris</swp-select-option>
|
||||||
|
<swp-select-option data-value="50" class="@(Model.NoShowFee == "50" ? "selected" : "")">50% af pris</swp-select-option>
|
||||||
|
<swp-select-option data-value="100" class="@(Model.NoShowFee == "100" ? "selected" : "")">Fuld pris</swp-select-option>
|
||||||
|
<swp-select-option data-value="fixed" class="@(Model.NoShowFee == "fixed" ? "selected" : "")">Fast beløb</swp-select-option>
|
||||||
|
</swp-select-dropdown>
|
||||||
|
</swp-select>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</swp-card>
|
||||||
|
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelRequirements</swp-section-label>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<div>
|
||||||
|
<swp-toggle-label>@Model.LabelRequiresConsultation</swp-toggle-label>
|
||||||
|
<swp-toggle-description>@Model.LabelRequiresConsultationDesc</swp-toggle-description>
|
||||||
|
</div>
|
||||||
|
<swp-toggle-slider data-value="@(Model.RequiresConsultation ? "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>
|
||||||
|
<div>
|
||||||
|
<swp-toggle-label>@Model.LabelRequiresPatchTest</swp-toggle-label>
|
||||||
|
<swp-toggle-description>@Model.LabelRequiresPatchTestDesc</swp-toggle-description>
|
||||||
|
</div>
|
||||||
|
<swp-toggle-slider data-value="@(Model.RequiresPatchTest ? "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>
|
||||||
|
<div>
|
||||||
|
<swp-toggle-label>@Model.LabelAgeRestriction</swp-toggle-label>
|
||||||
|
<swp-toggle-description>@Model.LabelAgeRestrictionDesc</swp-toggle-description>
|
||||||
|
</div>
|
||||||
|
<swp-toggle-slider data-value="@(Model.AgeRestriction ? "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>
|
||||||
|
|
||||||
|
<swp-card>
|
||||||
|
<swp-section-label>@Model.LabelOnlineBookingSettings</swp-section-label>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<swp-toggle-label>@Model.LabelShowInOnlineBooking</swp-toggle-label>
|
||||||
|
<swp-toggle-slider data-value="@(Model.ShowInOnlineBooking ? "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.LabelAllowEmployeeSelection</swp-toggle-label>
|
||||||
|
<swp-toggle-slider data-value="@(Model.AllowEmployeeSelection ? "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.LabelShowPrice</swp-toggle-label>
|
||||||
|
<swp-toggle-slider data-value="@(Model.ShowPrice ? "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.LabelShowDuration</swp-toggle-label>
|
||||||
|
<swp-toggle-slider data-value="@(Model.ShowDuration ? "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>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
string GetNoticeLabel(string hours)
|
||||||
|
{
|
||||||
|
return hours switch
|
||||||
|
{
|
||||||
|
"0" => "Ingen",
|
||||||
|
"168" => "1 uge",
|
||||||
|
_ => $"{hours} timer"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetAdvanceBookingLabel(string months)
|
||||||
|
{
|
||||||
|
return months switch
|
||||||
|
{
|
||||||
|
"1" => "1 måned",
|
||||||
|
"12" => "1 år",
|
||||||
|
_ => $"{months} måneder"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetNoShowFeeLabel(string fee)
|
||||||
|
{
|
||||||
|
return fee switch
|
||||||
|
{
|
||||||
|
"0" => "Intet",
|
||||||
|
"100" => "Fuld pris",
|
||||||
|
"fixed" => "Fast beløb",
|
||||||
|
_ => $"{fee}% af pris"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PlanTempus.Application.Features.Localization.Services;
|
||||||
|
|
||||||
|
namespace PlanTempus.Application.Features.Services.Components;
|
||||||
|
|
||||||
|
public class ServiceDetailRulesViewComponent : ViewComponent
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localization;
|
||||||
|
|
||||||
|
public ServiceDetailRulesViewComponent(ILocalizationService localization)
|
||||||
|
{
|
||||||
|
_localization = localization;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IViewComponentResult Invoke(string key)
|
||||||
|
{
|
||||||
|
var service = ServiceDetailCatalog.Get(key);
|
||||||
|
|
||||||
|
var model = new ServiceDetailRulesViewModel
|
||||||
|
{
|
||||||
|
// Data
|
||||||
|
MinNotice = service.MinNotice,
|
||||||
|
MaxAdvanceBooking = service.MaxAdvanceBooking,
|
||||||
|
CancellationDeadline = service.CancellationDeadline,
|
||||||
|
NoShowFee = service.NoShowFee,
|
||||||
|
RequiresConsultation = service.RequiresConsultation,
|
||||||
|
RequiresPatchTest = service.RequiresPatchTest,
|
||||||
|
AgeRestriction = service.AgeRestriction,
|
||||||
|
ShowInOnlineBooking = service.ShowInOnlineBookingRules,
|
||||||
|
AllowEmployeeSelection = service.AllowEmployeeSelection,
|
||||||
|
ShowPrice = service.ShowPrice,
|
||||||
|
ShowDuration = service.ShowDuration,
|
||||||
|
|
||||||
|
// Labels - Booking rules
|
||||||
|
LabelBookingRules = _localization.Get("services.detail.rules.bookingRules"),
|
||||||
|
LabelMinNotice = _localization.Get("services.detail.rules.minNotice"),
|
||||||
|
LabelMaxAdvanceBooking = _localization.Get("services.detail.rules.maxAdvanceBooking"),
|
||||||
|
LabelCancellationDeadline = _localization.Get("services.detail.rules.cancellationDeadline"),
|
||||||
|
LabelNoShowFee = _localization.Get("services.detail.rules.noShowFee"),
|
||||||
|
|
||||||
|
// Labels - Requirements
|
||||||
|
LabelRequirements = _localization.Get("services.detail.rules.requirements"),
|
||||||
|
LabelRequiresConsultation = _localization.Get("services.detail.rules.requiresConsultation"),
|
||||||
|
LabelRequiresConsultationDesc = _localization.Get("services.detail.rules.requiresConsultationDesc"),
|
||||||
|
LabelRequiresPatchTest = _localization.Get("services.detail.rules.requiresPatchTest"),
|
||||||
|
LabelRequiresPatchTestDesc = _localization.Get("services.detail.rules.requiresPatchTestDesc"),
|
||||||
|
LabelAgeRestriction = _localization.Get("services.detail.rules.ageRestriction"),
|
||||||
|
LabelAgeRestrictionDesc = _localization.Get("services.detail.rules.ageRestrictionDesc"),
|
||||||
|
|
||||||
|
// Labels - Online booking settings
|
||||||
|
LabelOnlineBookingSettings = _localization.Get("services.detail.rules.onlineBookingSettings"),
|
||||||
|
LabelShowInOnlineBooking = _localization.Get("services.detail.rules.showInOnlineBooking"),
|
||||||
|
LabelAllowEmployeeSelection = _localization.Get("services.detail.rules.allowEmployeeSelection"),
|
||||||
|
LabelShowPrice = _localization.Get("services.detail.rules.showPrice"),
|
||||||
|
LabelShowDuration = _localization.Get("services.detail.rules.showDuration"),
|
||||||
|
|
||||||
|
// Toggle labels
|
||||||
|
ToggleYes = _localization.Get("common.yes"),
|
||||||
|
ToggleNo = _localization.Get("common.no")
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ServiceDetailRulesViewModel
|
||||||
|
{
|
||||||
|
// Data - Booking rules
|
||||||
|
public required string MinNotice { get; init; }
|
||||||
|
public required string MaxAdvanceBooking { get; init; }
|
||||||
|
public required string CancellationDeadline { get; init; }
|
||||||
|
public required string NoShowFee { get; init; }
|
||||||
|
|
||||||
|
// Data - Requirements
|
||||||
|
public bool RequiresConsultation { get; init; }
|
||||||
|
public bool RequiresPatchTest { get; init; }
|
||||||
|
public bool AgeRestriction { get; init; }
|
||||||
|
|
||||||
|
// Data - Online booking settings
|
||||||
|
public bool ShowInOnlineBooking { get; init; }
|
||||||
|
public bool AllowEmployeeSelection { get; init; }
|
||||||
|
public bool ShowPrice { get; init; }
|
||||||
|
public bool ShowDuration { get; init; }
|
||||||
|
|
||||||
|
// Labels - Booking rules
|
||||||
|
public required string LabelBookingRules { get; init; }
|
||||||
|
public required string LabelMinNotice { get; init; }
|
||||||
|
public required string LabelMaxAdvanceBooking { get; init; }
|
||||||
|
public required string LabelCancellationDeadline { get; init; }
|
||||||
|
public required string LabelNoShowFee { get; init; }
|
||||||
|
|
||||||
|
// Labels - Requirements
|
||||||
|
public required string LabelRequirements { get; init; }
|
||||||
|
public required string LabelRequiresConsultation { get; init; }
|
||||||
|
public required string LabelRequiresConsultationDesc { get; init; }
|
||||||
|
public required string LabelRequiresPatchTest { get; init; }
|
||||||
|
public required string LabelRequiresPatchTestDesc { get; init; }
|
||||||
|
public required string LabelAgeRestriction { get; init; }
|
||||||
|
public required string LabelAgeRestrictionDesc { get; init; }
|
||||||
|
|
||||||
|
// Labels - Online booking settings
|
||||||
|
public required string LabelOnlineBookingSettings { get; init; }
|
||||||
|
public required string LabelShowInOnlineBooking { get; init; }
|
||||||
|
public required string LabelAllowEmployeeSelection { get; init; }
|
||||||
|
public required string LabelShowPrice { get; init; }
|
||||||
|
public required string LabelShowDuration { get; init; }
|
||||||
|
|
||||||
|
// Toggle labels
|
||||||
|
public required string ToggleYes { get; init; }
|
||||||
|
public required string ToggleNo { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -44,19 +44,13 @@
|
||||||
|
|
||||||
<swp-tab-content data-tab="prices">
|
<swp-tab-content data-tab="prices">
|
||||||
<swp-page-container>
|
<swp-page-container>
|
||||||
<swp-card>
|
@await Component.InvokeAsync("ServiceDetailPrices", Model.ServiceKey)
|
||||||
<swp-section-label>Priser</swp-section-label>
|
|
||||||
<p style="color: var(--color-text-secondary);">Priser-tab kommer snart...</p>
|
|
||||||
</swp-card>
|
|
||||||
</swp-page-container>
|
</swp-page-container>
|
||||||
</swp-tab-content>
|
</swp-tab-content>
|
||||||
|
|
||||||
<swp-tab-content data-tab="duration">
|
<swp-tab-content data-tab="duration">
|
||||||
<swp-page-container>
|
<swp-page-container>
|
||||||
<swp-card>
|
@await Component.InvokeAsync("ServiceDetailDuration", Model.ServiceKey)
|
||||||
<swp-section-label>Varighed</swp-section-label>
|
|
||||||
<p style="color: var(--color-text-secondary);">Varighed-tab kommer snart...</p>
|
|
||||||
</swp-card>
|
|
||||||
</swp-page-container>
|
</swp-page-container>
|
||||||
</swp-tab-content>
|
</swp-tab-content>
|
||||||
|
|
||||||
|
|
@ -80,10 +74,7 @@
|
||||||
|
|
||||||
<swp-tab-content data-tab="rules">
|
<swp-tab-content data-tab="rules">
|
||||||
<swp-page-container>
|
<swp-page-container>
|
||||||
<swp-card>
|
@await Component.InvokeAsync("ServiceDetailRules", Model.ServiceKey)
|
||||||
<swp-section-label>Regler</swp-section-label>
|
|
||||||
<p style="color: var(--color-text-secondary);">Regler-tab kommer snart...</p>
|
|
||||||
</swp-card>
|
|
||||||
</swp-page-container>
|
</swp-page-container>
|
||||||
</swp-tab-content>
|
</swp-tab-content>
|
||||||
</swp-service-detail-view>
|
</swp-service-detail-view>
|
||||||
|
|
|
||||||
|
|
@ -272,3 +272,157 @@ swp-service-name {
|
||||||
border-bottom: 1px dashed var(--color-teal);
|
border-bottom: 1px dashed var(--color-teal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
PRICE MODE TOGGLE (Priser tab)
|
||||||
|
=========================================== */
|
||||||
|
|
||||||
|
swp-price-mode {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-price-mode-btn {
|
||||||
|
padding: var(--spacing-3) var(--spacing-6);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--color-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
PRICE MATRIX TABLE (Priser tab)
|
||||||
|
=========================================== */
|
||||||
|
|
||||||
|
swp-price-matrix {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-data-table.price-matrix {
|
||||||
|
grid-template-columns: 3fr repeat(4, 2fr);
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-data-table.price-matrix swp-data-table-cell:first-child {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-data-table.price-matrix span[contenteditable="true"] {
|
||||||
|
outline: none;
|
||||||
|
padding: var(--spacing-1) var(--spacing-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-teal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
DURATION LIST (Varighed tab)
|
||||||
|
=========================================== */
|
||||||
|
|
||||||
|
swp-duration-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-duration-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-duration-name {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-duration-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
span[contenteditable="true"] {
|
||||||
|
outline: none;
|
||||||
|
padding: var(--spacing-1) var(--spacing-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-teal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-duration-unit {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-duration-delete {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-red);
|
||||||
|
background: var(--bg-red-subtle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-duration-item:hover swp-duration-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue