f6d457fe0e
Layer 1 — relocate off nav: Shop Display + Shop Mobile → Jobs page header (split dropdown on Blank Work Order) Powder Insights → Inventory page header button Layer 2 — remove orphan section headers: "Main Menu" (only had Dashboard under it) "Reports" (only had Reports link under it) Layer 3 — CSS density: nav-link padding 0.75rem → 0.55rem vertical nav-section-title padding 1rem/0.5rem → 0.65rem/0.3rem Operations mode: 27 elements → 22. Scrollbar eliminated on standard screens. 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>
|
|
}
|