54f444d981
Claude reviews every active catalog item against the shop's own operating costs and returns a per-item verdict (below-cost / thin-margin / high / ok) with a suggested price range, cost floor, and assumptions. - New entity: CatalogPriceCheckReport (JSON blob, archived per company) - New service: IAiCatalogPriceCheckService / AiCatalogPriceCheckService batches items 25 at a time to stay within model context limits - Two new controller actions: GET AiPriceCheck (view report) + POST RunAiPriceCheck - AiPriceCheck view: summary cards (counts by verdict), color-coded item cards with Edit Price link, assumptions detail, and loading spinner on submit - AI Price Check button added to catalog Index header - Migration AddCatalogPriceCheckReport applied 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>
|