Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Equipment/Index.cshtml
T
spouliot dd4785b048 Fix empty-state button/text on list pages when search returns no results
Show 'Add Your First X' and onboarding copy only when the list is truly
empty. When a search or filter is active with no results, show 'Add X'
and 'No X match your search/filters' instead.

Affected: Customers (table + mobile views), Equipment, Inventory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:49:23 -04:00

294 lines
18 KiB
Plaintext

@model PagedResult<PowderCoating.Application.DTOs.Equipment.EquipmentListDto>
@{
ViewData["Title"] = "Equipment";
ViewData["PageIcon"] = "bi-tools";
ViewData["PageHelpTitle"] = "Equipment";
ViewData["PageHelpContent"] = "Track all shop equipment — ovens, spray booths, compressors, and other machinery. Status shows whether each piece is Operational, Needs Maintenance, Under Maintenance, or Out of Service. Next Maintenance date is calculated from the last completed maintenance plus the equipment's maintenance interval. Click any row to view full details and maintenance history.";
}
<div class="pcl-metric-strip">
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "TOTAL", Value: Model.TotalCount.ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "OPERATIONAL", Value: Model.Items.Count(e => e.Status == "Operational").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "NEEDS SERVICE", Value: Model.Items.Count(e => e.Status == "NeedsMaintenance").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
<div class="pcl-metric-strip-cell">
@await Html.PartialAsync("_Metric", (Label: "IN MAINTENANCE", Value: Model.Items.Count(e => e.Status == "UnderMaintenance").ToString(), Delta: (string?)null, DeltaDir: (string?)null))
</div>
</div>
<!-- Equipment 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 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="statusFilter" class="form-select" style="max-width: 250px; min-width: 150px;" onchange="this.form.submit()">
<option value="">All Statuses</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.Operational)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.Operational)">Operational</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.NeedsMaintenance)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.NeedsMaintenance)">Needs Maintenance</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.UnderMaintenance)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.UnderMaintenance)">Under Maintenance</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.OutOfService)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.OutOfService)">Out of Service</option>
<option value="@((int)PowderCoating.Core.Enums.EquipmentStatus.Retired)" selected="@(ViewBag.StatusFilter == PowderCoating.Core.Enums.EquipmentStatus.Retired)">Retired</option>
</select>
<div class="input-group" style="max-width: 350px; min-width: 200px;">
<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 equipment..." 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 Equipment</span>
<span class="d-inline d-sm-none">Add</span>
</a>
</div>
</div>
</div>
<div class="card-body p-0">
@if (!Model.Items.Any())
{
var isEquipmentListFiltered = !string.IsNullOrEmpty(ViewBag.SearchTerm as string) || ViewBag.StatusFilter != null;
<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 equipment found</h5>
<p class="text-muted mb-4">@(isEquipmentListFiltered ? "No equipment matches your search." : "Get started by adding your first equipment.")</p>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>@(isEquipmentListFiltered ? "Add Equipment" : "Add Your First Equipment")
</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">Equipment</th>
<th sortable="EquipmentCode" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Code</th>
<th>Type</th>
<th sortable="Status" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Status</th>
<th>Location</th>
<th sortable="NextMaintenanceDate" current-sort="@ViewBag.SortColumn" current-direction="@ViewBag.SortDirection">Next Maintenance</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody id="equipmentTable">
@foreach (var equipment in Model.Items.Where(e => e.IsActive))
{
<tr class="equipment-row" data-equipment-id="@equipment.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, #f093fb 0%, #f5576c 100%); color: white; font-weight: 600;">
@if (!string.IsNullOrEmpty(equipment.EquipmentType))
{
@equipment.EquipmentType.Substring(0, 1).ToUpper()
}
else
{
@equipment.EquipmentName.Substring(0, 1).ToUpper()
}
</div>
<div>
<div class="fw-semibold">@equipment.EquipmentName</div>
@if (!string.IsNullOrEmpty(equipment.EquipmentNumber))
{
<small class="text-muted">@equipment.EquipmentNumber</small>
}
</div>
</div>
</td>
<td>@equipment.EquipmentType</td>
<td>
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.EquipmentStatus(equipment.Status), Text: equipment.StatusDisplay))
</td>
<td>
@if (!string.IsNullOrEmpty(equipment.Location))
{
<span><i class="bi bi-geo-alt me-1"></i>@equipment.Location</span>
}
else
{
<span class="text-muted">—</span>
}
</td>
<td>
@if (equipment.NextScheduledMaintenance.HasValue)
{
<span>@equipment.NextScheduledMaintenance.Value.ToString("MMM dd, yyyy")</span>
}
else
{
<span class="text-muted">Not scheduled</span>
}
</td>
<td class="text-end pe-4">
<div class="btn-group btn-group-sm">
<a asp-action="Details" asp-route-id="@equipment.Id" class="btn btn-outline-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
<a asp-action="Edit" asp-route-id="@equipment.Id" class="btn btn-outline-warning" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-route-id="@equipment.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 equipment in Model.Items.Where(e => e.IsActive))
{
<div class="mobile-data-card"
data-id="@equipment.Id"
onclick="window.location.href='@Url.Action("Details", new { id = equipment.Id })'">
<div class="mobile-card-header">
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<i class="bi bi-tools"></i>
</div>
<div class="mobile-card-title">
<h6>@equipment.EquipmentName</h6>
<small>@equipment.EquipmentType</small>
</div>
</div>
<div class="mobile-card-body">
@if (!string.IsNullOrEmpty(equipment.EquipmentNumber))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Code</span>
<span class="mobile-card-value">@equipment.EquipmentNumber</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value">
@switch (equipment.Status)
{
case "Operational":
<span class="badge bg-success bg-opacity-10 text-success">
<i class="bi bi-check-circle me-1"></i>@equipment.StatusDisplay
</span>
break;
case "NeedsMaintenance":
<span class="badge bg-warning bg-opacity-10 text-warning">
<i class="bi bi-exclamation-triangle me-1"></i>@equipment.StatusDisplay
</span>
break;
case "UnderMaintenance":
<span class="badge bg-info bg-opacity-10 text-info">
<i class="bi bi-wrench me-1"></i>@equipment.StatusDisplay
</span>
break;
case "OutOfService":
<span class="badge bg-danger bg-opacity-10 text-danger">
<i class="bi bi-x-circle me-1"></i>@equipment.StatusDisplay
</span>
break;
default:
<span class="badge bg-secondary bg-opacity-10 text-secondary">
@equipment.StatusDisplay
</span>
break;
}
</span>
</div>
@if (!string.IsNullOrEmpty(equipment.Location))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Location</span>
<span class="mobile-card-value"><i class="bi bi-geo-alt me-1"></i>@equipment.Location</span>
</div>
}
@if (equipment.NextScheduledMaintenance.HasValue)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Next Maintenance</span>
<span class="mobile-card-value">@equipment.NextScheduledMaintenance.Value.ToString("MMM dd, yyyy")</span>
</div>
}
</div>
<div class="mobile-card-footer">
<a href="@Url.Action("Details", new { id = equipment.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 = equipment.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>
// Simple search functionality
document.getElementById('searchInput')?.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#equipmentTable tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(searchTerm) ? '' : 'none';
});
});
// Make table rows clickable
document.querySelectorAll('.equipment-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 equipmentId = this.getAttribute('data-equipment-id');
window.location.href = '@Url.Action("Details", "Equipment")/' + equipmentId;
});
// Add hover effect
row.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#f8f9fa';
});
row.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
</script>
}