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:
parent
7aaa475a14
commit
dc2bab5702
16 changed files with 622 additions and 8 deletions
|
|
@ -64,7 +64,7 @@
|
||||||
<swp-row-detail-actions>
|
<swp-row-detail-actions>
|
||||||
<swp-btn class="secondary" data-zreport="043">
|
<swp-btn class="secondary" data-zreport="043">
|
||||||
<i class="ph ph-file-pdf"></i>
|
<i class="ph ph-file-pdf"></i>
|
||||||
<span localize="cash.table.downloadPdf">Download PDF</span>
|
<span localize="cash.table.downloadPdf">Download Z-Rapport</span>
|
||||||
</swp-btn>
|
</swp-btn>
|
||||||
<swp-btn class="primary">
|
<swp-btn class="primary">
|
||||||
<i class="ph ph-list-bullets"></i>
|
<i class="ph ph-list-bullets"></i>
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
<swp-row-detail-actions>
|
<swp-row-detail-actions>
|
||||||
<swp-btn class="secondary" data-zreport="042">
|
<swp-btn class="secondary" data-zreport="042">
|
||||||
<i class="ph ph-file-pdf"></i>
|
<i class="ph ph-file-pdf"></i>
|
||||||
<span localize="cash.table.downloadPdf">Download PDF</span>
|
<span localize="cash.table.downloadPdf">Download Z-Rapport</span>
|
||||||
</swp-btn>
|
</swp-btn>
|
||||||
<swp-btn class="primary">
|
<swp-btn class="primary">
|
||||||
<i class="ph ph-list-bullets"></i>
|
<i class="ph ph-list-bullets"></i>
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
<swp-row-detail-actions>
|
<swp-row-detail-actions>
|
||||||
<swp-btn class="secondary" data-zreport="041">
|
<swp-btn class="secondary" data-zreport="041">
|
||||||
<i class="ph ph-file-pdf"></i>
|
<i class="ph ph-file-pdf"></i>
|
||||||
<span localize="cash.table.downloadPdf">Download PDF</span>
|
<span localize="cash.table.downloadPdf">Download Z-Rapport</span>
|
||||||
</swp-btn>
|
</swp-btn>
|
||||||
<swp-btn class="primary">
|
<swp-btn class="primary">
|
||||||
<i class="ph ph-list-bullets"></i>
|
<i class="ph ph-list-bullets"></i>
|
||||||
|
|
@ -157,7 +157,7 @@
|
||||||
<swp-row-detail-actions>
|
<swp-row-detail-actions>
|
||||||
<swp-btn class="secondary" data-zreport="040">
|
<swp-btn class="secondary" data-zreport="040">
|
||||||
<i class="ph ph-file-pdf"></i>
|
<i class="ph ph-file-pdf"></i>
|
||||||
<span localize="cash.table.downloadPdf">Download PDF</span>
|
<span localize="cash.table.downloadPdf">Download Z_Rapport</span>
|
||||||
</swp-btn>
|
</swp-btn>
|
||||||
<swp-btn class="primary">
|
<swp-btn class="primary">
|
||||||
<i class="ph ph-list-bullets"></i>
|
<i class="ph ph-list-bullets"></i>
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
"showingCount": "Viser {count} afstemninger",
|
"showingCount": "Viser {count} afstemninger",
|
||||||
"exportSaft": "Eksporter SAF-T",
|
"exportSaft": "Eksporter SAF-T",
|
||||||
"downloadCsv": "Download CSV",
|
"downloadCsv": "Download CSV",
|
||||||
"downloadPdf": "Download PDF",
|
"downloadPdf": "Download Z-Rapport",
|
||||||
"viewTransactions": "Se transaktioner"
|
"viewTransactions": "Se transaktioner"
|
||||||
},
|
},
|
||||||
"revenue": {
|
"revenue": {
|
||||||
|
|
@ -575,6 +575,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"suppliers": {
|
||||||
|
"title": "Leverandører",
|
||||||
|
"subtitle": "Administrer leverandører og indkøb",
|
||||||
|
"searchPlaceholder": "Søg leverandør, kontaktperson...",
|
||||||
|
"export": "Eksporter",
|
||||||
|
"create": "Ny leverandør",
|
||||||
|
"emptySearch": "Ingen leverandører matcher din søgning",
|
||||||
|
"stats": {
|
||||||
|
"total": "Leverandører i alt",
|
||||||
|
"active": "Aktive",
|
||||||
|
"purchasesThisMonth": "Indkøb denne måned",
|
||||||
|
"pendingOrders": "Afventende ordrer"
|
||||||
|
},
|
||||||
|
"column": {
|
||||||
|
"supplier": "Leverandør",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"products": "Produkter",
|
||||||
|
"lastOrder": "Sidste ordre",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"active": "Aktiv",
|
||||||
|
"inactive": "Inaktiv"
|
||||||
|
}
|
||||||
|
},
|
||||||
"customers": {
|
"customers": {
|
||||||
"title": "Kunder",
|
"title": "Kunder",
|
||||||
"subtitle": "Administrer kunder og kundekort",
|
"subtitle": "Administrer kunder og kundekort",
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
"showingCount": "Showing {count} reconciliations",
|
"showingCount": "Showing {count} reconciliations",
|
||||||
"exportSaft": "Export SAF-T",
|
"exportSaft": "Export SAF-T",
|
||||||
"downloadCsv": "Download CSV",
|
"downloadCsv": "Download CSV",
|
||||||
"downloadPdf": "Download PDF",
|
"downloadPdf": "Download Z-Report",
|
||||||
"viewTransactions": "View transactions"
|
"viewTransactions": "View transactions"
|
||||||
},
|
},
|
||||||
"revenue": {
|
"revenue": {
|
||||||
|
|
@ -575,6 +575,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"suppliers": {
|
||||||
|
"title": "Suppliers",
|
||||||
|
"subtitle": "Manage suppliers and purchases",
|
||||||
|
"searchPlaceholder": "Search supplier, contact person...",
|
||||||
|
"export": "Export",
|
||||||
|
"create": "New supplier",
|
||||||
|
"emptySearch": "No suppliers match your search",
|
||||||
|
"stats": {
|
||||||
|
"total": "Total suppliers",
|
||||||
|
"active": "Active",
|
||||||
|
"purchasesThisMonth": "Purchases this month",
|
||||||
|
"pendingOrders": "Pending orders"
|
||||||
|
},
|
||||||
|
"column": {
|
||||||
|
"supplier": "Supplier",
|
||||||
|
"contact": "Contact",
|
||||||
|
"products": "Products",
|
||||||
|
"lastOrder": "Last order",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive"
|
||||||
|
}
|
||||||
|
},
|
||||||
"customers": {
|
"customers": {
|
||||||
"detail": {
|
"detail": {
|
||||||
"tabs": {
|
"tabs": {
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ public class MockMenuService : IMenuService
|
||||||
Id = "suppliers",
|
Id = "suppliers",
|
||||||
Label = "Leverandører",
|
Label = "Leverandører",
|
||||||
Icon = "ph-truck",
|
Icon = "ph-truck",
|
||||||
Url = "/poc-leverandoerer.html",
|
Url = "/leverandoerer",
|
||||||
MinimumRole = UserRole.Manager,
|
MinimumRole = UserRole.Manager,
|
||||||
SortOrder = 2
|
SortOrder = 2
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
39
PlanTempus.Application/Features/Suppliers/Pages/Index.cshtml
Normal file
39
PlanTempus.Application/Features/Suppliers/Pages/Index.cshtml
Normal 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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace PlanTempus.Application.Features.Suppliers.Pages;
|
||||||
|
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
<link rel="stylesheet" href="~/css/employees.css">
|
<link rel="stylesheet" href="~/css/employees.css">
|
||||||
<link rel="stylesheet" href="~/css/services.css">
|
<link rel="stylesheet" href="~/css/services.css">
|
||||||
<link rel="stylesheet" href="~/css/customers.css">
|
<link rel="stylesheet" href="~/css/customers.css">
|
||||||
|
<link rel="stylesheet" href="~/css/suppliers.css">
|
||||||
<link rel="stylesheet" href="~/css/settings.css">
|
<link rel="stylesheet" href="~/css/settings.css">
|
||||||
<link rel="stylesheet" href="~/css/reports.css">
|
<link rel="stylesheet" href="~/css/reports.css">
|
||||||
@await RenderSectionAsync("Styles", required: false)
|
@await RenderSectionAsync("Styles", required: false)
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,8 @@ swp-status-badge {
|
||||||
color: var(--color-blue);
|
color: var(--color-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.employee {
|
&.employee,
|
||||||
|
&.inactive {
|
||||||
background: var(--color-background-alt);
|
background: var(--color-background-alt);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
PlanTempus.Application/wwwroot/css/suppliers.css
Normal file
70
PlanTempus.Application/wwwroot/css/suppliers.css
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* Suppliers - Page Styling
|
||||||
|
*
|
||||||
|
* Feature-specific styling only.
|
||||||
|
* Reuses:
|
||||||
|
* - swp-sticky-header, swp-header-content, swp-page-container (page.css)
|
||||||
|
* - swp-stats-row.cols-4, swp-stat-card (stats.css)
|
||||||
|
* - swp-action-bar, swp-search-input (components.css)
|
||||||
|
* - swp-data-table, swp-status-badge, swp-empty-state (components.css)
|
||||||
|
* - swp-btn (components.css)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
SUPPLIER TABLE (uses swp-data-table from components.css)
|
||||||
|
=========================================== */
|
||||||
|
swp-card.suppliers-list {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table columns: Leverandør(1fr) | Kontakt(150px) | Produkter(80px) | Sidste(120px) | Status(100px) */
|
||||||
|
swp-card.suppliers-list swp-data-table {
|
||||||
|
grid-template-columns: minmax(200px, 1fr) 150px 80px 120px 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card.suppliers-list swp-data-table-header,
|
||||||
|
swp-card.suppliers-list swp-data-table-row {
|
||||||
|
padding: 0 var(--spacing-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card.suppliers-list swp-data-table-header swp-data-table-cell {
|
||||||
|
padding-top: var(--spacing-5);
|
||||||
|
padding-bottom: var(--spacing-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card.suppliers-list swp-data-table-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-card.suppliers-list swp-data-table-cell {
|
||||||
|
padding: var(--spacing-5) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Supplier cell (name + city) */
|
||||||
|
swp-supplier-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-supplier-name {
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-supplier-city {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Products column (center + mono) */
|
||||||
|
swp-card.suppliers-list swp-data-table-row swp-data-table-cell:nth-child(3) {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Last order column (muted) */
|
||||||
|
swp-card.suppliers-list swp-data-table-row swp-data-table-cell:nth-child(4) {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import { EmployeesController } from './modules/employees';
|
||||||
import { ControlsController } from './modules/controls';
|
import { ControlsController } from './modules/controls';
|
||||||
import { ServicesController } from './modules/services';
|
import { ServicesController } from './modules/services';
|
||||||
import { CustomersController } from './modules/customers';
|
import { CustomersController } from './modules/customers';
|
||||||
|
import { SuppliersController } from './modules/suppliers';
|
||||||
import { TrackingController } from './modules/tracking';
|
import { TrackingController } from './modules/tracking';
|
||||||
import { ReportsController } from './modules/reports';
|
import { ReportsController } from './modules/reports';
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ export class App {
|
||||||
readonly controls: ControlsController;
|
readonly controls: ControlsController;
|
||||||
readonly services: ServicesController;
|
readonly services: ServicesController;
|
||||||
readonly customers: CustomersController;
|
readonly customers: CustomersController;
|
||||||
|
readonly suppliers: SuppliersController;
|
||||||
readonly tracking: TrackingController;
|
readonly tracking: TrackingController;
|
||||||
readonly reports: ReportsController;
|
readonly reports: ReportsController;
|
||||||
|
|
||||||
|
|
@ -46,6 +48,7 @@ export class App {
|
||||||
this.controls = new ControlsController();
|
this.controls = new ControlsController();
|
||||||
this.services = new ServicesController();
|
this.services = new ServicesController();
|
||||||
this.customers = new CustomersController();
|
this.customers = new CustomersController();
|
||||||
|
this.suppliers = new SuppliersController();
|
||||||
this.tracking = new TrackingController();
|
this.tracking = new TrackingController();
|
||||||
this.reports = new ReportsController();
|
this.reports = new ReportsController();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
146
PlanTempus.Application/wwwroot/ts/modules/suppliers.ts
Normal file
146
PlanTempus.Application/wwwroot/ts/modules/suppliers.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
/**
|
||||||
|
* Suppliers Controller
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Fuzzy search with Fuse.js
|
||||||
|
* - Row click navigation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
interface SupplierItem {
|
||||||
|
name: string;
|
||||||
|
contact: string;
|
||||||
|
city: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuppliersController {
|
||||||
|
private fuse: Fuse<SupplierItem> | null = null;
|
||||||
|
private suppliers: SupplierItem[] = [];
|
||||||
|
private searchInput: HTMLInputElement | null = null;
|
||||||
|
private emptyState: HTMLElement | null = null;
|
||||||
|
private dataTable: HTMLElement | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Only initialize if we're on the suppliers page
|
||||||
|
const suppliersTable = document.querySelector('swp-card.suppliers-list');
|
||||||
|
if (!suppliersTable) return;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(): void {
|
||||||
|
this.searchInput = document.getElementById('supplierSearchInput') as HTMLInputElement;
|
||||||
|
this.emptyState = document.getElementById('supplierEmptyState');
|
||||||
|
this.dataTable = document.querySelector('swp-card.suppliers-list swp-data-table');
|
||||||
|
|
||||||
|
this.buildSupplierIndex();
|
||||||
|
this.setupSearch();
|
||||||
|
this.setupRowNavigation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSupplierIndex(): void {
|
||||||
|
const supplierRows = document.querySelectorAll('swp-card.suppliers-list swp-data-table-row');
|
||||||
|
|
||||||
|
supplierRows.forEach((row) => {
|
||||||
|
const element = row as HTMLElement;
|
||||||
|
|
||||||
|
const name = element.dataset.name || '';
|
||||||
|
const contact = element.dataset.contact || '';
|
||||||
|
const city = element.dataset.city || '';
|
||||||
|
|
||||||
|
this.suppliers.push({
|
||||||
|
name,
|
||||||
|
contact,
|
||||||
|
city,
|
||||||
|
element
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSearch(): void {
|
||||||
|
if (!this.searchInput) return;
|
||||||
|
|
||||||
|
// Initialize Fuse.js with multiple search keys
|
||||||
|
this.fuse = new Fuse(this.suppliers, {
|
||||||
|
keys: ['name', 'contact', 'city'],
|
||||||
|
threshold: 0.3,
|
||||||
|
minMatchCharLength: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for input with debounce
|
||||||
|
let debounceTimer: number;
|
||||||
|
this.searchInput.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = window.setTimeout(() => {
|
||||||
|
const query = (e.target as HTMLInputElement).value.trim();
|
||||||
|
this.filterSuppliers(query);
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterSuppliers(query: string): void {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
this.showAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.fuse) return;
|
||||||
|
|
||||||
|
// Get matching suppliers
|
||||||
|
const results = this.fuse.search(query);
|
||||||
|
const matchingSuppliers = new Set(results.map(r => r.item.element));
|
||||||
|
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
|
// Show/hide suppliers
|
||||||
|
this.suppliers.forEach(supplier => {
|
||||||
|
if (matchingSuppliers.has(supplier.element)) {
|
||||||
|
supplier.element.style.display = 'grid';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
supplier.element.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide empty state
|
||||||
|
this.updateEmptyState(visibleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showAll(): void {
|
||||||
|
this.suppliers.forEach(supplier => {
|
||||||
|
supplier.element.style.display = 'grid';
|
||||||
|
});
|
||||||
|
this.updateEmptyState(this.suppliers.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEmptyState(visibleCount: number): void {
|
||||||
|
if (!this.emptyState || !this.dataTable) return;
|
||||||
|
|
||||||
|
if (visibleCount === 0) {
|
||||||
|
this.emptyState.style.display = 'flex';
|
||||||
|
// Hide header when no results
|
||||||
|
const header = this.dataTable.querySelector('swp-data-table-header') as HTMLElement;
|
||||||
|
if (header) header.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
this.emptyState.style.display = 'none';
|
||||||
|
// Show header when results exist
|
||||||
|
const header = this.dataTable.querySelector('swp-data-table-header') as HTMLElement;
|
||||||
|
if (header) header.style.display = 'grid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupRowNavigation(): void {
|
||||||
|
// Click on rows navigates to supplier detail page
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const row = (e.target as HTMLElement).closest<HTMLElement>('swp-card.suppliers-list swp-data-table-row[data-href]');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
const href = row.dataset.href;
|
||||||
|
if (href) {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue