Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Customers/Activity.cshtml
T
spouliot a0bdd2b5b4 Sweep all .cshtml files for encoding corruption; add pre-commit guard
Replace all corruption variants with HTML entities across 226 view files:
- 3-char UTF-8-as-Win1252 sequences (ae-corruption)
- Standalone smart/curly quotes that break C# Razor expressions
- Partially re-corrupted variants where the 3rd byte was normalised to ASCII

tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the
script itself never contains a literal non-ASCII character; supports -DryRun

.githooks/pre-commit: blocks commits containing the ae-corruption byte
signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the
hook is repo-committed and active for all future work on this machine.

Build clean; 225 unit tests pass.

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

474 lines
26 KiB
Plaintext

@using PowderCoating.Application.DTOs.Job
@using PowderCoating.Application.DTOs.Quote
@using PowderCoating.Application.DTOs.Common
@{
ViewData["Title"] = $"Activity - {ViewBag.CustomerName}";
ViewData["PageIcon"] = "bi-clock-history";
var customerId = (int)ViewBag.CustomerId;
var customerName = (string)ViewBag.CustomerName;
var activeTab = (string)ViewBag.ActiveTab;
var jobs = (PagedResult<JobListDto>)ViewBag.Jobs;
var jobSort = (string)ViewBag.JobSort;
var jobDir = (string)ViewBag.JobDir;
var quotes = (PagedResult<QuoteListDto>)ViewBag.Quotes;
var quoteSort = (string)ViewBag.QuoteSort;
var quoteDir = (string)ViewBag.QuoteDir;
// Helper: build a URL that changes only the job sort column/direction
string JobSortUrl(string col)
{
var newDir = jobSort == col && jobDir == "asc" ? "desc" : "asc";
return Url.Action("Activity", new
{
id = customerId,
activeTab = "jobs",
jobSort = col,
jobDir = newDir,
jobPage = 1,
jobSize = jobs.PageSize,
quoteSort,
quoteDir,
quotePage = quotes.PageNumber,
quoteSize = quotes.PageSize
})!;
}
string JobSortIcon(string col)
{
if (jobSort != col) return "bi-arrow-down-up";
return jobDir == "asc" ? "bi-arrow-up" : "bi-arrow-down";
}
// Helper: build a URL that changes only the quote sort column/direction
string QuoteSortUrl(string col)
{
var newDir = quoteSort == col && quoteDir == "asc" ? "desc" : "asc";
return Url.Action("Activity", new
{
id = customerId,
activeTab = "quotes",
jobSort,
jobDir,
jobPage = jobs.PageNumber,
jobSize = jobs.PageSize,
quoteSort = col,
quoteDir = newDir,
quotePage = 1,
quoteSize = quotes.PageSize
})!;
}
string QuoteSortIcon(string col)
{
if (quoteSort != col) return "bi-arrow-down-up";
return quoteDir == "asc" ? "bi-arrow-up" : "bi-arrow-down";
}
// Helper: build page-change URL for jobs
string JobPageUrl(int page)
{
return Url.Action("Activity", new
{
id = customerId,
activeTab = "jobs",
jobSort,
jobDir,
jobPage = page,
jobSize = jobs.PageSize,
quoteSort,
quoteDir,
quotePage = quotes.PageNumber,
quoteSize = quotes.PageSize
})!;
}
// Helper: build page-change URL for quotes
string QuotePageUrl(int page)
{
return Url.Action("Activity", new
{
id = customerId,
activeTab = "quotes",
jobSort,
jobDir,
jobPage = jobs.PageNumber,
jobSize = jobs.PageSize,
quoteSort,
quoteDir,
quotePage = page,
quoteSize = quotes.PageSize
})!;
}
}
<div class="d-flex justify-content-end gap-2 mb-4">
<a asp-controller="Jobs" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-success">
<i class="bi bi-plus-circle me-2"></i>New Job
</a>
<a asp-controller="Quotes" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-outline-primary">
<i class="bi bi-file-text me-2"></i>New Quote
</a>
<a asp-action="Details" asp-route-id="@customerId" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Customer
</a>
</div>
<!-- Summary Badges -->
<div class="d-flex gap-3 mb-4">
<span class="badge bg-primary bg-opacity-10 text-primary fs-6 px-3 py-2">
<i class="bi bi-briefcase me-2"></i>@jobs.TotalCount Job@(jobs.TotalCount == 1 ? "" : "s")
</span>
<span class="badge bg-info bg-opacity-10 text-info fs-6 px-3 py-2">
<i class="bi bi-file-text me-2"></i>@quotes.TotalCount Quote@(quotes.TotalCount == 1 ? "" : "s")
</span>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-0" id="activityTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "jobs" ? "active" : "")" id="jobs-tab"
data-bs-toggle="tab" data-bs-target="#jobs-panel"
type="button" role="tab">
<i class="bi bi-briefcase me-2"></i>Jobs
<span class="badge bg-secondary ms-1">@jobs.TotalCount</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == "quotes" ? "active" : "")" id="quotes-tab"
data-bs-toggle="tab" data-bs-target="#quotes-panel"
type="button" role="tab">
<i class="bi bi-file-text me-2"></i>Quotes
<span class="badge bg-secondary ms-1">@quotes.TotalCount</span>
</button>
</li>
</ul>
<div class="tab-content" id="activityTabContent">
<!-- ===== JOBS TAB ===== -->
<div class="tab-pane fade @(activeTab == "jobs" ? "show active" : "")" id="jobs-panel" role="tabpanel">
<div class="card border-0 shadow-sm border-top-0" style="border-top-left-radius:0; border-top-right-radius:0;">
<div class="card-body p-0">
@if (!jobs.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 jobs found</h5>
<p class="text-muted mb-4">This customer has no jobs yet</p>
<a asp-controller="Jobs" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Create First Job
</a>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="ps-4">
<a href="@JobSortUrl("JobNumber")" class="text-decoration-none" style="color:inherit">
Job Number <i class="bi @JobSortIcon("JobNumber")"></i>
</a>
</th>
<th>Description</th>
<th>
<a href="@JobSortUrl("Status")" class="text-decoration-none" style="color:inherit">
Status <i class="bi @JobSortIcon("Status")"></i>
</a>
</th>
<th>
<a href="@JobSortUrl("Priority")" class="text-decoration-none" style="color:inherit">
Priority <i class="bi @JobSortIcon("Priority")"></i>
</a>
</th>
<th>
<a href="@JobSortUrl("DueDate")" class="text-decoration-none" style="color:inherit">
Due Date <i class="bi @JobSortIcon("DueDate")"></i>
</a>
</th>
<th>
<a href="@JobSortUrl("FinalPrice")" class="text-decoration-none" style="color:inherit">
Price <i class="bi @JobSortIcon("FinalPrice")"></i>
</a>
</th>
<th>
<a href="@JobSortUrl("CreatedAt")" class="text-decoration-none" style="color:inherit">
Created <i class="bi @JobSortIcon("CreatedAt")"></i>
</a>
</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var job in jobs.Items)
{
<tr style="cursor:pointer;" onclick="window.location.href='@Url.Action("Details", "Jobs", new { id = job.Id })'">
<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:38px;height:38px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;">
<i class="bi bi-briefcase"></i>
</div>
<span class="fw-semibold">@job.JobNumber</span>
</div>
</td>
<td class="text-truncate" style="max-width:220px;">@job.Description</td>
<td>
<span class="badge bg-@job.StatusColorClass bg-opacity-10 text-@job.StatusColorClass">
@job.StatusDisplayName
</span>
</td>
<td>
<span class="badge bg-@job.PriorityColorClass">@job.PriorityDisplayName</span>
</td>
<td>
@if (job.DueDate.HasValue)
{
var overdue = job.DueDate.Value < DateTime.Now
&& job.StatusCode != "COMPLETED"
&& job.StatusCode != "DELIVERED";
<span class="@(overdue ? "text-danger fw-semibold" : "")">
@job.DueDate.Value.ToString("MMM dd, yyyy")
@if (overdue) { <i class="bi bi-exclamation-triangle ms-1"></i> }
</span>
}
else
{
<span class="text-muted">&mdash;</span>
}
</td>
<td class="fw-semibold">@job.FinalPrice.ToString("C")</td>
<td class="text-muted small">@job.CreatedAt.ToString("MMM dd, yyyy")</td>
<td class="text-end pe-4" onclick="event.stopPropagation()">
<div class="btn-group btn-group-sm">
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.Id"
class="btn btn-outline-primary" title="View">
<i class="bi bi-eye"></i>
</a>
<a asp-controller="Jobs" asp-action="Edit" asp-route-id="@job.Id"
class="btn btn-outline-warning" title="Edit">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Jobs Pagination -->
@if (jobs.TotalPages > 1)
{
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-3 px-4">
<div class="text-muted small">
Showing @jobs.StartIndex&ndash;@jobs.EndIndex of @jobs.TotalCount jobs
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-1">
<span class="text-muted small me-1">Rows:</span>
@foreach (var sz in new[] { 10, 25, 50 })
{
var szUrl = Url.Action("Activity", new
{
id = customerId, activeTab = "jobs",
jobSort, jobDir, jobPage = 1, jobSize = sz,
quoteSort, quoteDir, quotePage = quotes.PageNumber, quoteSize = quotes.PageSize
});
<a href="@szUrl" class="btn btn-sm @(jobs.PageSize == sz ? "btn-primary" : "btn-outline-secondary")">@sz</a>
}
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item @(jobs.PageNumber == 1 ? "disabled" : "")">
<a class="page-link" href="@JobPageUrl(jobs.PageNumber - 1)">
<i class="bi bi-chevron-left"></i>
</a>
</li>
@for (var p = Math.Max(1, jobs.PageNumber - 2); p <= Math.Min(jobs.TotalPages, jobs.PageNumber + 2); p++)
{
<li class="page-item @(p == jobs.PageNumber ? "active" : "")">
<a class="page-link" href="@JobPageUrl(p)">@p</a>
</li>
}
<li class="page-item @(jobs.PageNumber == jobs.TotalPages ? "disabled" : "")">
<a class="page-link" href="@JobPageUrl(jobs.PageNumber + 1)">
<i class="bi bi-chevron-right"></i>
</a>
</li>
</ul>
</nav>
</div>
</div>
}
}
</div>
</div>
</div>
<!-- ===== QUOTES TAB ===== -->
<div class="tab-pane fade @(activeTab == "quotes" ? "show active" : "")" id="quotes-panel" role="tabpanel">
<div class="card border-0 shadow-sm border-top-0" style="border-top-left-radius:0; border-top-right-radius:0;">
<div class="card-body p-0">
@if (!quotes.Items.Any())
{
<div class="text-center py-5">
<i class="bi bi-file-text" style="font-size: 4rem; color: #d1d5db;"></i>
<h5 class="mt-3 text-muted">No quotes found</h5>
<p class="text-muted mb-4">This customer has no quotes yet</p>
<a asp-controller="Quotes" asp-action="Create" asp-route-customerId="@customerId" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Create First Quote
</a>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="ps-4">
<a href="@QuoteSortUrl("QuoteNumber")" class="text-decoration-none" style="color:inherit">
Quote # <i class="bi @QuoteSortIcon("QuoteNumber")"></i>
</a>
</th>
<th>
<a href="@QuoteSortUrl("Status")" class="text-decoration-none" style="color:inherit">
Status <i class="bi @QuoteSortIcon("Status")"></i>
</a>
</th>
<th>
<a href="@QuoteSortUrl("QuoteDate")" class="text-decoration-none" style="color:inherit">
Quote Date <i class="bi @QuoteSortIcon("QuoteDate")"></i>
</a>
</th>
<th>
<a href="@QuoteSortUrl("Expiration")" class="text-decoration-none" style="color:inherit">
Expires <i class="bi @QuoteSortIcon("Expiration")"></i>
</a>
</th>
<th>
<a href="@QuoteSortUrl("Total")" class="text-decoration-none" style="color:inherit">
Total <i class="bi @QuoteSortIcon("Total")"></i>
</a>
</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var quote in quotes.Items)
{
<tr style="cursor:pointer;" onclick="window.location.href='@Url.Action("Details", "Quotes", new { id = quote.Id })'">
<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:38px;height:38px;background:linear-gradient(135deg,#11998e 0%,#38ef7d 100%);color:white;">
<i class="bi bi-file-text"></i>
</div>
<span class="fw-semibold">@quote.QuoteNumber</span>
</div>
</td>
<td>
@{
var isExpired = quote.IsExpired;
}
@if (isExpired)
{
<span class="badge bg-danger bg-opacity-10 text-danger">Expired</span>
}
else
{
<span class="badge bg-@quote.StatusColorClass bg-opacity-10 text-@quote.StatusColorClass">
@quote.StatusDisplayName
</span>
}
</td>
<td>@quote.QuoteDate.ToString("MMM dd, yyyy")</td>
<td>
@if (quote.ExpirationDate.HasValue)
{
<span class="@(isExpired ? "text-danger fw-semibold" : "")">
@quote.ExpirationDate.Value.ToString("MMM dd, yyyy")
@if (isExpired) { <i class="bi bi-exclamation-triangle ms-1"></i> }
</span>
}
else
{
<span class="text-muted">&mdash;</span>
}
</td>
<td class="fw-semibold">@quote.Total.ToString("C")</td>
<td class="text-end pe-4" onclick="event.stopPropagation()">
<div class="btn-group btn-group-sm">
<a asp-controller="Quotes" asp-action="Details" asp-route-id="@quote.Id"
class="btn btn-outline-primary" title="View">
<i class="bi bi-eye"></i>
</a>
<a asp-controller="Quotes" asp-action="Edit" asp-route-id="@quote.Id"
class="btn btn-outline-warning" title="Edit">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Quotes Pagination -->
@if (quotes.TotalPages > 1)
{
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-3 px-4">
<div class="text-muted small">
Showing @quotes.StartIndex&ndash;@quotes.EndIndex of @quotes.TotalCount quotes
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-1">
<span class="text-muted small me-1">Rows:</span>
@foreach (var sz in new[] { 10, 25, 50 })
{
var szUrl = Url.Action("Activity", new
{
id = customerId, activeTab = "quotes",
jobSort, jobDir, jobPage = jobs.PageNumber, jobSize = jobs.PageSize,
quoteSort, quoteDir, quotePage = 1, quoteSize = sz
});
<a href="@szUrl" class="btn btn-sm @(quotes.PageSize == sz ? "btn-primary" : "btn-outline-secondary")">@sz</a>
}
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item @(quotes.PageNumber == 1 ? "disabled" : "")">
<a class="page-link" href="@QuotePageUrl(quotes.PageNumber - 1)">
<i class="bi bi-chevron-left"></i>
</a>
</li>
@for (var p = Math.Max(1, quotes.PageNumber - 2); p <= Math.Min(quotes.TotalPages, quotes.PageNumber + 2); p++)
{
<li class="page-item @(p == quotes.PageNumber ? "active" : "")">
<a class="page-link" href="@QuotePageUrl(p)">@p</a>
</li>
}
<li class="page-item @(quotes.PageNumber == quotes.TotalPages ? "disabled" : "")">
<a class="page-link" href="@QuotePageUrl(quotes.PageNumber + 1)">
<i class="bi bi-chevron-right"></i>
</a>
</li>
</ul>
</nav>
</div>
</div>
}
}
</div>
</div>
</div>
</div>