a0bdd2b5b4
Replace all corruption variants with HTML entities across 226 view files: - 3-char UTF-8-as-Win1252 sequences (ae-corruption) - Standalone smart/curly quotes that break C# Razor expressions - Partially re-corrupted variants where the 3rd byte was normalised to ASCII tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the script itself never contains a literal non-ASCII character; supports -DryRun .githooks/pre-commit: blocks commits containing the ae-corruption byte signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the hook is repo-committed and active for all future work on this machine. Build clean; 225 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
221 lines
9.9 KiB
Plaintext
221 lines
9.9 KiB
Plaintext
@model List<PowderCoating.Web.Controllers.CategoryWithItems>
|
|
@{
|
|
ViewData["Title"] = "Product Catalog";
|
|
ViewData["PageIcon"] = "bi-book";
|
|
ViewData["PageHelpTitle"] = "Product Catalog";
|
|
ViewData["PageHelpContent"] = "The Product Catalog is a library of standard items (wheels, brackets, panels, etc.) that your shop regularly quotes and invoices. Each item has a fixed price — when a catalog item is added to a quote or job, that price is used exactly as entered. No markup, no prep services, and no complexity charges are added on top. Organize items into categories to keep the catalog easy to browse.";
|
|
var totalItemsCount = ViewBag.TotalItemsCount ?? 0;
|
|
var activeItemsCount = ViewBag.ActiveItemsCount ?? 0;
|
|
var averagePrice = ViewBag.AveragePrice ?? 0m;
|
|
var categoryCount = ViewBag.CategoryCount ?? 0;
|
|
var currentCategoryId = ViewBag.CurrentCategoryId;
|
|
var searchTerm = ViewBag.SearchTerm ?? "";
|
|
var hasFilters = ViewBag.HasFilters ?? false;
|
|
var filteredItemsCount = ViewBag.FilteredItemsCount ?? 0;
|
|
}
|
|
|
|
@section Styles {
|
|
<link rel="stylesheet" href="~/css/catalog.css" />
|
|
}
|
|
|
|
@section Scripts {
|
|
<script src="~/js/catalog.js"></script>
|
|
}
|
|
|
|
<div class="d-flex justify-content-end align-items-center gap-2 mb-4">
|
|
<a asp-action="AiPriceCheck" class="btn btn-outline-primary text-nowrap">
|
|
<i class="bi bi-robot me-2"></i>
|
|
<span class="d-none d-sm-inline">AI Price Check</span>
|
|
<span class="d-inline d-sm-none">AI</span>
|
|
</a>
|
|
<a asp-action="ExportCatalogPdf" class="btn btn-primary text-nowrap">
|
|
<i class="bi bi-file-pdf me-2"></i>
|
|
<span class="d-none d-sm-inline">Export Product Catalog to PDF</span>
|
|
<span class="d-inline d-sm-none">PDF</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-6 col-md-3">
|
|
<div class="card catalog-stats-card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="catalog-text-muted mb-1" style="font-size: 0.875rem;">Total Items</p>
|
|
<h3 class="mb-0 fw-bold">@totalItemsCount</h3>
|
|
</div>
|
|
<div class="catalog-stats-icon blue">
|
|
<i class="bi bi-box-seam text-primary" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<div class="card catalog-stats-card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="catalog-text-muted mb-1" style="font-size: 0.875rem;">Active Items</p>
|
|
<h3 class="mb-0 fw-bold">@activeItemsCount</h3>
|
|
</div>
|
|
<div class="catalog-stats-icon green">
|
|
<i class="bi bi-check-circle text-success" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<div class="card catalog-stats-card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="catalog-text-muted mb-1" style="font-size: 0.875rem;">Average Price</p>
|
|
<h3 class="mb-0 fw-bold">@averagePrice.ToString("C")</h3>
|
|
</div>
|
|
<div class="catalog-stats-icon yellow">
|
|
<i class="bi bi-cash-stack text-warning" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<div class="card catalog-stats-card">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="catalog-text-muted mb-1" style="font-size: 0.875rem;">Categories</p>
|
|
<h3 class="mb-0 fw-bold">@categoryCount</h3>
|
|
</div>
|
|
<div class="catalog-stats-icon pink">
|
|
<i class="bi bi-folder text-danger" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (hasFilters)
|
|
{
|
|
<div class="alert alert-info alert-permanent d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<i class="bi bi-funnel me-2"></i>
|
|
Showing <strong>@filteredItemsCount</strong> item(s)
|
|
@if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
<span> matching "<strong>@searchTerm</strong>"</span>
|
|
}
|
|
@if (currentCategoryId != null)
|
|
{
|
|
var categoryName = (ViewBag.Categories as List<SelectListItem>)?.FirstOrDefault(c => c.Value == currentCategoryId.ToString())?.Text;
|
|
<span> in category "<strong>@categoryName</strong>"</span>
|
|
}
|
|
</div>
|
|
<a href="@Url.Action("Index")" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-x me-1"></i>Clear Filters
|
|
</a>
|
|
</div>
|
|
}
|
|
|
|
<!-- Catalog Items Card -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header catalog-card-header py-3">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
|
|
<div class="d-flex flex-column flex-sm-row gap-2 w-100 w-lg-auto">
|
|
<form asp-action="Index" method="get" class="d-flex flex-column flex-sm-row gap-2 flex-grow-1 flex-lg-grow-0">
|
|
<div class="input-group catalog-search-group">
|
|
<span class="input-group-text catalog-search-icon">
|
|
<i class="bi bi-search catalog-text-muted"></i>
|
|
</span>
|
|
<input type="text" name="searchTerm" class="form-control catalog-search-input"
|
|
placeholder="Search items..."
|
|
value="@searchTerm"
|
|
aria-label="Search catalog items">
|
|
</div>
|
|
<select name="categoryId" class="form-select catalog-category-select">
|
|
<option value="">All Categories</option>
|
|
@if (ViewBag.Categories != null)
|
|
{
|
|
foreach (var category in ViewBag.Categories as List<SelectListItem>)
|
|
{
|
|
<option value="@category.Value" selected="@(category.Value == currentCategoryId?.ToString())">
|
|
@category.Text
|
|
</option>
|
|
}
|
|
}
|
|
</select>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
@if (hasFilters)
|
|
{
|
|
<a href="@Url.Action("Index")" class="btn btn-outline-secondary" title="Clear filters">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
}
|
|
</form>
|
|
<div class="d-flex gap-2">
|
|
<a asp-controller="CatalogCategories" asp-action="Index" class="btn btn-outline-secondary text-nowrap">
|
|
<i class="bi bi-folder me-2"></i>
|
|
<span class="d-none d-sm-inline">Manage Categories</span>
|
|
<span class="d-inline d-sm-none">Categories</span>
|
|
</a>
|
|
<a asp-action="Create" class="btn btn-primary text-nowrap">
|
|
<i class="bi bi-plus-circle me-2"></i>
|
|
<span class="d-none d-sm-inline">Add Item</span>
|
|
<span class="d-inline d-sm-none">Add</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
@if (!Model.Any())
|
|
{
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-inbox catalog-empty-icon" style="font-size: 4rem;"></i>
|
|
<h5 class="mt-3 catalog-text-secondary">No catalog items found</h5>
|
|
@if (hasFilters)
|
|
{
|
|
<p class="catalog-text-muted mb-4">Try adjusting your filters</p>
|
|
<a href="@Url.Action("Index")" class="btn btn-outline-secondary">
|
|
<i class="bi bi-x-circle me-2"></i>Clear Filters
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<p class="catalog-text-muted mb-4">Get started by creating your first catalog item</p>
|
|
<a asp-action="Create" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle me-2"></i>Create Your First Item
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<!-- Nested Category View -->
|
|
<div class="catalog-tree p-3">
|
|
@foreach (var categoryWithItems in Model)
|
|
{
|
|
<partial name="_CategoryNode" model="categoryWithItems" />
|
|
}
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var PREFIX = 'pcl_catalog_acc_';
|
|
document.querySelectorAll('.catalog-tree .collapse').forEach(function (el) {
|
|
var stored = localStorage.getItem(PREFIX + el.id);
|
|
if (stored === null) return;
|
|
if (stored === '1') { el.classList.add('show'); }
|
|
else { el.classList.remove('show'); }
|
|
});
|
|
}());
|
|
</script>
|
|
}
|
|
</div>
|
|
</div>
|