Enhances service details with employees and addon sections

Adds new components for service employees and addons
Introduces detailed views with selectable employees and add-ons
Updates localization translations for new sections
Implements time range slider functionality for availability
This commit is contained in:
Janus C. H. Knudsen 2026-01-17 15:36:15 +01:00
parent 5e3811347c
commit 7643a6ab82
20 changed files with 830 additions and 336 deletions

View file

@ -0,0 +1,29 @@
@model PlanTempus.Application.Features.Services.Components.ServiceDetailAddonsViewModel
<swp-card>
<swp-section-label>@Model.LabelAddonsForService</swp-section-label>
<swp-selectable-list>
@foreach (var addon in Model.Addons)
{
<swp-selectable-item class="no-avatar @(addon.Selected ? "selected" : "")">
<swp-selectable-checkbox>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-selectable-checkbox>
<swp-selectable-info>
<swp-selectable-name>@addon.Name</swp-selectable-name>
<swp-selectable-meta>
<span>@addon.Price</span>
<span>@addon.Duration</span>
<swp-selectable-type class="@(addon.Required ? "required" : "")">@addon.Type</swp-selectable-type>
</swp-selectable-meta>
</swp-selectable-info>
</swp-selectable-item>
}
</swp-selectable-list>
<swp-add-button>
<i class="ph ph-plus"></i>
@Model.LabelAddExistingAddon
</swp-add-button>
</swp-card>

View file

@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Services.Components;
public class ServiceDetailAddonsViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
public ServiceDetailAddonsViewComponent(ILocalizationService localization)
{
_localization = localization;
}
public IViewComponentResult Invoke(string key)
{
var service = ServiceDetailCatalog.Get(key);
var model = new ServiceDetailAddonsViewModel
{
// Data
Addons = service.Addons,
// Labels
LabelAddonsForService = _localization.Get("services.detail.addons.addonsForService"),
LabelAddExistingAddon = _localization.Get("services.detail.addons.addExistingAddon")
};
return View(model);
}
}
public class ServiceDetailAddonsViewModel
{
// Data
public required List<ServiceAddon> Addons { get; init; }
// Labels
public required string LabelAddonsForService { get; init; }
public required string LabelAddExistingAddon { get; init; }
}

View file

@ -62,7 +62,35 @@ public static class ServiceDetailCatalog
ShowInOnlineBookingRules = true,
AllowEmployeeSelection = true,
ShowPrice = true,
ShowDuration = true
ShowDuration = true,
// Medarbejdere
Employees = new()
{
new("emp-1", "Anna Sørensen", "AS", "Master Stylist", "master", true, "Standard"),
new("emp-2", "Mette Jensen", "MJ", "Senior Stylist", "senior", true, "+15 min"),
new("emp-3", "Louise Nielsen", "LN", "Senior Stylist", "senior", true, "Standard"),
new("emp-4", "Katrine Pedersen", "KP", "Stylist", "junior", true, "+15 min"),
new("emp-5", "Sofie Andersen", "SA", "Junior Stylist", "junior", false, "—", "Ikke certificeret")
},
Availability = new()
{
new("Mandag", true, "08:00", "18:00"),
new("Tirsdag", true, "08:00", "18:00"),
new("Onsdag", true, "08:00", "18:00"),
new("Torsdag", true, "08:00", "12:00"),
new("Fredag", true, "08:00", "18:00"),
new("Lørdag", false, "08:00", "18:00"),
new("Søndag", false, "08:00", "18:00")
},
// Tilvalg
Addons = new()
{
new("addon-1", "Olaplex Behandling", "+250 kr", "+15 min", "Valgfri", true),
new("addon-2", "Kerastase Hårkur", "+195 kr", "+10 min", "Valgfri", true),
new("addon-3", "Styling / Curls", "+150 kr", "+20 min", "Valgfri", true),
new("addon-4", "Hovedbundsmassage", "+75 kr", "+5 min", "Valgfri", true),
new("addon-5", "Patch Test", "Gratis", "48t før", "Påkrævet (nye)", false, true)
}
},
["service-2"] = new ServiceDetailRecord
{
@ -233,6 +261,13 @@ public record ServiceDetailRecord
public bool AllowEmployeeSelection { get; init; } = true;
public bool ShowPrice { get; init; } = true;
public bool ShowDuration { get; init; } = true;
// Medarbejdere tab
public List<ServiceEmployee> Employees { get; init; } = new();
public List<ServiceAvailability> Availability { get; init; } = new();
// Tilvalg tab
public List<ServiceAddon> Addons { get; init; } = new();
}
public enum PriceMode { Simple, Matrix }
@ -242,3 +277,28 @@ public record PriceMatrixRow(string Level, string ShortHair, string MediumHair,
public record DurationVariant(string Name, int Minutes);
public record ServiceTag(string Text, string CssClass);
public record ServiceEmployee(
string Id,
string Name,
string Initials,
string Level,
string LevelCssClass,
bool Selected,
string OverrideValue,
string? Warning = null);
public record ServiceAddon(
string Id,
string Name,
string Price,
string Duration,
string Type,
bool Selected,
bool Required = false);
public record ServiceAvailability(
string Day,
bool Enabled,
string StartTime,
string EndTime);

View file

@ -0,0 +1,66 @@
@model PlanTempus.Application.Features.Services.Components.ServiceDetailEmployeesViewModel
<swp-detail-grid>
<div>
<swp-card>
<swp-section-label>@Model.LabelEmployeesForService</swp-section-label>
<swp-selectable-list>
@foreach (var employee in Model.Employees)
{
<swp-selectable-item class="@(employee.Selected ? "selected" : "") @(employee.Warning != null ? "disabled" : "")">
<swp-selectable-checkbox>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-selectable-checkbox>
<swp-avatar class="@employee.LevelCssClass">@employee.Initials</swp-avatar>
<swp-selectable-info>
<swp-selectable-name>
@employee.Name
@if (employee.Warning != null)
{
<swp-selectable-warning>@employee.Warning</swp-selectable-warning>
}
</swp-selectable-name>
<swp-tag class="@employee.LevelCssClass">@employee.Level</swp-tag>
</swp-selectable-info>
<swp-selectable-override>
@Model.LabelDuration: <swp-selectable-override-value>@employee.OverrideValue</swp-selectable-override-value>
</swp-selectable-override>
</swp-selectable-item>
}
</swp-selectable-list>
<swp-see-all>@Model.LabelSelectAll</swp-see-all>
</swp-card>
</div>
<div>
<swp-card>
<swp-section-label>@Model.LabelAvailability</swp-section-label>
<swp-availability-list>
@foreach (var day in Model.Availability)
{
<swp-availability-row data-enabled="@(day.Enabled ? "true" : "false")">
<swp-availability-day>@day.Day</swp-availability-day>
<swp-toggle-slider data-value="@(day.Enabled ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
<swp-availability-time>
<swp-time-range>
<swp-time-range-slider>
<swp-time-range-track></swp-time-range-track>
<swp-time-range-fill></swp-time-range-fill>
<input type="range" class="range-start" min="0" max="60" value="8" step="1" @(day.Enabled ? "" : "disabled")>
<input type="range" class="range-end" min="0" max="60" value="48" step="1" @(day.Enabled ? "" : "disabled")>
</swp-time-range-slider>
<swp-time-range-label>@day.StartTime @day.EndTime</swp-time-range-label>
</swp-time-range>
</swp-availability-time>
</swp-availability-row>
}
</swp-availability-list>
</swp-card>
</div>
</swp-detail-grid>

View file

@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Services.Components;
public class ServiceDetailEmployeesViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
public ServiceDetailEmployeesViewComponent(ILocalizationService localization)
{
_localization = localization;
}
public IViewComponentResult Invoke(string key)
{
var service = ServiceDetailCatalog.Get(key);
var model = new ServiceDetailEmployeesViewModel
{
// Data
Employees = service.Employees,
Availability = service.Availability,
// Labels
LabelEmployeesForService = _localization.Get("services.detail.employees.employeesForService"),
LabelSelectAll = _localization.Get("services.detail.employees.selectAll"),
LabelAvailability = _localization.Get("services.detail.employees.availability"),
LabelDuration = _localization.Get("services.detail.employees.duration"),
ToggleYes = _localization.Get("common.yes"),
ToggleNo = _localization.Get("common.no")
};
return View(model);
}
}
public class ServiceDetailEmployeesViewModel
{
// Data
public required List<ServiceEmployee> Employees { get; init; }
public required List<ServiceAvailability> Availability { get; init; }
// Labels
public required string LabelEmployeesForService { get; init; }
public required string LabelSelectAll { get; init; }
public required string LabelAvailability { get; init; }
public required string LabelDuration { get; init; }
// Toggle labels
public required string ToggleYes { get; init; }
public required string ToggleNo { get; init; }
}

View file

@ -56,19 +56,13 @@
<swp-tab-content data-tab="employees">
<swp-page-container>
<swp-card>
<swp-section-label>Medarbejdere</swp-section-label>
<p style="color: var(--color-text-secondary);">Medarbejdere-tab kommer snart...</p>
</swp-card>
@await Component.InvokeAsync("ServiceDetailEmployees", Model.ServiceKey)
</swp-page-container>
</swp-tab-content>
<swp-tab-content data-tab="addons">
<swp-page-container>
<swp-card>
<swp-section-label>Tilvalg</swp-section-label>
<p style="color: var(--color-text-secondary);">Tilvalg-tab kommer snart...</p>
</swp-card>
@await Component.InvokeAsync("ServiceDetailAddons", Model.ServiceKey)
</swp-page-container>
</swp-tab-content>