Files
PowderCoatingLogix/src/PowderCoating.Web/Views/AuditLog/Index.cshtml
T
spouliot 4ec55e7290 Restore all zeroed views + add bulk gift certificate creation
The HTML entity sweep script had a bug where it wrote empty files for any
view that contained no target Unicode characters, zeroing out 215 view files.
All views restored from the pre-sweep commit (cefdf3e).

Bulk gift certificate feature:
- BulkCreateGiftCertificateDto with Quantity (1-500), Amount, Reason, Expiry, Notes
- GenerateBulkGiftCertificatePdfAsync on IPdfService / PdfService: one Letter page
  per cert, reusing the same purple/gold branded ComposeGiftCertificateContent helper
- GiftCertificatesController: BulkCreate GET/POST, BulkResult GET, BulkDownloadPdf POST
- Views: BulkCreate.cshtml (form with live total preview), BulkResult.cshtml (table +
  Download All PDF button that POSTs cert IDs to avoid URL length limits)
- gift-certificate-bulk.js: live preview + spinner/disable on submit
- Index.cshtml: Bulk Create button added alongside New Certificate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:09:22 -04:00

230 lines
11 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.
@using PowderCoating.Core.Entities
@model List<AuditLog>
@section Styles {
<style>
[data-bs-theme="dark"] .card-header.bg-white { background-color: var(--bs-card-cap-bg) !important; }
</style>
}
@{
ViewData["Title"] = "Audit Log";
int page = ViewBag.Page;
int totalPages = ViewBag.TotalPages;
int totalCount = ViewBag.TotalCount;
int pageSize = ViewBag.PageSize;
string PageLink(int p) => Url.Action("Index", new {
search = ViewBag.Search, entityType = ViewBag.EntityType,
action = ViewBag.Action, companyId = ViewBag.CompanyId,
from = ViewBag.From, to = ViewBag.To,
page = p, pageSize
})!;
string BadgeClass(string action) => action switch {
"Created" => "bg-success",
"Updated" => "bg-primary",
"Deleted" => "bg-danger",
"Restored" => "bg-warning text-dark",
"ManualChange" => "bg-info",
"Login" => "bg-success",
"Login2FABypassed" => "bg-success",
"FailedLogin" => "bg-warning text-dark",
"LoginDenied" => "bg-warning text-dark",
"AccountLockedOut" => "bg-danger",
_ => "bg-secondary"
};
}
<div class="container-fluid py-3">
<div class="mb-2">
<a asp-controller="PlatformAdmin" asp-action="Observability" class="text-muted small text-decoration-none">
<i class="bi bi-arrow-left me-1"></i>Observability
</a>
</div>
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h4 class="mb-0"><i class="bi bi-shield-check me-2 text-primary"></i>Audit Log</h4>
<small class="text-muted">@totalCount.ToString("N0") entries</small>
</div>
</div>
@* Filters *@
<div class="card border-0 shadow-sm mb-3">
<div class="card-body py-2">
<form method="get" class="row g-2 align-items-end">
<div class="col-md-3">
<input name="search" value="@ViewBag.Search" class="form-control form-control-sm"
placeholder="User, entity name, ID…" />
</div>
<div class="col-md-2">
<select name="entityType" class="form-select form-select-sm">
<option value="">All entity types</option>
@foreach (var t in (List<string>)ViewBag.EntityTypes)
{
<option value="@t" selected="@(ViewBag.EntityType == t)">@t</option>
}
</select>
</div>
<div class="col-md-2">
<select name="action" class="form-select form-select-sm">
<option value="">All actions</option>
@foreach (var a in new[] { "Created", "Updated", "Deleted", "Restored", "ManualChange", "Login", "FailedLogin", "LoginDenied", "AccountLockedOut", "Login2FABypassed" })
{
<option value="@a" selected="@(ViewBag.Action == a)">@a</option>
}
</select>
</div>
<div class="col-md-2">
<select name="companyId" class="form-select form-select-sm">
<option value="">All companies</option>
@foreach (var c in (dynamic)ViewBag.Companies)
{
<option value="@c.Id" selected="@(ViewBag.CompanyId?.ToString() == c.Id.ToString())">@c.CompanyName</option>
}
</select>
</div>
<div class="col-md-1">
<input type="date" name="from" value="@ViewBag.From" class="form-control form-control-sm" title="From date" />
</div>
<div class="col-md-1">
<input type="date" name="to" value="@ViewBag.To" class="form-control form-control-sm" title="To date" />
</div>
<div class="col-md-1">
<button class="btn btn-sm btn-primary w-100">Filter</button>
</div>
<div class="col-auto">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary">Clear</a>
</div>
<input type="hidden" name="pageSize" value="@pageSize" />
</form>
</div>
</div>
@* Table *@
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 small">
<thead class="table-light">
<tr>
<th style="width:160px">Timestamp</th>
<th style="width:90px">Action</th>
<th>Entity</th>
<th>Description</th>
<th>User</th>
<th>Company</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr><td colspan="7" class="text-center text-muted py-5">No audit entries found.</td></tr>
}
@foreach (var log in Model)
{
<tr style="cursor:pointer" onclick="window.location='@Url.Action("Details", new { id = log.Id })'">
<td class="text-muted">@log.Timestamp.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm:ss")</td>
<td><span class="badge @BadgeClass(log.Action)">@log.Action</span></td>
<td>@log.EntityType <span class="text-muted">@log.EntityId</span></td>
<td>@log.EntityDescription</td>
<td>@log.UserName</td>
<td>@log.CompanyName</td>
<td onclick="event.stopPropagation()">
@if (log.OldValues != null || log.NewValues != null)
{
<a asp-action="Details" asp-route-id="@log.Id"
class="btn btn-xs btn-outline-secondary py-0 px-1">
<i class="bi bi-eye"></i>
</a>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Mobile card view — shown on screens < 992px (table-responsive hidden by mobile-cards.css) -->
<div class="mobile-card-view">
<div class="mobile-card-list">
@if (!Model.Any())
{
<div class="text-center text-muted py-5">No audit entries found.</div>
}
@foreach (var log in Model)
{
<a href="@Url.Action("Details", new { id = log.Id })" class="mobile-data-card text-decoration-none">
<div class="mobile-card-header">
<div class="mobile-card-icon bg-primary"><i class="bi bi-shield-check"></i></div>
<div class="mobile-card-title">
<h6><span class="badge @BadgeClass(log.Action)">@log.Action</span></h6>
<small class="text-muted">@log.Timestamp.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm")</small>
</div>
</div>
<div class="mobile-card-body">
<div class="mobile-card-row">
<span class="mobile-card-label">Entity</span>
<span class="mobile-card-value">@log.EntityType @log.EntityId</span>
</div>
@if (!string.IsNullOrEmpty(log.EntityDescription))
{
<div class="mobile-card-row">
<span class="mobile-card-label">Description</span>
<span class="mobile-card-value">@log.EntityDescription</span>
</div>
}
<div class="mobile-card-row">
<span class="mobile-card-label">User</span>
<span class="mobile-card-value">@log.UserName</span>
</div>
<div class="mobile-card-row">
<span class="mobile-card-label">Company</span>
<span class="mobile-card-value">@log.CompanyName</span>
</div>
</div>
<div class="mobile-card-footer">
<span class="btn btn-sm btn-outline-primary">View →</span>
</div>
</a>
}
</div>
</div>
@* Pagination *@
@if (totalPages > 1)
{
<div class="card-footer d-flex align-items-center justify-content-between py-2">
<small class="text-muted">
Showing @((page - 1) * pageSize + 1)@Math.Min(page * pageSize, totalCount) of @totalCount.ToString("N0")
</small>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item @(page == 1 ? "disabled" : "")">
<a class="page-link" href="@PageLink(page - 1)"><i class="bi bi-chevron-left"></i></a>
</li>
@for (int p = Math.Max(1, page - 2); p <= Math.Min(totalPages, page + 2); p++)
{
<li class="page-item @(p == page ? "active" : "")">
<a class="page-link" href="@PageLink(p)">@p</a>
</li>
}
<li class="page-item @(page == totalPages ? "disabled" : "")">
<a class="page-link" href="@PageLink(page + 1)"><i class="bi bi-chevron-right"></i></a>
</li>
</ul>
</nav>
<div>
<select class="form-select form-select-sm" style="width:auto"
onchange="window.location='@PageLink(1)'.replace('pageSize=@pageSize','pageSize='+this.value)">
@foreach (var ps in new[] { 25, 50, 100 })
{
<option value="@ps" selected="@(pageSize == ps)">@ps / page</option>
}
</select>
</div>
</div>
}
</div>
</div>