Enhances Services module with detail view and interactions
Adds comprehensive service detail view with multiple tabs and dynamic interactions Implements client-side navigation between service list and detail views Introduces mock service data catalog for flexible component rendering Extends localization support for new service detail screens Improves user experience by adding edit capabilities and smooth view transitions
This commit is contained in:
parent
fad5e46dfb
commit
120367acbb
22 changed files with 1780 additions and 597 deletions
|
|
@ -1,68 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class CategoryTableViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
|
||||
public CategoryTableViewComponent(ILocalizationService localization, IWebHostEnvironment env)
|
||||
{
|
||||
_localization = localization;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string key)
|
||||
{
|
||||
var data = LoadServiceData();
|
||||
var model = new CategoryTableViewModel
|
||||
{
|
||||
Key = key,
|
||||
CreateButtonText = _localization.Get("services.createCategory"),
|
||||
ColumnCategory = _localization.Get("services.table.category"),
|
||||
ColumnServiceCount = _localization.Get("services.table.serviceCount"),
|
||||
Categories = data.Categories
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Select(c => new CategoryItemViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.Name,
|
||||
SortOrder = c.SortOrder,
|
||||
ServiceCount = data.Services.Count(s => s.CategoryId == c.Id)
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private ServiceMockData LoadServiceData()
|
||||
{
|
||||
var jsonPath = Path.Combine(_env.ContentRootPath, "Features", "Services", "Data", "servicesMock.json");
|
||||
var json = System.IO.File.ReadAllText(jsonPath);
|
||||
return JsonSerializer.Deserialize<ServiceMockData>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new ServiceMockData();
|
||||
}
|
||||
}
|
||||
|
||||
public class CategoryTableViewModel
|
||||
{
|
||||
public required string Key { get; init; }
|
||||
public required string CreateButtonText { get; init; }
|
||||
public required string ColumnCategory { get; init; }
|
||||
public required string ColumnServiceCount { get; init; }
|
||||
public required IReadOnlyList<CategoryItemViewModel> Categories { get; init; }
|
||||
}
|
||||
|
||||
public class CategoryItemViewModel
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public int SortOrder { get; init; }
|
||||
public int ServiceCount { get; init; }
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.CategoryTableViewModel
|
||||
|
||||
<swp-services-header>
|
||||
<div></div>
|
||||
<swp-btn class="primary">
|
||||
<i class="ph ph-plus"></i>
|
||||
@Model.CreateButtonText
|
||||
</swp-btn>
|
||||
</swp-services-header>
|
||||
|
||||
<swp-card class="categories-list">
|
||||
<swp-data-table>
|
||||
<swp-data-table-header>
|
||||
<swp-data-table-cell>@Model.ColumnCategory</swp-data-table-cell>
|
||||
<swp-data-table-cell>@Model.ColumnServiceCount</swp-data-table-cell>
|
||||
<swp-data-table-cell></swp-data-table-cell>
|
||||
</swp-data-table-header>
|
||||
@foreach (var category in Model.Categories)
|
||||
{
|
||||
<swp-data-table-row data-category-detail="@category.Id">
|
||||
<swp-data-table-cell>@category.Name</swp-data-table-cell>
|
||||
<swp-data-table-cell>@category.ServiceCount</swp-data-table-cell>
|
||||
<swp-data-table-cell>
|
||||
<swp-row-toggle>
|
||||
<i class="ph ph-caret-right"></i>
|
||||
</swp-row-toggle>
|
||||
</swp-data-table-cell>
|
||||
</swp-data-table-row>
|
||||
}
|
||||
</swp-data-table>
|
||||
</swp-card>
|
||||
|
|
@ -10,7 +10,11 @@
|
|||
</swp-data-table-cell>
|
||||
<swp-data-table-cell></swp-data-table-cell>
|
||||
<swp-data-table-cell></swp-data-table-cell>
|
||||
<swp-data-table-cell></swp-data-table-cell>
|
||||
<swp-data-table-cell>
|
||||
<swp-category-edit data-category-id="@Model.Id">
|
||||
<i class="ph ph-pencil-simple"></i>
|
||||
</swp-category-edit>
|
||||
</swp-data-table-cell>
|
||||
</swp-category-row>
|
||||
|
||||
@foreach (var service in Model.Services)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shared catalog for service detail data.
|
||||
/// Used by all ServiceDetail* ViewComponents.
|
||||
/// </summary>
|
||||
public static class ServiceDetailCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, ServiceDetailRecord> Services = new()
|
||||
{
|
||||
["service-1"] = new ServiceDetailRecord
|
||||
{
|
||||
Key = "service-1",
|
||||
Name = "Klip & Farve",
|
||||
Category = "Kombi-behandlinger",
|
||||
CalendarColor = "deep-orange",
|
||||
IsActive = true,
|
||||
DurationRange = "60-120",
|
||||
FromPrice = "795 kr",
|
||||
EmployeeCount = "4",
|
||||
BookingsThisYear = "156",
|
||||
Tags = new() { new("Populær", "popular"), new("Kombi", "combo"), new("Farve", "color") },
|
||||
InternalNotes = "Komplet farvebehandling med klip. Husk konsultation ved første besøg. Anbefal Olaplex til kemisk behandlet hår.",
|
||||
CanBookAsMain = true,
|
||||
CanBookAsAddon = false,
|
||||
ShowInOnlineBooking = true,
|
||||
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."
|
||||
},
|
||||
["service-2"] = new ServiceDetailRecord
|
||||
{
|
||||
Key = "service-2",
|
||||
Name = "Herreklip",
|
||||
Category = "Klip",
|
||||
CalendarColor = "blue",
|
||||
IsActive = true,
|
||||
DurationRange = "30",
|
||||
FromPrice = "295 kr",
|
||||
EmployeeCount = "6",
|
||||
BookingsThisYear = "312",
|
||||
Tags = new() { new("Populær", "popular") },
|
||||
InternalNotes = "Standard herreklip. Inkluderer vask og styling.",
|
||||
CanBookAsMain = true,
|
||||
CanBookAsAddon = false,
|
||||
ShowInOnlineBooking = true,
|
||||
IsFeatured = true,
|
||||
Description = "Klassisk herreklip med vask, klip og styling. Vores erfarne stylister sikrer dig et skarpt og velplejet look."
|
||||
},
|
||||
["service-3"] = new ServiceDetailRecord
|
||||
{
|
||||
Key = "service-3",
|
||||
Name = "Dameklip",
|
||||
Category = "Klip",
|
||||
CalendarColor = "teal",
|
||||
IsActive = true,
|
||||
DurationRange = "45-60",
|
||||
FromPrice = "395 kr",
|
||||
EmployeeCount = "5",
|
||||
BookingsThisYear = "248",
|
||||
Tags = new() { new("Populær", "popular") },
|
||||
InternalNotes = "Standard dameklip. Altid konsultation først.",
|
||||
CanBookAsMain = true,
|
||||
CanBookAsAddon = false,
|
||||
ShowInOnlineBooking = true,
|
||||
IsFeatured = true,
|
||||
Description = "Professionel dameklip tilpasset din ansigtsform og ønsker. Inkluderer vask, klip og føn."
|
||||
},
|
||||
["service-4"] = new ServiceDetailRecord
|
||||
{
|
||||
Key = "service-4",
|
||||
Name = "Balayage",
|
||||
Category = "Farve",
|
||||
CalendarColor = "purple",
|
||||
IsActive = true,
|
||||
DurationRange = "120-180",
|
||||
FromPrice = "1.295 kr",
|
||||
EmployeeCount = "3",
|
||||
BookingsThisYear = "89",
|
||||
Tags = new() { new("Farve", "color"), new("Premium", "popular") },
|
||||
InternalNotes = "Avanceret farveteknik. Kun certificerede stylister. Kræver tid til konsultation.",
|
||||
CanBookAsMain = true,
|
||||
CanBookAsAddon = false,
|
||||
ShowInOnlineBooking = true,
|
||||
IsFeatured = false,
|
||||
Description = "Smuk, naturlig farveeffekt med håndmalede highlights. Perfekt til dig der ønsker et solkysset look med lavt vedligehold."
|
||||
},
|
||||
["service-5"] = new ServiceDetailRecord
|
||||
{
|
||||
Key = "service-5",
|
||||
Name = "Olaplex Behandling",
|
||||
Category = "Behandlinger",
|
||||
CalendarColor = "green",
|
||||
IsActive = true,
|
||||
DurationRange = "30",
|
||||
FromPrice = "295 kr",
|
||||
EmployeeCount = "6",
|
||||
BookingsThisYear = "134",
|
||||
Tags = new() { new("Tilvalg", "combo") },
|
||||
InternalNotes = "Kan tilføjes til alle farvebehandlinger. Anbefales ved kemisk behandlet hår.",
|
||||
CanBookAsMain = false,
|
||||
CanBookAsAddon = true,
|
||||
ShowInOnlineBooking = true,
|
||||
IsFeatured = false,
|
||||
Description = "Intensiv hårbehandling der reparerer og styrker håret. Ideel som tilvalg til farvebehandlinger."
|
||||
},
|
||||
["service-6"] = new ServiceDetailRecord
|
||||
{
|
||||
Key = "service-6",
|
||||
Name = "Bryllupsfrisure",
|
||||
Category = "Styling",
|
||||
CalendarColor = "amber",
|
||||
IsActive = false,
|
||||
DurationRange = "90-120",
|
||||
FromPrice = "895 kr",
|
||||
EmployeeCount = "2",
|
||||
BookingsThisYear = "24",
|
||||
Tags = new() { new("Premium", "popular"), new("Sæson", "combo") },
|
||||
InternalNotes = "Kun Maria og Anna kan bookes til dette. Kræver prøve-session 2 uger før.",
|
||||
CanBookAsMain = true,
|
||||
CanBookAsAddon = false,
|
||||
ShowInOnlineBooking = false,
|
||||
IsFeatured = false,
|
||||
Description = "Eksklusiv bryllupsfrisure med forudgående konsultation og prøvesession. Vi skaber din drømmefrisure til den store dag."
|
||||
}
|
||||
};
|
||||
|
||||
public static ServiceDetailRecord Get(string key)
|
||||
{
|
||||
if (!Services.TryGetValue(key, out var service))
|
||||
throw new KeyNotFoundException($"Service with key '{key}' not found");
|
||||
return service;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AllKeys => Services.Keys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete service detail record used across all detail ViewComponents.
|
||||
/// </summary>
|
||||
public record ServiceDetailRecord
|
||||
{
|
||||
// Identity
|
||||
public required string Key { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string Category { get; init; }
|
||||
public required string CalendarColor { get; init; }
|
||||
public required bool IsActive { get; init; }
|
||||
|
||||
// Stats (header)
|
||||
public required string DurationRange { get; init; }
|
||||
public required string FromPrice { get; init; }
|
||||
public required string EmployeeCount { get; init; }
|
||||
public required string BookingsThisYear { get; init; }
|
||||
|
||||
// Tags
|
||||
public List<ServiceTag> Tags { get; init; } = new();
|
||||
|
||||
// Generelt tab - Grundlæggende
|
||||
public required string InternalNotes { get; init; }
|
||||
|
||||
// Generelt tab - Bookingtype
|
||||
public required bool CanBookAsMain { get; init; }
|
||||
public required bool CanBookAsAddon { get; init; }
|
||||
|
||||
// Generelt tab - Online booking
|
||||
public required bool ShowInOnlineBooking { get; init; }
|
||||
public required bool IsFeatured { get; init; }
|
||||
public required string Description { get; init; }
|
||||
}
|
||||
|
||||
public record ServiceTag(string Text, string CssClass);
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceDetailGeneralViewModel
|
||||
|
||||
<swp-detail-grid>
|
||||
<!-- Left column -->
|
||||
<div>
|
||||
<!-- Basic Info Card -->
|
||||
<swp-card>
|
||||
<swp-section-label>@Model.LabelBasic</swp-section-label>
|
||||
<swp-edit-section>
|
||||
<swp-edit-row>
|
||||
<swp-edit-label>@Model.LabelServiceName</swp-edit-label>
|
||||
<input type="text" id="servicename" value="@Model.Name">
|
||||
</swp-edit-row>
|
||||
<swp-edit-row>
|
||||
<swp-edit-label>@Model.LabelCategory</swp-edit-label>
|
||||
<swp-select data-value="@Model.CategoryValue">
|
||||
<button type="button" popovertarget="category-select" aria-expanded="false">
|
||||
<swp-select-value>@Model.Category</swp-select-value>
|
||||
<i class="ph ph-caret-down"></i>
|
||||
</button>
|
||||
<div popover id="category-select">
|
||||
<swp-select-option data-value="kombi" class="@(Model.CategoryValue == "kombi" ? "selected" : "")">Kombi-behandlinger</swp-select-option>
|
||||
<swp-select-option data-value="klip" class="@(Model.CategoryValue == "klip" ? "selected" : "")">Klip</swp-select-option>
|
||||
<swp-select-option data-value="farve" class="@(Model.CategoryValue == "farve" ? "selected" : "")">Farve</swp-select-option>
|
||||
<swp-select-option data-value="behandlinger" class="@(Model.CategoryValue == "behandlinger" ? "selected" : "")">Behandlinger</swp-select-option>
|
||||
<swp-select-option data-value="styling" class="@(Model.CategoryValue == "styling" ? "selected" : "")">Styling</swp-select-option>
|
||||
</div>
|
||||
</swp-select>
|
||||
</swp-edit-row>
|
||||
<swp-edit-row>
|
||||
<swp-edit-label>@Model.LabelCalendarColor</swp-edit-label>
|
||||
<swp-select data-value="@Model.CalendarColor">
|
||||
<button type="button" popovertarget="color-select" aria-expanded="false">
|
||||
<swp-select-value>@Model.CalendarColorLabel</swp-select-value>
|
||||
<i class="ph ph-caret-down"></i>
|
||||
</button>
|
||||
<div popover id="color-select">
|
||||
<swp-select-option data-value="red" class="@(Model.CalendarColor == "red" ? "selected" : "")">Rød</swp-select-option>
|
||||
<swp-select-option data-value="pink" class="@(Model.CalendarColor == "pink" ? "selected" : "")">Pink</swp-select-option>
|
||||
<swp-select-option data-value="purple" class="@(Model.CalendarColor == "purple" ? "selected" : "")">Lilla</swp-select-option>
|
||||
<swp-select-option data-value="deep-purple" class="@(Model.CalendarColor == "deep-purple" ? "selected" : "")">Mørk lilla</swp-select-option>
|
||||
<swp-select-option data-value="indigo" class="@(Model.CalendarColor == "indigo" ? "selected" : "")">Indigo</swp-select-option>
|
||||
<swp-select-option data-value="blue" class="@(Model.CalendarColor == "blue" ? "selected" : "")">Blå</swp-select-option>
|
||||
<swp-select-option data-value="light-blue" class="@(Model.CalendarColor == "light-blue" ? "selected" : "")">Lyseblå</swp-select-option>
|
||||
<swp-select-option data-value="cyan" class="@(Model.CalendarColor == "cyan" ? "selected" : "")">Cyan</swp-select-option>
|
||||
<swp-select-option data-value="teal" class="@(Model.CalendarColor == "teal" ? "selected" : "")">Teal</swp-select-option>
|
||||
<swp-select-option data-value="green" class="@(Model.CalendarColor == "green" ? "selected" : "")">Grøn</swp-select-option>
|
||||
<swp-select-option data-value="light-green" class="@(Model.CalendarColor == "light-green" ? "selected" : "")">Lysegrøn</swp-select-option>
|
||||
<swp-select-option data-value="lime" class="@(Model.CalendarColor == "lime" ? "selected" : "")">Lime</swp-select-option>
|
||||
<swp-select-option data-value="yellow" class="@(Model.CalendarColor == "yellow" ? "selected" : "")">Gul</swp-select-option>
|
||||
<swp-select-option data-value="amber" class="@(Model.CalendarColor == "amber" ? "selected" : "")">Amber</swp-select-option>
|
||||
<swp-select-option data-value="orange" class="@(Model.CalendarColor == "orange" ? "selected" : "")">Orange</swp-select-option>
|
||||
<swp-select-option data-value="deep-orange" class="@(Model.CalendarColor == "deep-orange" ? "selected" : "")">Mørk orange</swp-select-option>
|
||||
</div>
|
||||
</swp-select>
|
||||
</swp-edit-row>
|
||||
<swp-edit-row>
|
||||
<swp-edit-label>@Model.LabelIsActive</swp-edit-label>
|
||||
<swp-toggle-slider data-value="@(Model.IsActive ? "yes" : "no")">
|
||||
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
|
||||
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
|
||||
</swp-toggle-slider>
|
||||
</swp-edit-row>
|
||||
</swp-edit-section>
|
||||
|
||||
<swp-section-label class="spaced">@Model.LabelInternalNotes</swp-section-label>
|
||||
<textarea id="internalnotes" rows="3">@Model.InternalNotes</textarea>
|
||||
</swp-card>
|
||||
|
||||
<!-- Booking Type Card -->
|
||||
<swp-card>
|
||||
<swp-section-label>@Model.LabelBookingType</swp-section-label>
|
||||
<swp-toggle-row>
|
||||
<div>
|
||||
<swp-toggle-label>@Model.LabelCanBookAsMain</swp-toggle-label>
|
||||
<swp-toggle-description>@Model.LabelCanBookAsMainDesc</swp-toggle-description>
|
||||
</div>
|
||||
<swp-toggle-slider data-value="@(Model.CanBookAsMain ? "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.LabelCanBookAsAddon</swp-toggle-label>
|
||||
<swp-toggle-description>@Model.LabelCanBookAsAddonDesc</swp-toggle-description>
|
||||
</div>
|
||||
<swp-toggle-slider data-value="@(Model.CanBookAsAddon ? "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>
|
||||
</div>
|
||||
|
||||
<!-- Right column -->
|
||||
<div>
|
||||
<!-- Online Booking Card -->
|
||||
<swp-card>
|
||||
<swp-section-label>@Model.LabelOnlineBooking</swp-section-label>
|
||||
<swp-toggle-row>
|
||||
<div>
|
||||
<swp-toggle-label>@Model.LabelShowInOnlineBooking</swp-toggle-label>
|
||||
<swp-toggle-description>@Model.LabelShowInOnlineBookingDesc</swp-toggle-description>
|
||||
</div>
|
||||
<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>
|
||||
<div>
|
||||
<swp-toggle-label>@Model.LabelIsFeatured</swp-toggle-label>
|
||||
<swp-toggle-description>@Model.LabelIsFeaturedDesc</swp-toggle-description>
|
||||
</div>
|
||||
<swp-toggle-slider data-value="@(Model.IsFeatured ? "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-section-label class="spaced">@Model.LabelDescription</swp-section-label>
|
||||
<textarea id="description" rows="4">@Model.Description</textarea>
|
||||
|
||||
<swp-section-label class="spaced">@Model.LabelImage</swp-section-label>
|
||||
<swp-btn class="secondary">@Model.LabelUploadImage</swp-btn>
|
||||
</swp-card>
|
||||
</div>
|
||||
</swp-detail-grid>
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceDetailGeneralViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
// Category display -> value mapping
|
||||
private static readonly Dictionary<string, string> CategoryValues = new()
|
||||
{
|
||||
["Kombi-behandlinger"] = "kombi",
|
||||
["Klip"] = "klip",
|
||||
["Farve"] = "farve",
|
||||
["Behandlinger"] = "behandlinger",
|
||||
["Styling"] = "styling"
|
||||
};
|
||||
|
||||
// Color value -> display label mapping
|
||||
private static readonly Dictionary<string, string> ColorLabels = new()
|
||||
{
|
||||
["red"] = "Rød",
|
||||
["pink"] = "Pink",
|
||||
["purple"] = "Lilla",
|
||||
["deep-purple"] = "Mørk lilla",
|
||||
["indigo"] = "Indigo",
|
||||
["blue"] = "Blå",
|
||||
["light-blue"] = "Lyseblå",
|
||||
["cyan"] = "Cyan",
|
||||
["teal"] = "Teal",
|
||||
["green"] = "Grøn",
|
||||
["light-green"] = "Lysegrøn",
|
||||
["lime"] = "Lime",
|
||||
["yellow"] = "Gul",
|
||||
["amber"] = "Amber",
|
||||
["orange"] = "Orange",
|
||||
["deep-orange"] = "Mørk orange"
|
||||
};
|
||||
|
||||
public ServiceDetailGeneralViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string key)
|
||||
{
|
||||
var service = ServiceDetailCatalog.Get(key);
|
||||
|
||||
var model = new ServiceDetailGeneralViewModel
|
||||
{
|
||||
// Data
|
||||
Name = service.Name,
|
||||
Category = service.Category,
|
||||
CategoryValue = CategoryValues.GetValueOrDefault(service.Category, "kombi"),
|
||||
CalendarColor = service.CalendarColor,
|
||||
CalendarColorLabel = ColorLabels.GetValueOrDefault(service.CalendarColor, service.CalendarColor),
|
||||
IsActive = service.IsActive,
|
||||
InternalNotes = service.InternalNotes,
|
||||
CanBookAsMain = service.CanBookAsMain,
|
||||
CanBookAsAddon = service.CanBookAsAddon,
|
||||
ShowInOnlineBooking = service.ShowInOnlineBooking,
|
||||
IsFeatured = service.IsFeatured,
|
||||
Description = service.Description,
|
||||
|
||||
// Labels
|
||||
LabelBasic = _localization.Get("services.detail.general.basic"),
|
||||
LabelServiceName = _localization.Get("services.detail.general.serviceName"),
|
||||
LabelCategory = _localization.Get("services.detail.general.category"),
|
||||
LabelCalendarColor = _localization.Get("services.detail.general.calendarColor"),
|
||||
LabelIsActive = _localization.Get("services.detail.general.isActive"),
|
||||
LabelInternalNotes = _localization.Get("services.detail.general.internalNotes"),
|
||||
LabelBookingType = _localization.Get("services.detail.general.bookingType"),
|
||||
LabelCanBookAsMain = _localization.Get("services.detail.general.canBookAsMain"),
|
||||
LabelCanBookAsMainDesc = _localization.Get("services.detail.general.canBookAsMainDesc"),
|
||||
LabelCanBookAsAddon = _localization.Get("services.detail.general.canBookAsAddon"),
|
||||
LabelCanBookAsAddonDesc = _localization.Get("services.detail.general.canBookAsAddonDesc"),
|
||||
LabelOnlineBooking = _localization.Get("services.detail.general.onlineBooking"),
|
||||
LabelShowInOnlineBooking = _localization.Get("services.detail.general.showInOnlineBooking"),
|
||||
LabelShowInOnlineBookingDesc = _localization.Get("services.detail.general.showInOnlineBookingDesc"),
|
||||
LabelIsFeatured = _localization.Get("services.detail.general.isFeatured"),
|
||||
LabelIsFeaturedDesc = _localization.Get("services.detail.general.isFeaturedDesc"),
|
||||
LabelDescription = _localization.Get("services.detail.general.description"),
|
||||
LabelImage = _localization.Get("services.detail.general.image"),
|
||||
LabelUploadImage = _localization.Get("services.detail.general.uploadImage"),
|
||||
ToggleYes = _localization.Get("common.yes"),
|
||||
ToggleNo = _localization.Get("common.no")
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceDetailGeneralViewModel
|
||||
{
|
||||
// Data
|
||||
public required string Name { get; init; }
|
||||
public required string Category { get; init; }
|
||||
public required string CategoryValue { get; init; }
|
||||
public required string CalendarColor { get; init; }
|
||||
public required string CalendarColorLabel { get; init; }
|
||||
public required bool IsActive { get; init; }
|
||||
public required string InternalNotes { get; init; }
|
||||
public required bool CanBookAsMain { get; init; }
|
||||
public required bool CanBookAsAddon { get; init; }
|
||||
public required bool ShowInOnlineBooking { get; init; }
|
||||
public required bool IsFeatured { get; init; }
|
||||
public required string Description { get; init; }
|
||||
|
||||
// Labels - Basic
|
||||
public required string LabelBasic { get; init; }
|
||||
public required string LabelServiceName { get; init; }
|
||||
public required string LabelCategory { get; init; }
|
||||
public required string LabelCalendarColor { get; init; }
|
||||
public required string LabelIsActive { get; init; }
|
||||
|
||||
// Labels - Internal Notes
|
||||
public required string LabelInternalNotes { get; init; }
|
||||
|
||||
// Labels - Booking Type
|
||||
public required string LabelBookingType { get; init; }
|
||||
public required string LabelCanBookAsMain { get; init; }
|
||||
public required string LabelCanBookAsMainDesc { get; init; }
|
||||
public required string LabelCanBookAsAddon { get; init; }
|
||||
public required string LabelCanBookAsAddonDesc { get; init; }
|
||||
|
||||
// Labels - Online Booking
|
||||
public required string LabelOnlineBooking { get; init; }
|
||||
public required string LabelShowInOnlineBooking { get; init; }
|
||||
public required string LabelShowInOnlineBookingDesc { get; init; }
|
||||
public required string LabelIsFeatured { get; init; }
|
||||
public required string LabelIsFeaturedDesc { get; init; }
|
||||
|
||||
// Labels - Description & Image
|
||||
public required string LabelDescription { get; init; }
|
||||
public required string LabelImage { get; init; }
|
||||
public required string LabelUploadImage { get; init; }
|
||||
|
||||
// Toggle labels
|
||||
public required string ToggleYes { get; init; }
|
||||
public required string ToggleNo { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceDetailHeaderViewModel
|
||||
|
||||
<swp-service-detail-header>
|
||||
<swp-service-info>
|
||||
<swp-service-name-row>
|
||||
<swp-service-name contenteditable="true">@Model.Name</swp-service-name>
|
||||
@if (Model.Tags.Any())
|
||||
{
|
||||
<swp-tags-row>
|
||||
@foreach (var tag in Model.Tags)
|
||||
{
|
||||
<swp-tag class="@tag.CssClass">@tag.Text</swp-tag>
|
||||
}
|
||||
</swp-tags-row>
|
||||
}
|
||||
<swp-status-indicator data-active="@Model.IsActive.ToString().ToLower()">
|
||||
<span class="icon">●</span>
|
||||
<span class="text">@Model.StatusText</span>
|
||||
</swp-status-indicator>
|
||||
</swp-service-name-row>
|
||||
<swp-fact-boxes-inline>
|
||||
<swp-fact-inline>
|
||||
<swp-fact-inline-value>@Model.DurationRange</swp-fact-inline-value>
|
||||
<swp-fact-inline-label>@Model.LabelDuration</swp-fact-inline-label>
|
||||
</swp-fact-inline>
|
||||
<swp-fact-inline>
|
||||
<swp-fact-inline-value>@Model.FromPrice</swp-fact-inline-value>
|
||||
<swp-fact-inline-label>@Model.LabelFromPrice</swp-fact-inline-label>
|
||||
</swp-fact-inline>
|
||||
<swp-fact-inline>
|
||||
<swp-fact-inline-value>@Model.EmployeeCount</swp-fact-inline-value>
|
||||
<swp-fact-inline-label>@Model.LabelEmployees</swp-fact-inline-label>
|
||||
</swp-fact-inline>
|
||||
<swp-fact-inline>
|
||||
<swp-fact-inline-value>@Model.BookingsThisYear</swp-fact-inline-value>
|
||||
<swp-fact-inline-label>@Model.LabelBookingsThisYear</swp-fact-inline-label>
|
||||
</swp-fact-inline>
|
||||
</swp-fact-boxes-inline>
|
||||
</swp-service-info>
|
||||
</swp-service-detail-header>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceDetailHeaderViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
public ServiceDetailHeaderViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string key)
|
||||
{
|
||||
var service = ServiceDetailCatalog.Get(key);
|
||||
|
||||
var model = new ServiceDetailHeaderViewModel
|
||||
{
|
||||
Name = service.Name,
|
||||
IsActive = service.IsActive,
|
||||
StatusText = service.IsActive
|
||||
? _localization.Get("services.detail.header.active")
|
||||
: _localization.Get("services.detail.header.inactive"),
|
||||
DurationRange = service.DurationRange,
|
||||
FromPrice = service.FromPrice,
|
||||
EmployeeCount = service.EmployeeCount,
|
||||
BookingsThisYear = service.BookingsThisYear,
|
||||
LabelDuration = _localization.Get("services.detail.header.duration"),
|
||||
LabelFromPrice = _localization.Get("services.detail.header.fromPrice"),
|
||||
LabelEmployees = _localization.Get("services.detail.header.employees"),
|
||||
LabelBookingsThisYear = _localization.Get("services.detail.header.bookingsThisYear"),
|
||||
Tags = service.Tags.Select(t => new ServiceTagViewModel { Text = t.Text, CssClass = t.CssClass }).ToList()
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceDetailHeaderViewModel
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required bool IsActive { get; init; }
|
||||
public required string StatusText { get; init; }
|
||||
public required string DurationRange { get; init; }
|
||||
public required string FromPrice { get; init; }
|
||||
public required string EmployeeCount { get; init; }
|
||||
public required string BookingsThisYear { get; init; }
|
||||
public required string LabelDuration { get; init; }
|
||||
public required string LabelFromPrice { get; init; }
|
||||
public required string LabelEmployees { get; init; }
|
||||
public required string LabelBookingsThisYear { get; init; }
|
||||
public List<ServiceTagViewModel> Tags { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ServiceTagViewModel
|
||||
{
|
||||
public required string Text { get; init; }
|
||||
public required string CssClass { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceDetailViewViewModel
|
||||
|
||||
<swp-service-detail-view id="service-detail-view" data-service="@Model.ServiceKey">
|
||||
<!-- Sticky Header (generic from page.css) -->
|
||||
<swp-sticky-header>
|
||||
<swp-header-content>
|
||||
<!-- Page Header with Back Button -->
|
||||
<swp-page-header>
|
||||
<swp-page-title>
|
||||
<swp-back-link data-service-back>
|
||||
<i class="ph ph-arrow-left"></i>
|
||||
@Model.BackText
|
||||
</swp-back-link>
|
||||
</swp-page-title>
|
||||
<swp-page-actions>
|
||||
<swp-btn class="primary">
|
||||
<i class="ph ph-floppy-disk"></i>
|
||||
@Model.SaveButtonText
|
||||
</swp-btn>
|
||||
</swp-page-actions>
|
||||
</swp-page-header>
|
||||
|
||||
<!-- Service Header -->
|
||||
@await Component.InvokeAsync("ServiceDetailHeader", Model.ServiceKey)
|
||||
</swp-header-content>
|
||||
|
||||
<!-- Tabs (outside header-content, inside sticky-header) -->
|
||||
<swp-tab-bar>
|
||||
<swp-tab class="active" data-tab="general">@Model.TabGeneral</swp-tab>
|
||||
<swp-tab data-tab="prices">@Model.TabPrices</swp-tab>
|
||||
<swp-tab data-tab="duration">@Model.TabDuration</swp-tab>
|
||||
<swp-tab data-tab="employees">@Model.TabEmployees</swp-tab>
|
||||
<swp-tab data-tab="addons">@Model.TabAddons</swp-tab>
|
||||
<swp-tab data-tab="rules">@Model.TabRules</swp-tab>
|
||||
</swp-tab-bar>
|
||||
</swp-sticky-header>
|
||||
|
||||
<!-- Tab Contents -->
|
||||
<swp-tab-content data-tab="general" class="active">
|
||||
<swp-page-container>
|
||||
@await Component.InvokeAsync("ServiceDetailGeneral", Model.ServiceKey)
|
||||
</swp-page-container>
|
||||
</swp-tab-content>
|
||||
|
||||
<swp-tab-content data-tab="prices">
|
||||
<swp-page-container>
|
||||
<swp-card>
|
||||
<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-tab-content>
|
||||
|
||||
<swp-tab-content data-tab="duration">
|
||||
<swp-page-container>
|
||||
<swp-card>
|
||||
<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-tab-content>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</swp-page-container>
|
||||
</swp-tab-content>
|
||||
|
||||
<swp-tab-content data-tab="rules">
|
||||
<swp-page-container>
|
||||
<swp-card>
|
||||
<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-tab-content>
|
||||
</swp-service-detail-view>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceDetailViewViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
public ServiceDetailViewViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string key)
|
||||
{
|
||||
var service = ServiceDetailCatalog.Get(key);
|
||||
|
||||
var model = new ServiceDetailViewViewModel
|
||||
{
|
||||
ServiceKey = service.Key,
|
||||
BackText = _localization.Get("services.detail.back"),
|
||||
SaveButtonText = _localization.Get("services.detail.save"),
|
||||
TabGeneral = _localization.Get("services.detail.tabs.general"),
|
||||
TabPrices = _localization.Get("services.detail.tabs.prices"),
|
||||
TabDuration = _localization.Get("services.detail.tabs.duration"),
|
||||
TabEmployees = _localization.Get("services.detail.tabs.employees"),
|
||||
TabAddons = _localization.Get("services.detail.tabs.addons"),
|
||||
TabRules = _localization.Get("services.detail.tabs.rules")
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceDetailViewViewModel
|
||||
{
|
||||
public required string ServiceKey { get; init; }
|
||||
public required string BackText { get; init; }
|
||||
public required string SaveButtonText { get; init; }
|
||||
public required string TabGeneral { get; init; }
|
||||
public required string TabPrices { get; init; }
|
||||
public required string TabDuration { get; init; }
|
||||
public required string TabEmployees { get; init; }
|
||||
public required string TabAddons { get; init; }
|
||||
public required string TabRules { get; init; }
|
||||
}
|
||||
|
|
@ -5,10 +5,16 @@
|
|||
<i class="ph ph-magnifying-glass"></i>
|
||||
<input type="text" placeholder="@Model.SearchPlaceholder" />
|
||||
</swp-search-input>
|
||||
<swp-btn class="primary">
|
||||
<i class="ph ph-plus"></i>
|
||||
@Model.CreateButtonText
|
||||
</swp-btn>
|
||||
<swp-btn-group>
|
||||
<swp-btn class="secondary" data-drawer-trigger="category-drawer">
|
||||
<i class="ph ph-folders"></i>
|
||||
@Model.CreateCategoryButtonText
|
||||
</swp-btn>
|
||||
<swp-btn class="primary">
|
||||
<i class="ph ph-plus"></i>
|
||||
@Model.CreateButtonText
|
||||
</swp-btn>
|
||||
</swp-btn-group>
|
||||
</swp-services-header>
|
||||
|
||||
<swp-card class="services-list">
|
||||
|
|
@ -25,3 +31,56 @@
|
|||
}
|
||||
</swp-data-table>
|
||||
</swp-card>
|
||||
|
||||
<!-- Category Drawer -->
|
||||
<div id="category-drawer" data-drawer="lg">
|
||||
<swp-drawer-header>
|
||||
<swp-drawer-title localize="services.categoryDrawer.title">Opret kategori</swp-drawer-title>
|
||||
<swp-drawer-close data-drawer-close>
|
||||
<i class="ph ph-x"></i>
|
||||
</swp-drawer-close>
|
||||
</swp-drawer-header>
|
||||
|
||||
<swp-drawer-body>
|
||||
<swp-form-row>
|
||||
<swp-form-label localize="services.categoryDrawer.name">Kategorinavn</swp-form-label>
|
||||
<input type="text" id="category-name" placeholder="F.eks. Klip & Styling">
|
||||
</swp-form-row>
|
||||
|
||||
<swp-form-row>
|
||||
<swp-form-label localize="services.categoryDrawer.description">Beskrivelse</swp-form-label>
|
||||
<textarea id="category-description" rows="3" placeholder="Vises i online booking..."></textarea>
|
||||
</swp-form-row>
|
||||
|
||||
<swp-form-divider></swp-form-divider>
|
||||
|
||||
<!-- Synlighed sektion -->
|
||||
<swp-section-label localize="services.categoryDrawer.visibilitySection">Synlighed</swp-section-label>
|
||||
|
||||
<swp-toggle-row>
|
||||
<swp-toggle-label>
|
||||
<span localize="services.categoryDrawer.showInBooking">Kategorien skal vises i online booking</span>
|
||||
<swp-toggle-description localize="services.categoryDrawer.showInBookingDescription">Kategorien vil stadig være synlig her i systemet</swp-toggle-description>
|
||||
</swp-toggle-label>
|
||||
<swp-toggle-slider data-value="yes">
|
||||
<swp-toggle-option localize="common.yes">Ja</swp-toggle-option>
|
||||
<swp-toggle-option localize="common.no">Nej</swp-toggle-option>
|
||||
</swp-toggle-slider>
|
||||
</swp-toggle-row>
|
||||
|
||||
<swp-form-row class="spaced">
|
||||
<swp-form-label localize="services.categoryDrawer.timePeriod">Skal kun være synlig i følgende tidsperiode</swp-form-label>
|
||||
<swp-date-range>
|
||||
<input type="date" id="category-start-date" class="inactive">
|
||||
<span>–</span>
|
||||
<input type="date" id="category-end-date" class="inactive">
|
||||
</swp-date-range>
|
||||
<swp-form-hint localize="services.categoryDrawer.timePeriodHint">Efterlad felterne blanke for ingen tidsbegrænsning</swp-form-hint>
|
||||
</swp-form-row>
|
||||
</swp-drawer-body>
|
||||
|
||||
<swp-drawer-footer>
|
||||
<swp-btn class="secondary" data-drawer-close localize="common.cancel">Annuller</swp-btn>
|
||||
<swp-btn class="primary" localize="services.categoryDrawer.save">Gem kategori</swp-btn>
|
||||
</swp-drawer-footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public class ServiceTableViewComponent : ViewComponent
|
|||
Key = key,
|
||||
SearchPlaceholder = _localization.Get("services.searchPlaceholder"),
|
||||
CreateButtonText = _localization.Get("services.createService"),
|
||||
CreateCategoryButtonText = _localization.Get("services.createCategory"),
|
||||
ColumnService = _localization.Get("services.table.service"),
|
||||
ColumnDuration = _localization.Get("services.table.duration"),
|
||||
ColumnPrice = _localization.Get("services.table.price"),
|
||||
|
|
@ -66,6 +67,7 @@ public class ServiceTableViewModel
|
|||
public required string Key { get; init; }
|
||||
public required string SearchPlaceholder { get; init; }
|
||||
public required string CreateButtonText { get; init; }
|
||||
public required string CreateCategoryButtonText { get; init; }
|
||||
public required string ColumnService { get; init; }
|
||||
public required string ColumnDuration { get; init; }
|
||||
public required string ColumnPrice { get; init; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue