Files
PowderCoatingLogix/src/PowderCoating.Web/Views/BugReport/Index.cshtml
T
spouliot 03d3f57f7b Fix time entry workers, powder usage logging, inventory edit, and mojibake
- JobTimeEntry: migrate to UserId/UserDisplayName; make ShopWorkerId nullable
  (migration MigrateTimeEntriesToUserId)
- Log Time modal: populate worker dropdown from Identity users instead of
  ShopWorkers; fix ShopMobile view same issue
- Inventory Ledger: scan-based JobUsage transactions now appear in
  Powder Usage By Job tab (synthesized from InventoryTransaction)
- Inventory Ledger: add Edit button for JobUsage transactions; new
  GetUsageForEdit + EditUsageTransaction endpoints; inventory-ledger.js
- InventoryTransactionRepository: include Job.Customer for ledger queries
- InventoryAiLookupService: handle JSON-LD @graph wrapper (Columbia
  Coatings / WooCommerce+Yoast); add HTML price snippet fallback
- Fix mojibake in 9 views: â†' → →, âœ" → ✓, âš  → ⚠

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:05:37 -04:00

365 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@model List<PowderCoating.Application.DTOs.BugReport.BugReportDto>
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Bug Reports";
ViewData["PageIcon"] = "bi-bug";
var sortCol = ViewBag.SortColumn as string ?? "CreatedAt";
var sortDir = ViewBag.SortDirection as string ?? "desc";
int totalCount = ViewBag.TotalCount ?? 0;
int pageNumber = ViewBag.PageNumber ?? 1;
int pageSize = ViewBag.PageSize ?? 25;
int totalPages = ViewBag.TotalPages ?? 1;
string NextDir(string col) => sortCol == col && sortDir == "asc" ? "desc" : "asc";
string SortIcon(string col) => sortCol == col
? (sortDir == "asc" ? "bi-sort-up" : "bi-sort-down")
: "bi-arrow-down-up text-muted";
}
@section Styles {
<style>
[data-bs-theme="dark"] .table-light th,
[data-bs-theme="dark"] .table-light td {
background-color: var(--bs-secondary-bg);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .card {
border-color: var(--bs-border-color) !important;
}
[data-bs-theme="dark"] .sort-link {
color: var(--bs-body-color) !important;
}
[data-bs-theme="dark"] .border-top {
border-color: var(--bs-border-color) !important;
}
[data-bs-theme="dark"] .pagination .page-link {
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .pagination .page-item.active .page-link {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: #fff;
}
</style>
}
<div class="container-fluid">
<div class="mb-4"></div>
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle"></i> @TempData["SuccessMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle"></i> @TempData["ErrorMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
<!-- Filters -->
<div class="card mb-3">
<div class="card-body py-2">
<form method="get" class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small mb-1">Search</label>
<input type="text" name="searchTerm" value="@ViewBag.SearchTerm" class="form-control form-control-sm"
placeholder="Title, description, or submitter..." />
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Status</label>
<select name="statusFilter" class="form-select form-select-sm">
<option value="">All Statuses</option>
@foreach (var s in Enum.GetValues<BugReportStatus>())
{
<option value="@s" selected="@(ViewBag.StatusFilter == s.ToString())">@s.ToString()</option>
}
</select>
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Priority</label>
<select name="priorityFilter" class="form-select form-select-sm">
<option value="">All Priorities</option>
@foreach (var p in Enum.GetValues<BugReportPriority>())
{
<option value="@p" selected="@(ViewBag.PriorityFilter == p.ToString())">@p.ToString()</option>
}
</select>
</div>
<div class="col-md-2">
<label class="form-label small mb-1">Per page</label>
<select name="pageSize" class="form-select form-select-sm">
@foreach (var n in new[] { 10, 25, 50, 100 })
{
<option value="@n" selected="@(pageSize == n)">@n</option>
}
</select>
</div>
<input type="hidden" name="sortColumn" value="@sortCol" />
<input type="hidden" name="sortDirection" value="@sortDir" />
<div class="col-md-2">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-search"></i> Filter
</button>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="card">
<div class="card-body p-0">
@if (!Model.Any())
{
<div class="text-center py-5 text-muted">
<i class="bi bi-check-circle display-4 d-block mb-2"></i>
<p>No bug reports found.</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>
<a asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-pageSize="@pageSize"
asp-route-sortColumn="Title"
asp-route-sortDirection="@NextDir("Title")"
class="text-decoration-none sort-link">
Title <i class="bi @SortIcon("Title")"></i>
</a>
</th>
<th>
<a asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-pageSize="@pageSize"
asp-route-sortColumn="Priority"
asp-route-sortDirection="@NextDir("Priority")"
class="text-decoration-none sort-link">
Priority <i class="bi @SortIcon("Priority")"></i>
</a>
</th>
<th>
<a asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-pageSize="@pageSize"
asp-route-sortColumn="Status"
asp-route-sortDirection="@NextDir("Status")"
class="text-decoration-none sort-link">
Status <i class="bi @SortIcon("Status")"></i>
</a>
</th>
<th>
<a asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-pageSize="@pageSize"
asp-route-sortColumn="Submitted"
asp-route-sortDirection="@NextDir("Submitted")"
class="text-decoration-none sort-link">
Submitted By <i class="bi @SortIcon("Submitted")"></i>
</a>
</th>
<th>Company</th>
<th>
<a asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-pageSize="@pageSize"
asp-route-sortColumn="CreatedAt"
asp-route-sortDirection="@NextDir("CreatedAt")"
class="text-decoration-none sort-link">
Submitted <i class="bi @SortIcon("CreatedAt")"></i>
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var report in Model)
{
<tr style="cursor:pointer" onclick="window.location='@Url.Action("Edit", new { id = report.Id })'">
<td>
<div class="fw-semibold">@report.Title</div>
<div class="text-muted small text-truncate" style="max-width:320px;" title="@report.Description">
@report.Description
</div>
</td>
<td>
@{
var priClass = report.Priority switch
{
BugReportPriority.Critical => "bg-danger",
BugReportPriority.High => "bg-warning text-dark",
BugReportPriority.Normal => "bg-primary",
_ => "bg-secondary"
};
}
<span class="badge @priClass">@report.Priority</span>
</td>
<td>
@{
var statusClass = report.Status switch
{
BugReportStatus.New => "bg-info text-dark",
BugReportStatus.InProgress => "bg-warning text-dark",
BugReportStatus.Completed => "bg-success",
BugReportStatus.Cancelled => "bg-secondary",
_ => "bg-secondary"
};
var statusLabel = report.Status switch
{
BugReportStatus.InProgress => "In Progress",
_ => report.Status.ToString()
};
}
<span class="badge @statusClass">@statusLabel</span>
</td>
<td class="small">@report.SubmittedByUserName</td>
<td class="small text-muted">@report.CompanyId</td>
<td class="small text-muted text-nowrap">@report.CreatedAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt")</td>
<td onclick="event.stopPropagation()">
<a asp-action="Edit" asp-route-id="@report.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Edit
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile card view — shown on screens < 992px -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@foreach (var report in Model)
{
var mPriClass = report.Priority switch
{
BugReportPriority.Critical => "bg-danger",
BugReportPriority.High => "bg-warning text-dark",
BugReportPriority.Normal => "bg-primary",
_ => "bg-secondary"
};
var mStatusClass = report.Status switch
{
BugReportStatus.New => "bg-info text-dark",
BugReportStatus.InProgress => "bg-warning text-dark",
BugReportStatus.Completed => "bg-success",
BugReportStatus.Cancelled => "bg-secondary",
_ => "bg-secondary"
};
var mStatusLabel = report.Status switch
{
BugReportStatus.InProgress => "In Progress",
_ => report.Status.ToString()
};
<a href="@Url.Action("Edit", new { id = report.Id })" class="mobile-data-card text-decoration-none">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-danger"><i class="bi bi-bug"></i></div>
<div class="mobile-card-title">
<h6>@report.Title</h6>
<small>@report.CreatedAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Status</span>
<span class="mobile-card-value"><span class="badge @mStatusClass">@mStatusLabel</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Priority</span>
<span class="mobile-card-value"><span class="badge @mPriClass">@report.Priority</span></span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Reporter</span>
<span class="mobile-card-value">@report.SubmittedByUserName</span>
</div>
@if (report.CompanyId > 0)
{
<div class="mobile-card-row">
<span class="mobile-card-label">Company ID</span>
<span class="mobile-card-value">@report.CompanyId</span>
</div>
}
</div>
<div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">Edit →</span>
</div>
</a>
}
</div>
</div>
<!-- Pagination -->
@if (totalPages > 1)
{
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top">
<small class="text-muted">
Showing @((pageNumber - 1) * pageSize + 1)@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount
</small>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item @(pageNumber <= 1 ? "disabled" : "")">
<a class="page-link" asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-sortColumn="@sortCol"
asp-route-sortDirection="@sortDir"
asp-route-pageSize="@pageSize"
asp-route-pageNumber="@(pageNumber - 1)">
<i class="bi bi-chevron-left"></i>
</a>
</li>
@for (int i = Math.Max(1, pageNumber - 2); i <= Math.Min(totalPages, pageNumber + 2); i++)
{
<li class="page-item @(i == pageNumber ? "active" : "")">
<a class="page-link" asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-sortColumn="@sortCol"
asp-route-sortDirection="@sortDir"
asp-route-pageSize="@pageSize"
asp-route-pageNumber="@i">@i</a>
</li>
}
<li class="page-item @(pageNumber >= totalPages ? "disabled" : "")">
<a class="page-link" asp-action="Index"
asp-route-searchTerm="@ViewBag.SearchTerm"
asp-route-statusFilter="@ViewBag.StatusFilter"
asp-route-priorityFilter="@ViewBag.PriorityFilter"
asp-route-sortColumn="@sortCol"
asp-route-sortDirection="@sortDir"
asp-route-pageSize="@pageSize"
asp-route-pageNumber="@(pageNumber + 1)">
<i class="bi bi-chevron-right"></i>
</a>
</li>
</ul>
</nav>
</div>
}
}
</div>
</div>
</div>