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>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
@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 — 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 — 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>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user