03d3f57f7b
- 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>
365 lines
20 KiB
Plaintext
365 lines
20 KiB
Plaintext
@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>
|