Add services feature with mock data and components
Introduces comprehensive services management module with: - Dynamic service and category tables - Localization support for services section - Mock data for services and categories - Responsive UI components for services listing - Menu navigation and styling updates Enhances application's service management capabilities
This commit is contained in:
parent
408e590922
commit
4cf30e1f27
20 changed files with 951 additions and 0 deletions
|
|
@ -0,0 +1,68 @@
|
|||
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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
@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>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceCategoryViewModel
|
||||
|
||||
<swp-category-row data-category="@Model.Id" data-expanded="true">
|
||||
<swp-data-table-cell>
|
||||
<swp-category-toggle>
|
||||
<i class="ph ph-caret-down"></i>
|
||||
</swp-category-toggle>
|
||||
<span class="category-name">@Model.Name</span>
|
||||
<span class="category-count">(@Model.Services.Count)</span>
|
||||
</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-row>
|
||||
|
||||
@foreach (var service in Model.Services)
|
||||
{
|
||||
@await Component.InvokeAsync("ServiceRow", service)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceCategoryGroupViewComponent : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(ServiceCategoryViewModel category)
|
||||
{
|
||||
return View(category);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceItemViewModel
|
||||
|
||||
<swp-data-table-row data-service-detail="@Model.Id">
|
||||
<swp-data-table-cell>@Model.Name</swp-data-table-cell>
|
||||
<swp-data-table-cell>@Model.Duration min</swp-data-table-cell>
|
||||
<swp-data-table-cell>@Model.Price.ToString("N0") kr</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>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceRowViewComponent : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(ServiceItemViewModel service)
|
||||
{
|
||||
return View(service);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceStatCardViewModel
|
||||
|
||||
<swp-stat-card data-key="@Model.Key" class="@Model.Variant">
|
||||
<swp-stat-value>@Model.Value</swp-stat-value>
|
||||
<swp-stat-label>@Model.Label</swp-stat-label>
|
||||
</swp-stat-card>
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceStatCardViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
public ServiceStatCardViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string key)
|
||||
{
|
||||
var model = ServiceStatCardCatalog.Get(key, _localization);
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceStatCardViewModel
|
||||
{
|
||||
public required string Key { get; init; }
|
||||
public required string Value { get; init; }
|
||||
public required string Label { get; init; }
|
||||
public string? Variant { get; init; }
|
||||
}
|
||||
|
||||
internal class ServiceStatCardData
|
||||
{
|
||||
public required string Key { get; init; }
|
||||
public required string Value { get; init; }
|
||||
public required string LabelKey { get; init; }
|
||||
public string? Variant { get; init; }
|
||||
}
|
||||
|
||||
public static class ServiceStatCardCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, ServiceStatCardData> Cards = new()
|
||||
{
|
||||
["total-services"] = new ServiceStatCardData
|
||||
{
|
||||
Key = "total-services",
|
||||
Value = "78",
|
||||
LabelKey = "services.stats.totalServices",
|
||||
Variant = "teal"
|
||||
},
|
||||
["active-categories"] = new ServiceStatCardData
|
||||
{
|
||||
Key = "active-categories",
|
||||
Value = "14",
|
||||
LabelKey = "services.stats.activeCategories",
|
||||
Variant = "purple"
|
||||
},
|
||||
["average-price"] = new ServiceStatCardData
|
||||
{
|
||||
Key = "average-price",
|
||||
Value = "856 kr",
|
||||
LabelKey = "services.stats.averagePrice",
|
||||
Variant = "amber"
|
||||
}
|
||||
};
|
||||
|
||||
public static ServiceStatCardViewModel Get(string key, ILocalizationService localization)
|
||||
{
|
||||
if (!Cards.TryGetValue(key, out var card))
|
||||
throw new KeyNotFoundException($"ServiceStatCard with key '{key}' not found");
|
||||
|
||||
return new ServiceStatCardViewModel
|
||||
{
|
||||
Key = card.Key,
|
||||
Value = card.Value,
|
||||
Label = localization.Get(card.LabelKey),
|
||||
Variant = card.Variant
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<string> AllKeys => Cards.Keys;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
@model PlanTempus.Application.Features.Services.Components.ServiceTableViewModel
|
||||
|
||||
<swp-services-header>
|
||||
<swp-search-input>
|
||||
<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-services-header>
|
||||
|
||||
<swp-card class="services-list">
|
||||
<swp-data-table>
|
||||
<swp-data-table-header>
|
||||
<swp-data-table-cell>@Model.ColumnService</swp-data-table-cell>
|
||||
<swp-data-table-cell>@Model.ColumnDuration</swp-data-table-cell>
|
||||
<swp-data-table-cell>@Model.ColumnPrice</swp-data-table-cell>
|
||||
<swp-data-table-cell></swp-data-table-cell>
|
||||
</swp-data-table-header>
|
||||
@foreach (var category in Model.Categories)
|
||||
{
|
||||
@await Component.InvokeAsync("ServiceCategoryGroup", category)
|
||||
}
|
||||
</swp-data-table>
|
||||
</swp-card>
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Components;
|
||||
|
||||
public class ServiceTableViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
|
||||
public ServiceTableViewComponent(ILocalizationService localization, IWebHostEnvironment env)
|
||||
{
|
||||
_localization = localization;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string key)
|
||||
{
|
||||
var data = LoadServiceData();
|
||||
var model = new ServiceTableViewModel
|
||||
{
|
||||
Key = key,
|
||||
SearchPlaceholder = _localization.Get("services.searchPlaceholder"),
|
||||
CreateButtonText = _localization.Get("services.createService"),
|
||||
ColumnService = _localization.Get("services.table.service"),
|
||||
ColumnDuration = _localization.Get("services.table.duration"),
|
||||
ColumnPrice = _localization.Get("services.table.price"),
|
||||
Categories = data.Categories
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Select(c => new ServiceCategoryViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.Name,
|
||||
Services = data.Services
|
||||
.Where(s => s.CategoryId == c.Id)
|
||||
.Select(s => new ServiceItemViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = s.Name,
|
||||
Duration = s.Duration,
|
||||
Price = s.Price
|
||||
})
|
||||
.ToList()
|
||||
})
|
||||
.Where(c => c.Services.Any())
|
||||
.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 ServiceTableViewModel
|
||||
{
|
||||
public required string Key { get; init; }
|
||||
public required string SearchPlaceholder { get; init; }
|
||||
public required string CreateButtonText { get; init; }
|
||||
public required string ColumnService { get; init; }
|
||||
public required string ColumnDuration { get; init; }
|
||||
public required string ColumnPrice { get; init; }
|
||||
public required IReadOnlyList<ServiceCategoryViewModel> Categories { get; init; }
|
||||
}
|
||||
|
||||
public class ServiceCategoryViewModel
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required IReadOnlyList<ServiceItemViewModel> Services { get; init; }
|
||||
}
|
||||
|
||||
public class ServiceItemViewModel
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public int Duration { get; init; }
|
||||
public decimal Price { get; init; }
|
||||
}
|
||||
|
||||
internal class ServiceMockData
|
||||
{
|
||||
public List<ServiceCategoryData> Categories { get; set; } = new();
|
||||
public List<ServiceData> Services { get; set; } = new();
|
||||
}
|
||||
|
||||
internal class ServiceCategoryData
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
internal class ServiceData
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string CategoryId { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int Duration { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
108
PlanTempus.Application/Features/Services/Data/servicesMock.json
Normal file
108
PlanTempus.Application/Features/Services/Data/servicesMock.json
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
{
|
||||
"categories": [
|
||||
{ "id": "cat-1", "name": "Klip dame, herre og børn", "sortOrder": 1 },
|
||||
{ "id": "cat-2", "name": "Farvebehandlinger", "sortOrder": 2 },
|
||||
{ "id": "cat-3", "name": "Striber/Refleksbehandling", "sortOrder": 3 },
|
||||
{ "id": "cat-4", "name": "Hårvask med styling eller uden styling", "sortOrder": 4 },
|
||||
{ "id": "cat-5", "name": "Henna naturlig hårfarver", "sortOrder": 5 },
|
||||
{ "id": "cat-6", "name": "Kurbehandling", "sortOrder": 6 },
|
||||
{ "id": "cat-7", "name": "Bryn og vipper", "sortOrder": 7 },
|
||||
{ "id": "cat-8", "name": "Balayage", "sortOrder": 8 },
|
||||
{ "id": "cat-9", "name": "Skæg", "sortOrder": 9 },
|
||||
{ "id": "cat-10", "name": "Gloss", "sortOrder": 10 },
|
||||
{ "id": "cat-11", "name": "Håropsætning", "sortOrder": 11 },
|
||||
{ "id": "cat-12", "name": "Modeller", "sortOrder": 12 },
|
||||
{ "id": "cat-13", "name": "Tristan farve modeller", "sortOrder": 13 },
|
||||
{ "id": "cat-14", "name": "Tilvalg services", "sortOrder": 14 }
|
||||
],
|
||||
"services": [
|
||||
{ "id": "svc-1", "categoryId": "cat-1", "name": "Dameklip", "duration": 60, "price": 725 },
|
||||
{ "id": "svc-2", "categoryId": "cat-1", "name": "Dameklip uden snak", "duration": 60, "price": 725 },
|
||||
{ "id": "svc-3", "categoryId": "cat-1", "name": "Dameklip spidser mellemlangt og langt hår", "duration": 40, "price": 575 },
|
||||
{ "id": "svc-4", "categoryId": "cat-1", "name": "Dameklip Luksus ekstra glans og pleje", "duration": 75, "price": 925 },
|
||||
{ "id": "svc-5", "categoryId": "cat-1", "name": "Herreklip", "duration": 60, "price": 645 },
|
||||
{ "id": "svc-6", "categoryId": "cat-1", "name": "Herreklip uden snak", "duration": 60, "price": 645 },
|
||||
{ "id": "svc-7", "categoryId": "cat-1", "name": "Skin fade", "duration": 60, "price": 645 },
|
||||
{ "id": "svc-8", "categoryId": "cat-1", "name": "Klip med maskine (herre klip)", "duration": 30, "price": 475 },
|
||||
{ "id": "svc-9", "categoryId": "cat-1", "name": "Børneklip 0-4 år", "duration": 45, "price": 475 },
|
||||
{ "id": "svc-10", "categoryId": "cat-1", "name": "Børneklip 4-8 år", "duration": 45, "price": 525 },
|
||||
{ "id": "svc-11", "categoryId": "cat-1", "name": "Børneklip 9-12 år", "duration": 60, "price": 450 },
|
||||
{ "id": "svc-12", "categoryId": "cat-1", "name": "Touch up Dame", "duration": 10, "price": 0 },
|
||||
{ "id": "svc-13", "categoryId": "cat-1", "name": "Touch up Herre", "duration": 10, "price": 0 },
|
||||
{ "id": "svc-14", "categoryId": "cat-1", "name": "Pandehår helt nyt", "duration": 20, "price": 325 },
|
||||
{ "id": "svc-15", "categoryId": "cat-1", "name": "Konsultation uden behandling", "duration": 10, "price": 0 },
|
||||
|
||||
{ "id": "svc-20", "categoryId": "cat-2", "name": "Bundfarve almindelig udgroning maks 3 cm", "duration": 90, "price": 785 },
|
||||
{ "id": "svc-21", "categoryId": "cat-2", "name": "Helfarve kort hår", "duration": 105, "price": 950 },
|
||||
{ "id": "svc-22", "categoryId": "cat-2", "name": "Helfarve mellemlangt hår", "duration": 120, "price": 1450 },
|
||||
{ "id": "svc-23", "categoryId": "cat-2", "name": "Helfarve langt hår", "duration": 120, "price": 1550 },
|
||||
{ "id": "svc-24", "categoryId": "cat-2", "name": "Bundfarve/Lysning", "duration": 105, "price": 975 },
|
||||
{ "id": "svc-25", "categoryId": "cat-2", "name": "Afblegning kort hår + gloss", "duration": 150, "price": 1895 },
|
||||
|
||||
{ "id": "svc-30", "categoryId": "cat-3", "name": "Striber kort hår", "duration": 120, "price": 1465 },
|
||||
{ "id": "svc-31", "categoryId": "cat-3", "name": "Striber mellemlangt hår", "duration": 150, "price": 1665 },
|
||||
{ "id": "svc-32", "categoryId": "cat-3", "name": "Striber langt hår", "duration": 180, "price": 1865 },
|
||||
{ "id": "svc-33", "categoryId": "cat-3", "name": "Striber på toppen/overhår", "duration": 90, "price": 1065 },
|
||||
{ "id": "svc-34", "categoryId": "cat-3", "name": "Striber babylights tæt lysning mellemlangt hår", "duration": 180, "price": 2650 },
|
||||
{ "id": "svc-35", "categoryId": "cat-3", "name": "Striber babylights tæt lysning langt hår", "duration": 180, "price": 2850 },
|
||||
{ "id": "svc-36", "categoryId": "cat-3", "name": "Striber babylights tæt lysning på toppen", "duration": 120, "price": 1650 },
|
||||
{ "id": "svc-37", "categoryId": "cat-3", "name": "AirTouch skulderlangt hår", "duration": 210, "price": 3250 },
|
||||
{ "id": "svc-38", "categoryId": "cat-3", "name": "AirTouch langt hår", "duration": 240, "price": 3850 },
|
||||
|
||||
{ "id": "svc-40", "categoryId": "cat-4", "name": "Hårvask uden styling", "duration": 30, "price": 265 },
|
||||
{ "id": "svc-41", "categoryId": "cat-4", "name": "Hårvask med styling (kun føn)", "duration": 40, "price": 450 },
|
||||
{ "id": "svc-42", "categoryId": "cat-4", "name": "Vask + Styling med varme glatning/krøller (mellemlangt/langt)", "duration": 60, "price": 650 },
|
||||
|
||||
{ "id": "svc-50", "categoryId": "cat-5", "name": "Henna kort hår", "duration": 90, "price": 965 },
|
||||
{ "id": "svc-51", "categoryId": "cat-5", "name": "Henna mellemlangt/langt hår", "duration": 90, "price": 1265 },
|
||||
{ "id": "svc-52", "categoryId": "cat-5", "name": "Henna bundfarve", "duration": 90, "price": 750 },
|
||||
|
||||
{ "id": "svc-60", "categoryId": "cat-6", "name": "Olaplex Stand alone", "duration": 60, "price": 550 },
|
||||
{ "id": "svc-61", "categoryId": "cat-6", "name": "Kurbehandling fugt/protein", "duration": 40, "price": 365 },
|
||||
|
||||
{ "id": "svc-70", "categoryId": "cat-7", "name": "Farvning vipper & bryn", "duration": 30, "price": 345 },
|
||||
{ "id": "svc-71", "categoryId": "cat-7", "name": "Farvning vipper", "duration": 20, "price": 185 },
|
||||
{ "id": "svc-72", "categoryId": "cat-7", "name": "Farvning og retning af bryn", "duration": 20, "price": 185 },
|
||||
{ "id": "svc-73", "categoryId": "cat-7", "name": "Retning af bryn", "duration": 10, "price": 100 },
|
||||
|
||||
{ "id": "svc-80", "categoryId": "cat-8", "name": "Balayage maks til skulderen", "duration": 150, "price": 1850 },
|
||||
{ "id": "svc-81", "categoryId": "cat-8", "name": "Balayage maks skulder + gloss/toning", "duration": 180, "price": 2250 },
|
||||
{ "id": "svc-82", "categoryId": "cat-8", "name": "Balayage langt hår", "duration": 150, "price": 2150 },
|
||||
{ "id": "svc-83", "categoryId": "cat-8", "name": "Balayage langt hår + gloss/toning", "duration": 180, "price": 2550 },
|
||||
|
||||
{ "id": "svc-90", "categoryId": "cat-9", "name": "Skægtrim", "duration": 20, "price": 300 },
|
||||
|
||||
{ "id": "svc-100", "categoryId": "cat-10", "name": "Gloss ekstra langt/tykt hår", "duration": 75, "price": 900 },
|
||||
{ "id": "svc-101", "categoryId": "cat-10", "name": "Glossing kort hår", "duration": 60, "price": 685 },
|
||||
{ "id": "svc-102", "categoryId": "cat-10", "name": "Glossing mellemlangt/langt hår", "duration": 60, "price": 745 },
|
||||
{ "id": "svc-103", "categoryId": "cat-10", "name": "Glossing mænd", "duration": 40, "price": 350 },
|
||||
{ "id": "svc-104", "categoryId": "cat-10", "name": "Gloss ifb. anden farvebehandling", "duration": 20, "price": 450 },
|
||||
|
||||
{ "id": "svc-110", "categoryId": "cat-11", "name": "Håropsætning kort hår", "duration": 60, "price": 850 },
|
||||
{ "id": "svc-111", "categoryId": "cat-11", "name": "Håropsætning langt hår", "duration": 60, "price": 1450 },
|
||||
{ "id": "svc-112", "categoryId": "cat-11", "name": "Håropsætning Brud/brudepiger/Galla/Oscar", "duration": 90, "price": 1599 },
|
||||
{ "id": "svc-113", "categoryId": "cat-11", "name": "Make-up Special Brud/Galla mm", "duration": 90, "price": 3000 },
|
||||
|
||||
{ "id": "svc-120", "categoryId": "cat-12", "name": "Dameklip Model", "duration": 60, "price": 0 },
|
||||
{ "id": "svc-121", "categoryId": "cat-12", "name": "Herreklip Model", "duration": 60, "price": 0 },
|
||||
{ "id": "svc-122", "categoryId": "cat-12", "name": "Balayage Model", "duration": 240, "price": 0 },
|
||||
{ "id": "svc-123", "categoryId": "cat-12", "name": "Striber Model", "duration": 180, "price": 0 },
|
||||
{ "id": "svc-124", "categoryId": "cat-12", "name": "Bryn & Vippe Model", "duration": 40, "price": 0 },
|
||||
{ "id": "svc-125", "categoryId": "cat-12", "name": "Bundfarve Model", "duration": 120, "price": 0 },
|
||||
{ "id": "svc-126", "categoryId": "cat-12", "name": "Gloss Model", "duration": 30, "price": 0 },
|
||||
|
||||
{ "id": "svc-130", "categoryId": "cat-13", "name": "Bundfarve med HP/HPF", "duration": 90, "price": 325 },
|
||||
{ "id": "svc-131", "categoryId": "cat-13", "name": "Striber Model", "duration": 240, "price": 400 },
|
||||
|
||||
{ "id": "svc-140", "categoryId": "cat-14", "name": "Touch up kur", "duration": 15, "price": 175 },
|
||||
{ "id": "svc-141", "categoryId": "cat-14", "name": "Root shading", "duration": 30, "price": 425 },
|
||||
{ "id": "svc-142", "categoryId": "cat-14", "name": "Styling med varme (efter behandling)", "duration": 60, "price": 475 },
|
||||
{ "id": "svc-143", "categoryId": "cat-14", "name": "Styling kort hår (efter farve)", "duration": 20, "price": 175 },
|
||||
{ "id": "svc-144", "categoryId": "cat-14", "name": "Olaplex efter afblegning", "duration": 10, "price": 325 },
|
||||
{ "id": "svc-145", "categoryId": "cat-14", "name": "Let afrensning af gloss/klor/kemi", "duration": 20, "price": 220 },
|
||||
{ "id": "svc-146", "categoryId": "cat-14", "name": "Forpigmentering", "duration": 20, "price": 300 },
|
||||
{ "id": "svc-147", "categoryId": "cat-14", "name": "Knække bund ifb. farvebehandling", "duration": 20, "price": 400 },
|
||||
{ "id": "svc-148", "categoryId": "cat-14", "name": "Olaplex i farve", "duration": 10, "price": 230 },
|
||||
{ "id": "svc-149", "categoryId": "cat-14", "name": "Metal DX intens kur redken gloss", "duration": 20, "price": 225 }
|
||||
]
|
||||
}
|
||||
50
PlanTempus.Application/Features/Services/Pages/Index.cshtml
Normal file
50
PlanTempus.Application/Features/Services/Pages/Index.cshtml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
@page "/services"
|
||||
@using PlanTempus.Application.Features.Services.Components
|
||||
@model PlanTempus.Application.Features.Services.Pages.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "Services";
|
||||
}
|
||||
|
||||
<swp-services-list-view id="services-list-view">
|
||||
<swp-sticky-header>
|
||||
<swp-header-content>
|
||||
<swp-page-header>
|
||||
<swp-page-title>
|
||||
<h1 localize="services.title">Services</h1>
|
||||
<p localize="services.subtitle">Administrer services og priser</p>
|
||||
</swp-page-title>
|
||||
</swp-page-header>
|
||||
|
||||
<swp-stats-row>
|
||||
@await Component.InvokeAsync("ServiceStatCard", "total-services")
|
||||
@await Component.InvokeAsync("ServiceStatCard", "active-categories")
|
||||
@await Component.InvokeAsync("ServiceStatCard", "average-price")
|
||||
</swp-stats-row>
|
||||
</swp-header-content>
|
||||
|
||||
<swp-tab-bar>
|
||||
<swp-tab class="active" data-tab="services">
|
||||
<i class="ph ph-scissors"></i>
|
||||
<span localize="services.tabs.services">Services</span>
|
||||
</swp-tab>
|
||||
<swp-tab data-tab="categories">
|
||||
<i class="ph ph-folders"></i>
|
||||
<span localize="services.tabs.categories">Kategorier</span>
|
||||
</swp-tab>
|
||||
</swp-tab-bar>
|
||||
</swp-sticky-header>
|
||||
|
||||
<!-- Tab: Services -->
|
||||
<swp-tab-content data-tab="services" class="active">
|
||||
<swp-page-container>
|
||||
@await Component.InvokeAsync("ServiceTable", "all-services")
|
||||
</swp-page-container>
|
||||
</swp-tab-content>
|
||||
|
||||
<!-- Tab: Categories -->
|
||||
<swp-tab-content data-tab="categories">
|
||||
<swp-page-container>
|
||||
@await Component.InvokeAsync("CategoryTable", "all-categories")
|
||||
</swp-page-container>
|
||||
</swp-tab-content>
|
||||
</swp-services-list-view>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace PlanTempus.Application.Features.Services.Pages;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue