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>
235 lines
12 KiB
Plaintext
235 lines
12 KiB
Plaintext
@using PowderCoating.Core.Enums
|
||
@using PowderCoating.Web.Controllers
|
||
@model List<PlatformNotificationRow>
|
||
@{
|
||
ViewData["Title"] = "Platform Notifications";
|
||
int page = (int)ViewBag.Page;
|
||
int totalPages = (int)ViewBag.TotalPages;
|
||
}
|
||
|
||
@section Styles {
|
||
<style>
|
||
[data-bs-theme="dark"] a.text-dark { color: var(--bs-body-color) !important; }
|
||
[data-bs-theme="dark"] .badge.bg-light { background-color: var(--bs-secondary-bg) !important; color: var(--bs-body-color) !important; }
|
||
</style>
|
||
}
|
||
|
||
<div class="container-fluid py-3">
|
||
<div class="d-flex align-items-center gap-3 mb-3">
|
||
<h4 class="mb-0">
|
||
<i class="bi bi-bell me-2 text-primary"></i>Platform Notifications
|
||
</h4>
|
||
</div>
|
||
|
||
@* Summary *@
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-6 col-md-3">
|
||
<div class="card text-center border-0 shadow-sm">
|
||
<div class="card-body py-2">
|
||
<div class="fs-4 fw-bold">@ViewBag.TotalCount.ToString("N0")</div>
|
||
<div class="small text-muted">Total Sent</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-3">
|
||
<div class="card text-center border-0 shadow-sm border-danger">
|
||
<div class="card-body py-2">
|
||
<div class="fs-4 fw-bold text-danger">@ViewBag.FailedCount.ToString("N0")</div>
|
||
<div class="small text-muted">Failed</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-3">
|
||
<div class="card text-center border-0 shadow-sm">
|
||
<div class="card-body py-2">
|
||
<div class="fs-4 fw-bold text-info">@ViewBag.Last24hCount.ToString("N0")</div>
|
||
<div class="small text-muted">Last 24 Hours</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-3">
|
||
<div class="card text-center border-0 shadow-sm">
|
||
<div class="card-body py-2">
|
||
<div class="fs-4 fw-bold">@((int)ViewBag.FilteredCount)</div>
|
||
<div class="small text-muted">Filtered Results</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@* Filters *@
|
||
<div class="card shadow-sm mb-3">
|
||
<div class="card-body py-2">
|
||
<form method="get" class="row g-2 align-items-end">
|
||
<div class="col-12 col-md-2">
|
||
<label class="form-label small mb-1">Company</label>
|
||
<select name="companyId" class="form-select form-select-sm">
|
||
<option value="">All Companies</option>
|
||
@foreach (var c in (IEnumerable<dynamic>)ViewBag.Companies)
|
||
{
|
||
<option value="@c.Id" selected="@(ViewBag.CompanyIdFilter?.ToString() == c.Id.ToString())">@c.CompanyName</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<label class="form-label small mb-1">Type</label>
|
||
<select name="type" class="form-select form-select-sm">
|
||
<option value="">All Types</option>
|
||
@foreach (var t in Enum.GetValues<NotificationType>())
|
||
{
|
||
<option value="@t" selected="@(ViewBag.TypeFilter == t.ToString())">@t</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<label class="form-label small mb-1">Status</label>
|
||
<select name="status" class="form-select form-select-sm">
|
||
<option value="">All</option>
|
||
@foreach (var s in Enum.GetValues<NotificationStatus>())
|
||
{
|
||
<option value="@s" selected="@(ViewBag.StatusFilter == s.ToString())">@s</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<label class="form-label small mb-1">Channel</label>
|
||
<select name="channel" class="form-select form-select-sm">
|
||
<option value="">All</option>
|
||
@foreach (var ch in Enum.GetValues<NotificationChannel>())
|
||
{
|
||
<option value="@ch" selected="@(ViewBag.ChannelFilter == ch.ToString())">@ch</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<label class="form-label small mb-1">From</label>
|
||
<input type="date" name="from" class="form-control form-control-sm" value="@ViewBag.From" />
|
||
</div>
|
||
<div class="col-6 col-md-2">
|
||
<label class="form-label small mb-1">To</label>
|
||
<input type="date" name="to" class="form-control form-control-sm" value="@ViewBag.To" />
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<label class="form-label small mb-1">Search recipient / subject</label>
|
||
<input type="text" name="search" class="form-control form-control-sm" value="@ViewBag.Search" />
|
||
</div>
|
||
<div class="col-auto">
|
||
<button type="submit" class="btn btn-primary btn-sm">Filter</button>
|
||
<a href="@Url.Action("Index")" class="btn btn-outline-secondary btn-sm ms-1">Clear</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
@* Table *@
|
||
<div class="card shadow-sm">
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Sent</th>
|
||
<th>Company</th>
|
||
<th>Type</th>
|
||
<th>Channel</th>
|
||
<th>Recipient</th>
|
||
<th>Subject</th>
|
||
<th>Status</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@if (!Model.Any())
|
||
{
|
||
<tr><td colspan="8" class="text-center text-muted py-4">No notifications found.</td></tr>
|
||
}
|
||
@foreach (var row in Model)
|
||
{
|
||
var sc = row.Status == NotificationStatus.Sent ? "success"
|
||
: row.Status == NotificationStatus.Failed ? "danger"
|
||
: "secondary";
|
||
<tr>
|
||
<td class="small">@row.SentAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm")</td>
|
||
<td class="small">@(row.CompanyName ?? $"#{row.CompanyId}")</td>
|
||
<td><span class="badge bg-secondary-subtle text-body border small">@row.NotificationType</span></td>
|
||
<td class="small">@row.Channel</td>
|
||
<td class="small">@row.RecipientName<br><span class="text-muted">@row.Recipient</span></td>
|
||
<td class="small text-truncate" style="max-width:200px">@row.Subject</td>
|
||
<td><span class="badge bg-@sc">@row.Status</span></td>
|
||
<td>
|
||
<a asp-action="Details" asp-route-id="@row.Id" class="btn btn-outline-secondary btn-sm py-0">View</a>
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<!-- Mobile card view — shown on screens < 992px -->
|
||
<div class="mobile-card-view">
|
||
<div class="mobile-card-list">
|
||
@if (!Model.Any())
|
||
{
|
||
<p class="text-center text-muted py-4">No notifications found.</p>
|
||
}
|
||
@foreach (var row in Model)
|
||
{
|
||
var sc = row.Status == NotificationStatus.Sent ? "success"
|
||
: row.Status == NotificationStatus.Failed ? "danger"
|
||
: "secondary";
|
||
<a asp-action="Details" asp-route-id="@row.Id" class="mobile-data-card text-decoration-none">
|
||
<div class="mobile-card-header">
|
||
<div class="mobile-card-icon bg-primary"><i class="bi bi-bell"></i></div>
|
||
<div class="mobile-card-title">
|
||
<h6>@(row.Subject ?? row.NotificationType.ToString())</h6>
|
||
<small>@row.SentAt.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">Status</span>
|
||
<span class="mobile-card-value"><span class="badge bg-@sc">@row.Status</span></span>
|
||
</div>
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Company</span>
|
||
<span class="mobile-card-value">@(row.CompanyName ?? $"#{row.CompanyId}")</span>
|
||
</div>
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Recipient</span>
|
||
<span class="mobile-card-value">@(row.RecipientName ?? row.Recipient)</span>
|
||
</div>
|
||
<div class="mobile-card-row">
|
||
<span class="mobile-card-label">Channel</span>
|
||
<span class="mobile-card-value">@row.Channel</span>
|
||
</div>
|
||
</div>
|
||
<div class="mobile-card-footer">
|
||
<span class="btn btn-sm btn-outline-secondary">View →</span>
|
||
</div>
|
||
</a>
|
||
}
|
||
</div>
|
||
</div>
|
||
@if (totalPages > 1)
|
||
{
|
||
<div class="card-footer d-flex justify-content-between align-items-center py-2">
|
||
<span class="small text-muted">Page @(page) of @(totalPages)</span>
|
||
<nav>
|
||
<ul class="pagination pagination-sm mb-0">
|
||
@if (page > 1)
|
||
{
|
||
<li class="page-item">
|
||
<a class="page-link" href="@Url.Action("Index", new { page = page - 1, pageSize = ViewBag.PageSize, companyId = ViewBag.CompanyIdFilter, type = ViewBag.TypeFilter, status = ViewBag.StatusFilter, channel = ViewBag.ChannelFilter, search = ViewBag.Search, from = ViewBag.From, to = ViewBag.To })">‹</a>
|
||
</li>
|
||
}
|
||
@if (page < totalPages)
|
||
{
|
||
<li class="page-item">
|
||
<a class="page-link" href="@Url.Action("Index", new { page = page + 1, pageSize = ViewBag.PageSize, companyId = ViewBag.CompanyIdFilter, type = ViewBag.TypeFilter, status = ViewBag.StatusFilter, channel = ViewBag.ChannelFilter, search = ViewBag.Search, from = ViewBag.From, to = ViewBag.To })">›</a>
|
||
</li>
|
||
}
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|