4ec55e7290
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).
Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
455 lines
24 KiB
Plaintext
455 lines
24 KiB
Plaintext
@model PagedResult<PowderCoating.Application.DTOs.Inventory.InventoryListDto>
|
|
|
|
@{
|
|
ViewData["Title"] = "Inventory";
|
|
ViewData["PageIcon"] = "bi-box-seam";
|
|
ViewData["PageHelpTitle"] = "Inventory";
|
|
ViewData["PageHelpContent"] = "Track powder coatings, consumables, and other shop materials. Items show as Low Stock when quantity falls at or below the Reorder Point — the Low Stock count at the top is your reorder alert. Click any row to view full details or edit. Use the search box and category filter to narrow the list. Low Stock filter shows only items needing attention.";
|
|
var lowStockCount = (int)(ViewBag.StatsLowStockCount ?? 0);
|
|
var activeCount = (int)(ViewBag.StatsActiveCount ?? 0);
|
|
var totalValue = (decimal)(ViewBag.StatsTotalValue ?? 0m);
|
|
}
|
|
|
|
<div class="d-flex justify-content-end align-items-center mb-4">
|
|
<div class="d-flex gap-2">
|
|
<a asp-action="Ledger" class="btn btn-outline-secondary">
|
|
<i class="bi bi-journal-text me-2"></i>Inventory Activity
|
|
</a>
|
|
<a asp-controller="PowderInsights" asp-action="Index" class="btn btn-outline-secondary" title="Powder usage analytics">
|
|
<i class="bi bi-graph-up me-2"></i>Powder Insights
|
|
</a>
|
|
<a asp-action="SamplePanels" class="btn btn-outline-primary">
|
|
<i class="bi bi-palette me-2"></i>Manage Sample Panels
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Cards - Desktop -->
|
|
<div class="stats-cards-desktop">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="text-muted mb-1" style="font-size: 0.875rem;">Total Items</p>
|
|
<h3 class="mb-0 fw-bold">@Model.TotalCount</h3>
|
|
</div>
|
|
<div class="rounded-circle p-3" style="background: #dbeafe;">
|
|
<i class="bi bi-box-seam text-primary" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="text-muted mb-1" style="font-size: 0.875rem;">Low Stock Items</p>
|
|
<h3 class="mb-0 fw-bold @(lowStockCount > 0 ? "text-danger" : "")">@lowStockCount</h3>
|
|
</div>
|
|
<div class="rounded-circle p-3" style="background: #fee2e2;">
|
|
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="text-muted mb-1" style="font-size: 0.875rem;">Active Items</p>
|
|
<h3 class="mb-0 fw-bold">@activeCount</h3>
|
|
</div>
|
|
<div class="rounded-circle p-3" style="background: #d1fae5;">
|
|
<i class="bi bi-check-circle text-success" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<p class="text-muted mb-1" style="font-size: 0.875rem;">Total Value</p>
|
|
<h3 class="mb-0 fw-bold">@totalValue.ToString("C")</h3>
|
|
</div>
|
|
<div class="rounded-circle p-3" style="background: #fef3c7;">
|
|
<i class="bi bi-currency-dollar text-warning" style="font-size: 1.5rem;"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compact Stats - Mobile -->
|
|
<div class="mobile-stats-compact">
|
|
<div class="card">
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-icon"><i class="bi bi-box-seam text-primary"></i></div>
|
|
<div class="stat-value">@Model.TotalCount</div>
|
|
<div class="stat-label">Total</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon"><i class="bi bi-exclamation-triangle text-danger"></i></div>
|
|
<div class="stat-value @(lowStockCount > 0 ? "text-danger" : "")">@lowStockCount</div>
|
|
<div class="stat-label">Low Stock</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon"><i class="bi bi-check-circle text-success"></i></div>
|
|
<div class="stat-value">@activeCount</div>
|
|
<div class="stat-label">Active</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-icon"><i class="bi bi-currency-dollar text-warning"></i></div>
|
|
<div class="stat-value">@totalValue.ToString("C0")</div>
|
|
<div class="stat-label">Value</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@{
|
|
var lowStockOnly = (bool)(ViewBag.LowStockOnly ?? false);
|
|
}
|
|
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm) || !string.IsNullOrEmpty(ViewBag.Category) || lowStockOnly)
|
|
{
|
|
<div class="alert @(lowStockOnly ? "alert-warning" : "alert-info") alert-permanent d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<i class="bi bi-funnel-fill me-2"></i>
|
|
@if (lowStockOnly)
|
|
{
|
|
<span>Showing <strong>@Model.TotalCount</strong> low stock item@(Model.TotalCount == 1 ? "" : "s") — at or below reorder point</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Showing <strong>@Model.TotalCount</strong> item(s)</span>
|
|
@if (!string.IsNullOrEmpty(ViewBag.SearchTerm))
|
|
{
|
|
<span> matching "<strong>@ViewBag.SearchTerm</strong>"</span>
|
|
}
|
|
@if (!string.IsNullOrEmpty(ViewBag.Category))
|
|
{
|
|
<span> in category "<strong>@ViewBag.Category</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>
|
|
}
|
|
|
|
<!-- Inventory Table Card -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 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">
|
|
<input type="hidden" name="sortColumn" value="@ViewBag.SortColumn" />
|
|
<input type="hidden" name="sortDirection" value="@ViewBag.SortDirection" />
|
|
<input type="hidden" name="pageSize" value="@Model.PageSize" />
|
|
<select name="category" class="form-select" style="max-width: 250px; min-width: 150px;" onchange="this.form.submit()">
|
|
<option value="">All Categories</option>
|
|
@foreach (var cat in ViewBag.Categories)
|
|
{
|
|
<option value="@cat" selected="@(cat == ViewBag.Category)">@cat</option>
|
|
}
|
|
</select>
|
|
<div class="input-group" style="max-width: 480px; min-width: 300px;">
|
|
<span class="input-group-text bg-white border-end-0">
|
|
<i class="bi bi-search text-muted"></i>
|
|
</span>
|
|
<input type="text" name="searchTerm" class="form-control border-start-0"
|
|
placeholder="Search inventory..."
|
|
value="@ViewBag.SearchTerm">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<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 class="card-body p-0">
|
|
@if (!Model.Items.Any())
|
|
{
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-inbox" style="font-size: 4rem; color: #d1d5db;"></i>
|
|
<h5 class="mt-3 text-muted">No inventory items found</h5>
|
|
<p class="text-muted mb-4">Get started by adding your first inventory item</p>
|
|
<a asp-action="Create" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle me-2"></i>Add Your First Item
|
|
</a>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th sortable="Name" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection" class="ps-4">Item Name</th>
|
|
<th sortable="Category" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Category</th>
|
|
<th sortable="ColorName" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Color</th>
|
|
<th>Vendor</th>
|
|
<th sortable="QuantityOnHand" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Quantity</th>
|
|
<th>Reorder Point</th>
|
|
<th sortable="UnitCost" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Unit Cost</th>
|
|
<th>Stock Value</th>
|
|
<th sortable="IsActive" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
|
|
<th class="text-end pe-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="inventoryTable">
|
|
@foreach (var item in Model.Items)
|
|
{
|
|
<tr class="inventory-row" data-item-id="@item.Id" style="cursor: pointer;">
|
|
<td class="ps-4">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="rounded-circle d-flex align-items-center justify-content-center"
|
|
style="width: 40px; height: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600;">
|
|
<i class="bi bi-box"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">@item.Name</div>
|
|
<small class="text-muted">@item.SKU</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary bg-opacity-10 text-secondary">
|
|
@item.Category
|
|
</span>
|
|
</td>
|
|
<td>
|
|
@if (!string.IsNullOrEmpty(item.ColorName))
|
|
{
|
|
<span>@item.ColorName</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">—</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (!string.IsNullOrEmpty(item.PrimaryVendorName))
|
|
{
|
|
<span class="text-muted">@item.PrimaryVendorName</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">—</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<span class="@(item.IsOutOfStock ? "text-dark fw-semibold" : item.IsLowStock ? "text-danger fw-semibold" : "")">
|
|
@item.QuantityOnHand.ToString("N2") @item.UnitOfMeasure
|
|
@if (item.IsOutOfStock)
|
|
{
|
|
<i class="bi bi-x-circle ms-1"></i>
|
|
}
|
|
else if (item.IsLowStock)
|
|
{
|
|
<i class="bi bi-exclamation-triangle ms-1"></i>
|
|
}
|
|
</span>
|
|
</td>
|
|
<td>@item.ReorderPoint.ToString("N2") @item.UnitOfMeasure</td>
|
|
<td>@item.UnitCost.ToString("C")</td>
|
|
<td>
|
|
<span class="fw-semibold">@((item.QuantityOnHand * item.UnitCost).ToString("C"))</span>
|
|
</td>
|
|
<td>
|
|
@if (item.IsIncoming)
|
|
{
|
|
<span class="badge bg-warning bg-opacity-25 text-warning-emphasis">
|
|
<i class="bi bi-truck me-1"></i>Incoming
|
|
</span>
|
|
}
|
|
else if (item.IsActive)
|
|
{
|
|
<span class="badge bg-success bg-opacity-10 text-success">
|
|
<i class="bi bi-check-circle me-1"></i>Active
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-danger bg-opacity-10 text-danger">
|
|
<i class="bi bi-x-circle me-1"></i>Inactive
|
|
</span>
|
|
}
|
|
</td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group btn-group-sm">
|
|
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-outline-primary" title="View Details">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-outline-warning" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-outline-danger" title="Delete">
|
|
<i class="bi bi-trash"></i>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Mobile Card View -->
|
|
<div class="mobile-card-view">
|
|
<div class="mobile-card-list">
|
|
@foreach (var item in Model.Items)
|
|
{
|
|
<div class="mobile-data-card"
|
|
data-id="@item.Id"
|
|
onclick="window.location.href='@Url.Action("Details", new { id = item.Id })'">
|
|
|
|
<div class="mobile-card-header">
|
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
|
<i class="bi bi-box"></i>
|
|
</div>
|
|
<div class="mobile-card-title">
|
|
<h6>@item.Name</h6>
|
|
<small>@item.SKU</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mobile-card-body">
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Category</span>
|
|
<span class="mobile-card-value">
|
|
<span class="badge bg-secondary bg-opacity-10 text-secondary">
|
|
@item.Category
|
|
</span>
|
|
</span>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(item.ColorName))
|
|
{
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Color</span>
|
|
<span class="mobile-card-value">@item.ColorName</span>
|
|
</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(item.PrimaryVendorName))
|
|
{
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Vendor</span>
|
|
<span class="mobile-card-value">@item.PrimaryVendorName</span>
|
|
</div>
|
|
}
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Quantity</span>
|
|
<span class="mobile-card-value @(item.IsOutOfStock ? "text-dark fw-semibold" : item.IsLowStock ? "text-danger fw-semibold" : "")">
|
|
@item.QuantityOnHand.ToString("N2") @item.UnitOfMeasure
|
|
@if (item.IsOutOfStock)
|
|
{
|
|
<i class="bi bi-x-circle ms-1"></i>
|
|
}
|
|
else if (item.IsLowStock)
|
|
{
|
|
<i class="bi bi-exclamation-triangle ms-1"></i>
|
|
}
|
|
</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Reorder Point</span>
|
|
<span class="mobile-card-value">@item.ReorderPoint.ToString("N2") @item.UnitOfMeasure</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Unit Cost</span>
|
|
<span class="mobile-card-value">@item.UnitCost.ToString("C")</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Stock Value</span>
|
|
<span class="mobile-card-value fw-semibold text-primary">@((item.QuantityOnHand * item.UnitCost).ToString("C"))</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Status</span>
|
|
<span class="mobile-card-value">
|
|
@if (item.IsActive)
|
|
{
|
|
<span class="badge bg-success bg-opacity-10 text-success">
|
|
<i class="bi bi-check-circle me-1"></i>Active
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-danger bg-opacity-10 text-danger">
|
|
<i class="bi bi-x-circle me-1"></i>Inactive
|
|
</span>
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mobile-card-footer">
|
|
<a href="@Url.Action("Details", new { id = item.Id })"
|
|
class="btn btn-sm btn-outline-primary"
|
|
onclick="event.stopPropagation();">
|
|
<i class="bi bi-eye me-1"></i>View
|
|
</a>
|
|
<a href="@Url.Action("Edit", new { id = item.Id })"
|
|
class="btn btn-sm btn-outline-secondary"
|
|
onclick="event.stopPropagation();">
|
|
<i class="bi bi-pencil me-1"></i>Edit
|
|
</a>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
@if (Model.TotalCount > 0)
|
|
{
|
|
@await Html.PartialAsync("_Pagination", Model)
|
|
}
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
// Make table rows clickable
|
|
document.querySelectorAll('.inventory-row').forEach(row => {
|
|
row.addEventListener('click', function(e) {
|
|
// Don't navigate if clicking on action buttons or links
|
|
if (e.target.closest('.btn-group') || e.target.closest('a') || e.target.closest('button')) {
|
|
return;
|
|
}
|
|
|
|
const itemId = this.getAttribute('data-item-id');
|
|
window.location.href = '@Url.Action("Details", "Inventory")/' + itemId;
|
|
});
|
|
|
|
// Add hover effect
|
|
row.addEventListener('mouseenter', function() {
|
|
this.style.backgroundColor = '#f8f9fa';
|
|
});
|
|
|
|
row.addEventListener('mouseleave', function() {
|
|
this.style.backgroundColor = '';
|
|
});
|
|
});
|
|
</script>
|
|
}
|