-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@Html.Raw(Model.BodyHtml)
+
+
+
+
+
+
+
+ The email sends one company at a time to that company's Primary Contact Email.
+ Rich text is supported, and the preview step will render one merged sample before anything sends.
-
-
-
-
-
-
-
-
-
-
Hold Ctrl / Cmd to select multiple.
-
-
-
- @ViewBag.ActiveCount company email(s) will receive this message.
+
+
+
+
+
Available Merge Tokens
+
+
+
+
+
+
+
+
+
+
+
Click a token to insert it into the editor.
+
-
- @* Right: compose *@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- This will send a real email to the primary contact address of each matching company. Double-check your recipient selection before sending.
-
-
-
+
+
-
+
-
+
@section Scripts {
+
}
diff --git a/src/PowderCoating.Web/Views/EmailBroadcast/Preview.cshtml b/src/PowderCoating.Web/Views/EmailBroadcast/Preview.cshtml
new file mode 100644
index 0000000..f844f99
--- /dev/null
+++ b/src/PowderCoating.Web/Views/EmailBroadcast/Preview.cshtml
@@ -0,0 +1,135 @@
+@using PowderCoating.Web.Controllers
+@model AdminEmailPreviewModel
+@{
+ ViewData["Title"] = "Preview Admin Email";
+}
+
+
+
+
+
Admin Email Wizard
+
Step 3 of 3: preview one merged sample, then send sequentially.
+
+
+ @Model.EligibleCount ready to send
+ @if (Model.SkippedCount > 0)
+ {
+ @Model.SkippedCount missing email
+ }
+
+
+
+
+
+
+
+
+
Recipient Sample
+
+
@Model.SamplePreview.RecipientName
+
@Model.SamplePreview.RecipientEmail
+
@Model.SamplePreview.CompanyName
+
+
+
Rendered Subject
+
@Model.SamplePreview.RenderedSubject
+
+
Rendered HTML Body
+
+ @Html.Raw(Model.SamplePreview.RenderedHtmlBody)
+
+
+
+
+
+
+
+
+
+
+ The system will process each selected company one at a time.
+ The sample shown on the left uses the first available recipient after token replacement.
+
+
+
+
+
+
+
+
+
+
+ | Company |
+ Recipient |
+ Company Admin |
+ Ready |
+
+
+
+ @foreach (var row in Model.SelectedCompanies)
+ {
+
+ |
+ @row.CompanyName
+ #@row.CompanyId
+ |
+
+ @row.RecipientName
+ @(string.IsNullOrWhiteSpace(row.RecipientEmail) ? "No primary contact email configured" : row.RecipientEmail)
+ |
+
+ @(string.IsNullOrWhiteSpace(row.CompanyAdminName) ? "—" : row.CompanyAdminName)
+ @if (!string.IsNullOrWhiteSpace(row.CompanyAdminEmail))
+ {
+ @row.CompanyAdminEmail
+ }
+ |
+
+ @if (row.CanSend)
+ {
+ Ready
+ }
+ else
+ {
+ @row.SkipReason
+ }
+ |
+
+ }
+
+
+
+
+
+
+
+
+
diff --git a/src/PowderCoating.Web/Views/EmailBroadcast/SelectCompanies.cshtml b/src/PowderCoating.Web/Views/EmailBroadcast/SelectCompanies.cshtml
new file mode 100644
index 0000000..e942eb6
--- /dev/null
+++ b/src/PowderCoating.Web/Views/EmailBroadcast/SelectCompanies.cshtml
@@ -0,0 +1,170 @@
+@using PowderCoating.Web.Controllers
+@model AdminEmailSelectionModel
+@{
+ ViewData["Title"] = "Choose Companies";
+}
+
+
+
+
+
Admin Email Wizard
+
Step 2 of 3: choose which companies should receive this message.
+
+
@Model.AvailableCompanies.Count company records
+
+
+
+
+
+
+
Subject
+
@Model.Subject
+
+
+
Message Summary
+
Rich-text message prepared. Merge tokens will render on the preview step.
+
+
+
+
+
+
+
+
+@section Scripts {
+
+
+}
diff --git a/tests/PowderCoating.UnitTests/EmailBroadcastControllerTests.cs b/tests/PowderCoating.UnitTests/EmailBroadcastControllerTests.cs
new file mode 100644
index 0000000..3b3f06f
--- /dev/null
+++ b/tests/PowderCoating.UnitTests/EmailBroadcastControllerTests.cs
@@ -0,0 +1,203 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Moq;
+using PowderCoating.Application.Interfaces;
+using PowderCoating.Core.Entities;
+using PowderCoating.Core.Enums;
+using PowderCoating.Infrastructure.Data;
+using PowderCoating.Shared.Constants;
+using PowderCoating.Web.Controllers;
+
+namespace PowderCoating.UnitTests;
+
+public class EmailBroadcastControllerTests
+{
+ [Fact]
+ public async Task Preview_RendersMergedSampleAndSanitizesHtml()
+ {
+ await using var context = CreateContext();
+ context.Companies.Add(new Company
+ {
+ Id = 7,
+ CompanyId = 7,
+ CompanyName = "River City Powder",
+ PrimaryContactName = "Jamie Rivera",
+ PrimaryContactEmail = "jamie@example.com",
+ IsActive = true
+ });
+ context.Users.Add(new ApplicationUser
+ {
+ Id = "admin-7",
+ CompanyId = 7,
+ CompanyRole = AppConstants.CompanyRoles.CompanyAdmin,
+ FirstName = "Alex",
+ LastName = "Admin",
+ Email = "alex@example.com",
+ UserName = "alex@example.com",
+ IsActive = true
+ });
+ await context.SaveChangesAsync();
+
+ var controller = CreateController(context);
+
+ var result = await controller.Preview(new AdminEmailSelectionModel
+ {
+ Subject = "Update for {{CompanyName}}",
+ BodyHtml = "
Hi {{FirstName}}, contact {{CompanyAdminName}} at {{CompanyAdminEmail}}.
",
+ CompanyIds = [7]
+ });
+
+ var view = Assert.IsType
(result);
+ var model = Assert.IsType(view.Model);
+
+ Assert.Equal("Update for River City Powder", model.SamplePreview.RenderedSubject);
+ Assert.Contains("Hi Jamie", model.SamplePreview.RenderedHtmlBody);
+ Assert.Contains("Alex Admin", model.SamplePreview.RenderedHtmlBody);
+ Assert.Contains("alex@example.com", model.SamplePreview.RenderedHtmlBody);
+ Assert.DoesNotContain("