Adds suppliers feature to application

Introduces comprehensive suppliers management with mock data, localization, and UI components

Implements:
- Suppliers page with data table
- Localization for Danish and English
- Search and filtering functionality
- Responsive table design
- Mock data for initial population
This commit is contained in:
Janus C. H. Knudsen 2026-01-24 00:13:05 +01:00
parent 7aaa475a14
commit dc2bab5702
16 changed files with 622 additions and 8 deletions

View file

@ -0,0 +1,16 @@
@model PlanTempus.Application.Features.Suppliers.Components.SupplierItemViewModel
<swp-data-table-row data-name="@Model.Name" data-contact="@Model.ContactPerson" data-city="@Model.City" data-href="/leverandoerer/@Model.Id">
<swp-data-table-cell>
<swp-supplier-cell>
<swp-supplier-name>@Model.Name</swp-supplier-name>
<swp-supplier-city>@Model.City</swp-supplier-city>
</swp-supplier-cell>
</swp-data-table-cell>
<swp-data-table-cell>@Model.ContactPerson</swp-data-table-cell>
<swp-data-table-cell>@Model.ProductCount</swp-data-table-cell>
<swp-data-table-cell>@Model.LastOrderDate</swp-data-table-cell>
<swp-data-table-cell>
<swp-status-badge class="@Model.StatusClass">@Model.StatusText</swp-status-badge>
</swp-data-table-cell>
</swp-data-table-row>

View file

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Suppliers.Components;
public class SupplierRowViewComponent : ViewComponent
{
public IViewComponentResult Invoke(SupplierItemViewModel supplier)
{
return View(supplier);
}
}

View file

@ -0,0 +1,39 @@
@model PlanTempus.Application.Features.Suppliers.Components.SupplierTableViewModel
<swp-action-bar>
<swp-search-input>
<i class="ph ph-magnifying-glass"></i>
<input type="text" id="supplierSearchInput" placeholder="@Model.SearchPlaceholder" />
</swp-search-input>
<swp-btn-group>
<swp-btn class="secondary">
<i class="ph ph-export"></i>
<span>@Model.ExportButtonText</span>
</swp-btn>
<swp-btn class="primary">
<i class="ph ph-plus"></i>
<span>@Model.CreateButtonText</span>
</swp-btn>
</swp-btn-group>
</swp-action-bar>
<swp-card class="suppliers-list">
<swp-data-table>
<swp-data-table-header>
<swp-data-table-cell>@Model.ColumnSupplier</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnContact</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnProducts</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnLastOrder</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnStatus</swp-data-table-cell>
</swp-data-table-header>
@foreach (var supplier in Model.Suppliers)
{
@await Component.InvokeAsync("SupplierRow", supplier)
}
</swp-data-table>
<swp-empty-state id="supplierEmptyState" style="display: none;">
<i class="ph ph-package"></i>
<span>@Model.EmptySearchText</span>
</swp-empty-state>
</swp-card>

View file

@ -0,0 +1,116 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Suppliers.Components;
public class SupplierTableViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
private readonly IWebHostEnvironment _env;
public SupplierTableViewComponent(ILocalizationService localization, IWebHostEnvironment env)
{
_localization = localization;
_env = env;
}
public IViewComponentResult Invoke()
{
var data = LoadSupplierData();
var model = new SupplierTableViewModel
{
SearchPlaceholder = _localization.Get("suppliers.searchPlaceholder"),
ExportButtonText = _localization.Get("suppliers.export"),
CreateButtonText = _localization.Get("suppliers.create"),
ColumnSupplier = _localization.Get("suppliers.column.supplier"),
ColumnContact = _localization.Get("suppliers.column.contact"),
ColumnProducts = _localization.Get("suppliers.column.products"),
ColumnLastOrder = _localization.Get("suppliers.column.lastOrder"),
ColumnStatus = _localization.Get("suppliers.column.status"),
EmptySearchText = _localization.Get("suppliers.emptySearch"),
Suppliers = data.Suppliers
.OrderBy(s => s.Name)
.Select(s => new SupplierItemViewModel
{
Id = s.Id,
Name = s.Name,
City = s.City,
ContactPerson = s.ContactPerson,
ProductCount = s.ProductCount,
LastOrderDate = FormatLastOrder(s.LastOrder),
IsActive = s.IsActive,
StatusClass = s.IsActive ? "active" : "inactive",
StatusText = s.IsActive
? _localization.Get("suppliers.status.active")
: _localization.Get("suppliers.status.inactive")
})
.ToList()
};
return View(model);
}
private SupplierMockData LoadSupplierData()
{
var jsonPath = Path.Combine(_env.ContentRootPath, "Features", "Suppliers", "Data", "suppliersMock.json");
var json = System.IO.File.ReadAllText(jsonPath);
return JsonSerializer.Deserialize<SupplierMockData>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new SupplierMockData();
}
private static string FormatLastOrder(string dateStr)
{
if (DateTime.TryParse(dateStr, out var date))
{
return date.ToString("d. MMMM yyyy", new CultureInfo("da-DK"));
}
return dateStr;
}
}
public class SupplierTableViewModel
{
public required string SearchPlaceholder { get; init; }
public required string ExportButtonText { get; init; }
public required string CreateButtonText { get; init; }
public required string ColumnSupplier { get; init; }
public required string ColumnContact { get; init; }
public required string ColumnProducts { get; init; }
public required string ColumnLastOrder { get; init; }
public required string ColumnStatus { get; init; }
public required string EmptySearchText { get; init; }
public required IReadOnlyList<SupplierItemViewModel> Suppliers { get; init; }
}
public class SupplierItemViewModel
{
public required string Id { get; init; }
public required string Name { get; init; }
public required string City { get; init; }
public required string ContactPerson { get; init; }
public int ProductCount { get; init; }
public required string LastOrderDate { get; init; }
public bool IsActive { get; init; }
public required string StatusClass { get; init; }
public required string StatusText { get; init; }
}
internal class SupplierMockData
{
public List<SupplierData> Suppliers { get; set; } = new();
}
internal class SupplierData
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string City { get; set; } = "";
public string ContactPerson { get; set; } = "";
public int ProductCount { get; set; }
public string LastOrder { get; set; } = "";
public bool IsActive { get; set; }
}

View file

@ -0,0 +1,112 @@
{
"suppliers": [
{
"id": "beauty-group-denmark",
"name": "Beauty Group Denmark",
"city": "København",
"contactPerson": "Lars Hansen",
"productCount": 24,
"lastOrder": "2024-12-15",
"isActive": true
},
{
"id": "salon-supplies-aps",
"name": "Salon Supplies ApS",
"city": "Aarhus",
"contactPerson": "Mette Nielsen",
"productCount": 18,
"lastOrder": "2024-12-22",
"isActive": true
},
{
"id": "pro-hair-distribution",
"name": "Pro Hair Distribution",
"city": "Odense",
"contactPerson": "Anders Sørensen",
"productCount": 32,
"lastOrder": "2024-12-10",
"isActive": true
},
{
"id": "nordic-beauty-import",
"name": "Nordic Beauty Import",
"city": "Aalborg",
"contactPerson": "Pia Kristensen",
"productCount": 15,
"lastOrder": "2024-11-28",
"isActive": true
},
{
"id": "color-world-as",
"name": "Color World A/S",
"city": "Vejle",
"contactPerson": "Thomas Berg",
"productCount": 8,
"lastOrder": "2024-12-05",
"isActive": false
},
{
"id": "tools-and-more",
"name": "Tools & More",
"city": "Roskilde",
"contactPerson": "Karen Olsen",
"productCount": 12,
"lastOrder": "2024-12-18",
"isActive": true
},
{
"id": "scandinavian-cosmetics",
"name": "Scandinavian Cosmetics",
"city": "Helsingør",
"contactPerson": "Erik Madsen",
"productCount": 45,
"lastOrder": "2024-12-20",
"isActive": true
},
{
"id": "hair-products-international",
"name": "Hair Products International",
"city": "Frederiksberg",
"contactPerson": "Sofie Andersen",
"productCount": 67,
"lastOrder": "2024-12-12",
"isActive": true
},
{
"id": "danish-beauty-supply",
"name": "Danish Beauty Supply",
"city": "Esbjerg",
"contactPerson": "Michael Petersen",
"productCount": 21,
"lastOrder": "2024-11-15",
"isActive": false
},
{
"id": "salon-essentials",
"name": "Salon Essentials",
"city": "Kolding",
"contactPerson": "Anne Marie Larsen",
"productCount": 38,
"lastOrder": "2024-12-19",
"isActive": true
},
{
"id": "nordic-hair-care",
"name": "Nordic Hair Care",
"city": "Horsens",
"contactPerson": "Christian Holm",
"productCount": 29,
"lastOrder": "2024-12-08",
"isActive": true
},
{
"id": "beauty-wholesale-dk",
"name": "Beauty Wholesale DK",
"city": "Silkeborg",
"contactPerson": "Louise Jensen",
"productCount": 53,
"lastOrder": "2024-12-21",
"isActive": true
}
]
}

View file

@ -0,0 +1,39 @@
@page "/leverandoerer"
@model PlanTempus.Application.Features.Suppliers.Pages.IndexModel
@{
ViewData["Title"] = "Leverandører";
}
<swp-sticky-header>
<swp-header-content>
<swp-page-header>
<swp-page-title>
<h1 localize="suppliers.title">Leverandører</h1>
<p localize="suppliers.subtitle">Administrer leverandører og indkøb</p>
</swp-page-title>
</swp-page-header>
<swp-stats-row class="cols-4">
<swp-stat-card class="highlight">
<swp-stat-value>12</swp-stat-value>
<swp-stat-label localize="suppliers.stats.total">Leverandører i alt</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>10</swp-stat-value>
<swp-stat-label localize="suppliers.stats.active">Aktive</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>45.230 kr</swp-stat-value>
<swp-stat-label localize="suppliers.stats.purchasesThisMonth">Indkøb denne måned</swp-stat-label>
</swp-stat-card>
<swp-stat-card class="warning">
<swp-stat-value>3</swp-stat-value>
<swp-stat-label localize="suppliers.stats.pendingOrders">Afventende ordrer</swp-stat-label>
</swp-stat-card>
</swp-stats-row>
</swp-header-content>
</swp-sticky-header>
<swp-page-container>
@await Component.InvokeAsync("SupplierTable")
</swp-page-container>

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PlanTempus.Application.Features.Suppliers.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}