a0bdd2b5b4
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>
165 lines
8.8 KiB
Plaintext
165 lines
8.8 KiB
Plaintext
@using PowderCoating.Core.Entities
|
|
@model List<Announcement>
|
|
@{
|
|
ViewData["Title"] = "Announcements";
|
|
|
|
string TypeBadge(string type) => type switch {
|
|
"success" => "bg-success",
|
|
"warning" => "bg-warning",
|
|
"danger" => "bg-danger",
|
|
_ => "bg-info"
|
|
};
|
|
}
|
|
|
|
@section Styles {
|
|
<style>
|
|
[data-bs-theme="dark"] a.text-dark { color: var(--bs-body-color) !important; }
|
|
</style>
|
|
}
|
|
|
|
<div class="container-fluid py-3">
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
<h4 class="mb-0"><i class="bi bi-megaphone me-2 text-primary"></i>Announcements</h4>
|
|
<a asp-action="Create" class="btn btn-primary">
|
|
<i class="bi bi-plus-lg me-1"></i>New Announcement
|
|
</a>
|
|
</div>
|
|
|
|
@if (TempData["Success"] != null)
|
|
{
|
|
<div class="alert alert-success alert-dismissible mb-3" role="alert">
|
|
@TempData["Success"]<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
}
|
|
|
|
<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>Title</th>
|
|
<th>Type</th>
|
|
<th>Target</th>
|
|
<th>Starts</th>
|
|
<th>Expires</th>
|
|
<th>Active</th>
|
|
<th>Dismissible</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (!Model.Any())
|
|
{
|
|
<tr><td colspan="8" class="text-center text-muted py-5">No announcements yet.</td></tr>
|
|
}
|
|
@foreach (var a in Model)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var isLive = a.IsActive && a.StartsAt <= now && (a.ExpiresAt == null || a.ExpiresAt > now);
|
|
<tr class="@(!a.IsActive ? "opacity-50" : "")" style="cursor:pointer"
|
|
onclick="window.location='@Url.Action("Edit", new { id = a.Id })'">
|
|
<td>
|
|
<div class="fw-medium">@a.Title</div>
|
|
<small class="text-muted">@a.Message.Substring(0, Math.Min(60, a.Message.Length))@(a.Message.Length > 60 ? "…" : "")</small>
|
|
</td>
|
|
<td><span class="badge @TypeBadge(a.Type)">@a.Type</span></td>
|
|
<td>
|
|
@if (a.Target == "All") { <span class="badge bg-secondary">All</span> }
|
|
else if (a.Target == "Plan") { <span class="badge bg-primary">Plan @a.TargetPlan</span> }
|
|
else { <span class="badge bg-warning">Co. #@a.TargetCompanyId</span> }
|
|
</td>
|
|
<td>@a.StartsAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm")</td>
|
|
<td>@(a.ExpiresAt.HasValue ? a.ExpiresAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm") : "Never")</td>
|
|
<td>
|
|
@if (isLive)
|
|
{ <span class="badge bg-success">Live</span> }
|
|
else if (!a.IsActive)
|
|
{ <span class="badge bg-secondary">Disabled</span> }
|
|
else if (a.ExpiresAt.HasValue && a.ExpiresAt < now)
|
|
{ <span class="badge bg-secondary">Expired</span> }
|
|
else
|
|
{ <span class="badge bg-warning">Scheduled</span> }
|
|
</td>
|
|
<td>
|
|
@(a.IsDismissible
|
|
? Html.Raw("<i class=\"bi bi-check-circle-fill text-success\"></i>")
|
|
: Html.Raw("<i class=\"bi bi-dash-circle text-muted\"></i>"))
|
|
</td>
|
|
<td class="text-end" onclick="event.stopPropagation()">
|
|
<a asp-action="Edit" asp-route-id="@a.Id"
|
|
class="btn btn-sm btn-outline-primary py-0 px-2 me-1">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<form method="post" asp-action="ResetDismissals" asp-route-id="@a.Id" class="d-inline"
|
|
onsubmit="return confirm('Reset all dismissals? The announcement will reappear for all users.')">
|
|
@Html.AntiForgeryToken()
|
|
<button class="btn btn-sm btn-outline-warning py-0 px-2" title="Reset dismissals">
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
</button>
|
|
</form>
|
|
<form method="post" asp-action="Delete" asp-route-id="@a.Id" class="d-inline"
|
|
onsubmit="return confirm('Delete this announcement?')">
|
|
@Html.AntiForgeryToken()
|
|
<button class="btn btn-sm btn-outline-danger py-0 px-2">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
</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 announcements yet.</p>
|
|
}
|
|
@foreach (var a in Model)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var isLive = a.IsActive && a.StartsAt <= now && (a.ExpiresAt == null || a.ExpiresAt > now);
|
|
string statusBadge, statusText;
|
|
if (isLive) { statusBadge = "bg-success"; statusText = "Live"; }
|
|
else if (!a.IsActive) { statusBadge = "bg-secondary"; statusText = "Disabled"; }
|
|
else if (a.ExpiresAt.HasValue && a.ExpiresAt < now) { statusBadge = "bg-secondary"; statusText = "Expired"; }
|
|
else { statusBadge = "bg-warning"; statusText = "Scheduled"; }
|
|
|
|
<a href="@Url.Action("Edit", new { id = a.Id })" class="mobile-data-card text-decoration-none">
|
|
<div class="mobile-card-header">
|
|
<div class="mobile-card-icon bg-primary"><i class="bi bi-megaphone"></i></div>
|
|
<div class="mobile-card-title">
|
|
<h6>@a.Title</h6>
|
|
<small>@a.StartsAt.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 @statusBadge">@statusText</span></span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Target</span>
|
|
<span class="mobile-card-value">
|
|
@if (a.Target == "All") { <span class="badge bg-secondary">All</span> }
|
|
else if (a.Target == "Plan") { <span class="badge bg-primary">Plan @a.TargetPlan</span> }
|
|
else { <span class="badge bg-warning">Co. #@a.TargetCompanyId</span> }
|
|
</span>
|
|
</div>
|
|
<div class="mobile-card-row">
|
|
<span class="mobile-card-label">Expires</span>
|
|
<span class="mobile-card-value">@(a.ExpiresAt.HasValue ? a.ExpiresAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy") : "Never")</span>
|
|
</div>
|
|
</div>
|
|
<div class="mobile-card-footer">
|
|
<span class="btn btn-sm btn-outline-primary">Edit →</span>
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|