Files
PowderCoatingLogix/src/PowderCoating.Web/Views/CompanySettings/EditTemplate.cshtml
T
spouliot e4a256a6c4 Fix subscription expiry logic and HTML entities in page titles
Subscription expiry (SubscriptionExpiryBackgroundService):
- Trials with no grace period now go directly Active -> Expired instead
  of briefly entering GracePeriod for a day, which was causing repeated
  'Grace Period Started' admin notification emails
- Remove redundant isTrial variable (query already filters to non-Stripe
  companies, so all processed companies are trials by definition)
- Save per-company inside the loop so a single SaveChangesAsync failure
  no longer discards all other companies' status changes and notification
  log entries (which was the other cause of repeated emails)

HTML entities in page titles (33 views):
- Replace – / — with plain ' - ' in ViewData["Title"] C#
  strings; Razor HTML-encodes these when rendering @ViewData["Title"],
  causing browsers to display the literal text '–' instead of a dash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:58:37 -04:00

207 lines
9.8 KiB
Plaintext

@model PowderCoating.Application.DTOs.Notification.NotificationTemplateDto
@{
ViewData["Title"] = $"Edit Template - {Model.DisplayName}";
ViewData["PageIcon"] = "bi-envelope-gear";
var placeholders = ViewBag.Placeholders as List<(string Placeholder, string Description)>
?? new List<(string, string)>();
var isEmail = Model.IsEmail;
}
@section Styles {
<style>
.placeholder-pill {
cursor: pointer;
transition: background-color 0.15s;
font-family: monospace;
font-size: 0.85rem;
}
.placeholder-pill:hover { background-color: #0d6efd !important; color: white !important; }
.copy-feedback { display: none; font-size: 0.75rem; color: #198754; }
</style>
}
<div class="container-fluid">
<div class="d-flex justify-content-start mb-4">
<a asp-controller="CompanySettings" asp-action="NotificationTemplates" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left"></i>
</a>
</div>
<div class="row g-4">
<!-- LEFT: Edit form -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header d-flex align-items-center gap-2">
@if (isEmail)
{
<span class="badge bg-primary"><i class="bi bi-envelope"></i> Email</span>
}
else
{
<span class="badge bg-success"><i class="bi bi-phone"></i> SMS</span>
}
<span class="fw-semibold">@Model.NotificationType</span>
</div>
<div class="card-body">
<form asp-action="EditTemplate" asp-route-id="@Model.Id" method="post" id="templateForm">
@Html.AntiForgeryToken()
<input type="hidden" asp-for="Id" />
@if (isEmail)
{
<div class="mb-3">
<label asp-for="Subject" class="form-label fw-semibold">Subject</label>
<input asp-for="Subject" class="form-control" placeholder="Email subject line" />
<span asp-validation-for="Subject" class="text-danger small"></span>
</div>
}
<div class="mb-3">
<label class="form-label fw-semibold">
Body
@if (!isEmail)
{
<span class="text-muted fw-normal small ms-2">
<span id="charCount">@Model.Body.Length</span> characters
(<span id="segCount">@((Model.Body.Length / 160) + 1)</span> SMS segment@(((Model.Body.Length / 160) + 1) != 1 ? "s" : ""))
</span>
}
</label>
@if (isEmail)
{
<!-- Raw HTML textarea for email &mdash; supports {{placeholders}} and full HTML -->
<textarea asp-for="Body" class="form-control font-monospace" rows="16"
placeholder="Enter HTML email body..."></textarea>
<div class="form-text text-muted mt-1">
<i class="bi bi-code-slash me-1"></i>HTML is supported. Use <code>{{placeholder}}</code> tokens anywhere in the body.
</div>
}
else
{
<!-- Plain textarea for SMS -->
<textarea asp-for="Body" class="form-control" rows="5"
id="smsBody" maxlength="1000"
placeholder="Enter your SMS message text..."></textarea>
<span asp-validation-for="Body" class="text-danger small"></span>
<div class="form-text text-muted">
<i class="bi bi-info-circle"></i>
Standard SMS segments are 160 characters. Messages over 160 chars are split into multiple segments.
Always include <code>Reply STOP to opt out</code> for CTIA compliance.
</div>
}
</div>
<div class="d-flex justify-content-between align-items-center pt-2">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-floppy"></i> Save Template
</button>
<a asp-controller="CompanySettings" asp-action="NotificationTemplates"
class="btn btn-outline-secondary">
Cancel
</a>
</div>
<form asp-action="ResetTemplate" asp-route-id="@Model.Id" method="post"
onsubmit="return confirm('Reset this template to the built-in default? Your customisations will be lost.');">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-arrow-counterclockwise"></i> Reset to Default
</button>
</form>
</div>
</form>
</div>
</div>
</div>
<!-- RIGHT: Placeholder reference -->
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-braces"></i> Available Placeholders</h6>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Click any placeholder to copy it to your clipboard, then paste it into the template body.
</p>
<div class="d-flex flex-column gap-2">
@foreach (var (placeholder, description) in placeholders)
{
<div>
<span class="badge bg-light text-dark border placeholder-pill px-2 py-1"
onclick="copyPlaceholder('@placeholder', this)"
title="@description &mdash; click to copy">
@placeholder
</span>
<span class="copy-feedback ms-1">Copied!</span>
<div class="text-muted" style="font-size: 0.78rem;">@description</div>
</div>
}
</div>
</div>
</div>
<div class="card shadow-sm mt-3">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-lightbulb"></i> Tips</h6>
<ul class="small text-muted mb-0 ps-3">
<li>Placeholders are case-insensitive.</li>
<li>Unrecognised placeholders are left as-is.</li>
@if (isEmail)
{
<li>Edit raw HTML directly. A plain-text version is generated automatically for email clients that require it.</li>
<li>An unsubscribe link is always appended to comply with CAN-SPAM.</li>
}
else
{
<li>Keep messages under 160 characters to avoid splitting.</li>
<li>Always include opt-out instructions (e.g. "Reply STOP") to comply with CTIA guidelines.</li>
}
</ul>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@if (!isEmail)
{
<script>
const smsBody = document.getElementById('smsBody');
const charCount = document.getElementById('charCount');
const segCount = document.getElementById('segCount');
smsBody?.addEventListener('input', function () {
const len = this.value.length;
charCount.textContent = len;
const segs = Math.ceil(len / 160) || 1;
segCount.textContent = segs;
const suffixEl = segCount.nextSibling;
if (suffixEl) suffixEl.textContent = ` SMS segment${segs !== 1 ? 's' : ''}`;
});
</script>
}
<script>
function copyPlaceholder(text, el) {
navigator.clipboard.writeText(text).then(function () {
const feedback = el.nextElementSibling;
if (feedback) {
feedback.style.display = 'inline';
setTimeout(() => { feedback.style.display = 'none'; }, 1800);
}
}).catch(function () {
// Fallback for browsers that don't support clipboard API
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
}
</script>
}