From be00af43fb3e1e7981d1812734a770a22fb881aa Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 10 Aug 2026 15:27:16 -0700 Subject: [PATCH 1/4] AB#26913: Send Bulk Emails - Initial Draft --- ...tificationsPermissionDefinitionProvider.cs | 4 + .../Permissions/NotificationsPermissions.cs | 1 + .../Localization/Notifications/en.json | 1 + .../Emails/IEmailLogsRepository.cs | 1 + .../Repositories/EmailLogsRepository.cs | 22 +- .../BulkEmailNotificationDto.cs | 28 +++ .../BulkEmailNotificationResultDto.cs | 13 ++ .../IBulkEmailNotificationAppService.cs | 12 ++ .../BulkEmailNotificationAppService.cs | 192 ++++++++++++++++++ .../Localization/GrantManager/en.json | 12 ++ .../Permissions/PermissionGrantsDataSeeder.cs | 1 + .../SendEmailNotificationModal.cshtml | 105 ++++++++++ .../SendEmailNotificationModal.cshtml.cs | 161 +++++++++++++++ .../SendEmailNotificationModal.css | 2 + .../SendEmailNotificationModal.js | 67 ++++++ .../SendEmailNotificationSummaryModal.cshtml | 50 +++++ ...endEmailNotificationSummaryModal.cshtml.cs | 51 +++++ .../BulkEmailNotificationViewModel.cs | 23 +++ .../EmailNotificationNoteViewModel.cs | 34 ++++ .../Shared/Components/ActionBar/ActionBar.cs | 4 + .../Components/ActionBar/Default.cshtml | 11 + .../Shared/Components/ActionBar/Default.js | 48 +++++ 22 files changed, 835 insertions(+), 8 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs index ce368255c9..b5a9b3fbe2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs @@ -20,6 +20,10 @@ public override void Define(IPermissionDefinitionContext context) NotificationsPermissions.Email.Send, L($"Permission:{NotificationsPermissions.Email.Send}")); + notificationsPermissions.AddChild( + NotificationsPermissions.Email.SendBulk, + L($"Permission:{NotificationsPermissions.Email.SendBulk}")); + notificationsPermissions.AddChild( NotificationsPermissions.Email.DeleteDraft, L($"Permission:{NotificationsPermissions.Email.DeleteDraft}")); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs index 20bd2b4ec1..b3ef9af03b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs @@ -11,6 +11,7 @@ public static class Email { public const string Default = "Notifications.Email"; public const string Send = "Notifications.Email.Send"; + public const string SendBulk = "Notifications.Email.SendBulk"; public const string DeleteDraft = "Notifications.Email.DeleteDraft"; public const string CancelScheduled = "Notifications.Email.CancelScheduled"; public const string Schedule = "Notifications.Email.Schedule"; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json index 2d5375f419..2ea5782562 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json @@ -10,6 +10,7 @@ "Permission:Notifications": "Notifications", "Permission:Notifications.Email": "Email", "Permission:Notifications.Email.Send": "Send Email for Individual Application", + "Permission:Notifications.Email.SendBulk": "Send Bulk Email Notification", "Permission:Notifications.Email.DeleteDraft": "Delete Draft Email for Individual Application", "Permission:Notifications.Email.CancelScheduled": "Cancel Scheduled Email for Individual Application", "Permission:Notifications.Email.Schedule": "Schedule Email for Individual Application", diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs index 8ba3f48bc8..af1128ae19 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs @@ -9,5 +9,6 @@ public interface IEmailLogsRepository : IRepository { Task GetByIdAsync(Guid id, bool includeDetails = false); Task> GetByApplicationIdAsync(Guid applicationId); + Task> GetByApplicationIdsAndStatusAsync(List applicationIds, string status); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs index ad5bafe3c1..a2f1181711 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs @@ -15,18 +15,24 @@ public class EmailLogsRepository : EfCoreRepository dbContextProvider) : base(dbContextProvider) { - } - + } + public async Task GetByIdAsync(Guid id, bool includeDetails = false) { var dbSet = await GetDbSetAsync(); return await dbSet.FirstOrDefaultAsync(s => s.Id == id); - } - - public async Task> GetByApplicationIdAsync(Guid applicationId) - { - var dbSet = await GetDbSetAsync(); - return await dbSet.Where(x => x.ApplicationId == applicationId).ToListAsync(); + } + + public async Task> GetByApplicationIdAsync(Guid applicationId) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.Where(x => x.ApplicationId == applicationId).ToListAsync(); + } + + public async Task> GetByApplicationIdsAndStatusAsync(List applicationIds, string status) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.Where(x => applicationIds.Contains(x.ApplicationId) && x.Status == status).ToListAsync(); } public override async Task> WithDetailsAsync() diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs new file mode 100644 index 0000000000..6ba92460d0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.GrantApplications +{ + public class BulkEmailNotificationDto + { + public BulkEmailNotificationDto() + { + ValidationMessages = []; + ReferenceNo = string.Empty; + ApplicantName = string.Empty; + FormName = string.Empty; + ApplicationStatus = string.Empty; + } + + public List ValidationMessages { get; set; } + public bool IsValid { get; set; } + + public Guid ApplicationId { get; set; } + public Guid? EmailId { get; set; } + public string? EmailSubject { get; set; } + public string ReferenceNo { get; set; } + public string ApplicantName { get; set; } + public string FormName { get; set; } + public string ApplicationStatus { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs new file mode 100644 index 0000000000..5f1e68a8f5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Unity.GrantManager.GrantApplications +{ + public class BulkEmailNotificationResultDto + { + public List Successes { get; set; } = []; + + [JsonProperty("failures")] + public List> Failures { get; set; } = []; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs new file mode 100644 index 0000000000..19e6a91606 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Unity.GrantManager.GrantApplications +{ + public interface IBulkEmailNotificationAppService + { + Task SendBulkEmailNotifications(List batchApplicationsToEmail); + Task> GetApplicationsForBulkEmail(Guid[] applicationGuids); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs new file mode 100644 index 0000000000..ba70226559 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs @@ -0,0 +1,192 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Notifications.Email; +using Unity.Modules.Shared.Utils; +using Unity.Notifications.Emails; +using Unity.Notifications.Permissions; +using Volo.Abp; + +namespace Unity.GrantManager.GrantApplications +{ + [Authorize(NotificationsPermissions.Email.SendBulk)] + public class BulkEmailNotificationAppService( + IApplicationRepository applicationRepository, + IEmailLogsRepository emailLogsRepository, + IEmailAppService emailAppService) : GrantManagerAppService, IBulkEmailNotificationAppService + { + /// + /// Get applications for bulk email with added draft validation information + /// + /// + /// + public async Task> GetApplicationsForBulkEmail(Guid[] applicationGuids) + { + var applications = await applicationRepository.GetListByIdsAsync(applicationGuids); + var draftEmails = await emailLogsRepository.GetByApplicationIdsAndStatusAsync([.. applicationGuids], EmailStatus.Draft); + var draftsByApplication = draftEmails.GroupBy(e => e.ApplicationId).ToDictionary(g => g.Key, g => g.ToList()); + + var applicationsForEmail = new List(); + foreach (var application in applications) + { + draftsByApplication.TryGetValue(application.Id, out var drafts); + applicationsForEmail.Add(MapBulkEmailNotification(application, drafts ?? [])); + } + + return applicationsForEmail; + } + + /// + /// Send bulk email notifications for the given batch of draft emails + /// + /// + /// + public async Task SendBulkEmailNotifications(List batchApplicationsToEmail) + { + var bulkEmailResult = new BulkEmailNotificationResultDto(); + + // Fail the whole batch up front if notifications are disabled, rather than reporting false successes + // for emails that SendAsync would silently drop (it always returns true after publishing the event). + if (!await FeatureChecker.IsEnabledAsync("Unity.Notifications")) + { + foreach (var applicationToEmail in batchApplicationsToEmail) + { + bulkEmailResult.Failures.Add(new KeyValuePair(applicationToEmail.ReferenceNo, "Email notifications are currently disabled.")); + } + return bulkEmailResult; + } + + // We send individually here so that a failure on one application does not block the rest of the batch + foreach (var applicationToEmail in batchApplicationsToEmail) + { + try + { + if (!applicationToEmail.EmailId.HasValue) + { + throw new UserFriendlyException("No draft email was found for this application."); + } + + // Re-fetch the draft fresh (defense-in-depth: it may have changed since the modal opened) + var draft = await emailLogsRepository.GetAsync(applicationToEmail.EmailId.Value); + if (draft.Status != EmailStatus.Draft) + { + throw new UserFriendlyException("This email is no longer a draft."); + } + + // The posted ApplicationId is client-controlled (hidden form field) — never trust it for + // authorization-relevant writes. Confirm it still matches the draft's real owning application + // and use the draft's own ApplicationId, not the posted one, when sending. + if (draft.ApplicationId != applicationToEmail.ApplicationId) + { + throw new UserFriendlyException("This draft no longer matches the selected application."); + } + + // Re-check the "exactly one draft" invariant fresh at send time: this endpoint can be reached + // directly (bypassing the modal's GetApplicationsForBulkEmail check), and another draft may + // have been created for this application after the modal was loaded. + var currentDrafts = await emailLogsRepository.GetByApplicationIdsAndStatusAsync([draft.ApplicationId], EmailStatus.Draft); + if (currentDrafts.Count != 1) + { + throw new UserFriendlyException("Multiple draft emails found for this application. Please retain only one draft before proceeding."); + } + + // A non-blank ToAddress can still parse to zero recipients (e.g. ";" or ",") — the same check + // the send pipeline itself uses. Catch that here instead of reporting a false success: SendAsync + // always returns true, but the handler silently drops emails with no parseable recipients. + if (draft.ToAddress.ParseEmailList() is not { Count: > 0 }) + { + throw new UserFriendlyException("Draft email is missing a To address. Please update the draft before proceeding."); + } + + await emailAppService.SendAsync(new CreateEmailDto + { + EmailId = draft.Id, + ApplicationId = draft.ApplicationId, + EmailTo = draft.ToAddress, + EmailFrom = draft.FromAddress, + EmailSubject = draft.Subject, + EmailBody = draft.Body, + EmailCC = draft.CC, + EmailBCC = draft.BCC, + EmailTemplateName = draft.TemplateName, + SendOnDateTime = draft.SendOnDateTime + }); + + bulkEmailResult.Successes.Add(applicationToEmail.ReferenceNo); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error sending bulk email notification for application with ID: {ApplicationId} and ReferenceNo: {ReferenceNo}", + applicationToEmail.ApplicationId, + applicationToEmail.ReferenceNo); + + bulkEmailResult.Failures.Add(new KeyValuePair(applicationToEmail.ReferenceNo, ex.Message)); + } + } + + return bulkEmailResult; + } + + /// + /// Map the application to a BulkEmailNotificationDto with validation messages based on its draft emails + /// + /// + /// + /// + private static BulkEmailNotificationDto MapBulkEmailNotification(Application application, List drafts) + { + var validationMessages = new List(); + Guid? emailId = null; + string? emailSubject = null; + + if (drafts.Count == 0) + { + validationMessages.Add("NO_DRAFT_FOUND"); + } + else if (drafts.Count > 1) + { + validationMessages.Add("MULTIPLE_DRAFTS_FOUND"); + } + else + { + var draft = drafts[0]; + emailId = draft.Id; + emailSubject = draft.Subject; + + if (string.IsNullOrWhiteSpace(draft.Subject)) + { + validationMessages.Add("MISSING_SUBJECT"); + } + if (draft.ToAddress.ParseEmailList() is not { Count: > 0 }) + { + validationMessages.Add("MISSING_TO_ADDRESS"); + } + if (string.IsNullOrWhiteSpace(draft.FromAddress)) + { + validationMessages.Add("MISSING_FROM_ADDRESS"); + } + if (string.IsNullOrWhiteSpace(draft.Body)) + { + validationMessages.Add("MISSING_BODY"); + } + } + + return new BulkEmailNotificationDto() + { + ApplicationId = application.Id, + EmailId = emailId, + EmailSubject = emailSubject, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.Applicant?.ApplicantName ?? string.Empty, + ApplicationStatus = application.ApplicationStatus.InternalStatus, + FormName = application.ApplicationForm?.ApplicationFormName ?? string.Empty, + ValidationMessages = validationMessages, + IsValid = validationMessages.Count == 0 + }; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 3f0b387506..231017ccba 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -89,6 +89,7 @@ "ApplicationList:StartAssessmentButton": "Start Assessment", "ApplicationList:CompleteAssessmentButton": "Complete Assessment", "ApplicationList:TagButton": "Tags", + "ApplicationList:SendEmailButton": "Send Email", "ApplicationList:ResyncSubmissionAttachmentsButton": "Resync", "ApplicationList:ManageTagButton": "Manage Tags", @@ -533,6 +534,17 @@ "ApplicationBatchApprovalRequest:InvalidApprovedAmount": "Invalid Approved Amount, it must be greater than 0.00", "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00", + "SendEmailNotificationRequest:Title": "Send Email Notification", + "SendEmailNotificationRequest:SubmitButtonText": "Send", + "SendEmailNotificationRequest:CancelButtonText": "Cancel", + "SendEmailNotificationRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk email. Please reduce the number to {0} or fewer", + "SendEmailNotificationRequest:NoDraftFound": "No draft email found for this application. Please create a draft email before proceeding.", + "SendEmailNotificationRequest:MultipleDraftsFound": "Multiple draft emails found for this application. Please retain only one draft before proceeding.", + "SendEmailNotificationRequest:MissingSubject": "Draft email is missing a Subject. Please update the draft before proceeding.", + "SendEmailNotificationRequest:MissingToAddress": "Draft email is missing a To address. Please update the draft before proceeding.", + "SendEmailNotificationRequest:MissingFromAddress": "Draft email is missing a From address. Please update the draft before proceeding.", + "SendEmailNotificationRequest:MissingBody": "Draft email is missing a Body. Please update the draft before proceeding.", + "ApplicationBatchPublishRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk status publishing. Please reduce the number to {0} or fewer", "ApplicationBatchPublishRequest:MinCountExceeded": "You have no items selected for bulk status publishing. Please close this prompt to continue", "ApplicationBatchPublishRequest:ConfirmationNote": "By confirming, the selected application statuses will be published and made visible to applicants in the portal.", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs index 8a83f33587..a839abf533 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -70,6 +70,7 @@ public PermissionGrantsDataSeeder(IPermissionDataSeeder permissionDataSeeder) public readonly List Notifications_CommonPermissions = [ NotificationsPermissions.Email.Default, NotificationsPermissions.Email.Send, + NotificationsPermissions.Email.SendBulk, NotificationsPermissions.Email.DeleteDraft ]; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml new file mode 100644 index 0000000000..524984aaff --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml @@ -0,0 +1,105 @@ +@page +@model Unity.GrantManager.Web.Pages.BulkEmailNotifications.SendEmailNotificationModalModel +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization + +@inject IStringLocalizer L + +@{ + Layout = null; +} + +
+ + + + @if (ViewData["Error"] != null) + { + + } + else + { + + + + + @for (var i = 0; i < Model.BulkEmailNotifications?.Count; i++) + { +
+ + +
+
@Model.BulkEmailNotifications[i].ReferenceNo
+
@Model.BulkEmailNotifications[i].ApplicantName
+
@string.Format("({0})", @Model.BulkEmailNotifications[i].FormName)
+
@Model.BulkEmailNotifications[i].ApplicationStatus
+
+
+ + + +
+ + + + + + + + + + + + + @for (var j = 0; j < Model.BulkEmailNotifications[i].Notes?.Count; j++) + { + + + @if (Model.BulkEmailNotifications[i].Notes[j].IsError) + { + Error + } + else + { + Note + } + @Model.BulkEmailNotifications[i].Notes[j].Description + + + } + +
+ } +
+
+
+ Error @Model.MaxBatchCountExceededError +
+ } +
+ + + + + + + +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs new file mode 100644 index 0000000000..300cb41a2d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs @@ -0,0 +1,161 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications; + +public class SendEmailNotificationModalModel(IBulkEmailNotificationAppService bulkEmailNotificationAppService, + ApplicationIdsCacheService cacheService) : AbpPageModel +{ + [BindProperty] + public List? BulkEmailNotifications { get; set; } + + [TempData] + public int ApplicationsCount { get; set; } + + [TempData] + public bool Invalid { get; set; } + + [TempData] + public int MaxBatchCount { get; set; } + + [TempData] + public string? MaxBatchCountExceededError { get; set; } + + [TempData] + public bool MaxBatchCountExceeded { get; set; } + + public async Task OnGetAsync(string cacheKey) + { + MaxBatchCount = BatchApprovalConsts.MaxBatchCount; + BulkEmailNotifications = []; + MaxBatchCountExceededError = L["SendEmailNotificationRequest:MaxCountExceeded", BatchApprovalConsts.MaxBatchCount.ToString()].Value; + + try + { + // Retrieve application IDs from distributed cache + var selectedApplicationIds = await cacheService.GetApplicationIdsAsync(cacheKey); + + if (selectedApplicationIds == null || selectedApplicationIds.Count == 0) + { + Logger.LogWarning("Cache key expired or invalid: {CacheKey}", cacheKey); + ViewData["Error"] = "The session has expired. Please try selecting applications again."; + Invalid = true; + return; + } + + Guid[] applicationGuids = selectedApplicationIds.ToArray(); + + // Clean up cache after retrieval (one-time use) + await cacheService.RemoveAsync(cacheKey); + + if (!ValidCount(applicationGuids)) + { + MaxBatchCountExceeded = true; + } + + if (applicationGuids.Length == 0) + { + return; + } + + var applications = await bulkEmailNotificationAppService.GetApplicationsForBulkEmail(applicationGuids); + + foreach (var application in applications) + { + var bulkEmailNotification = new BulkEmailNotificationViewModel + { + ApplicationId = application.ApplicationId, + EmailId = application.EmailId, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.ApplicantName, + EmailSubject = application.EmailSubject, + ApplicationStatus = application.ApplicationStatus, + FormName = application.FormName, + IsValid = application.IsValid + }; + + SetNotes(application, bulkEmailNotification); + BulkEmailNotifications.Add(bulkEmailNotification); + } + + Invalid = applications.Exists(s => !s.IsValid) || MaxBatchCountExceeded; + ApplicationsCount = applications.Count; + + Logger.LogInformation("Successfully loaded bulk email modal for {Count} applications", selectedApplicationIds.Count); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading bulk email modal"); + ViewData["Error"] = "An error occurred while loading the email form. Please try again."; + Invalid = true; + } + } + + private void SetNotes(BulkEmailNotificationDto application, BulkEmailNotificationViewModel bulkEmailNotification) + { + List notes = EmailNotificationNoteViewModel.CreateNotesList(localizer: L); + + foreach (var validation in application.ValidationMessages) + { + var index = notes.FindIndex(note => note.Key == validation); + if (index != -1) + { + notes[index] = new EmailNotificationNoteViewModel(validation, true, notes[index].Description, notes[index].IsError); + } + } + + bulkEmailNotification.Notes = notes; + } + + public async Task OnPostAsync() + { + try + { + if (BulkEmailNotifications == null) return NoContent(); + + var emailRequests = MapBulkEmailRequests(); + + var result = await bulkEmailNotificationAppService.SendBulkEmailNotifications(emailRequests); + + return new OkObjectResult(result); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error sending bulk email notifications"); + } + + return NoContent(); + } + + private List MapBulkEmailRequests() + { + var bulkEmailNotifications = new List(); + + foreach (var application in BulkEmailNotifications ?? []) + { + bulkEmailNotifications.Add(new BulkEmailNotificationDto() + { + ApplicationId = application.ApplicationId, + EmailId = application.EmailId, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.ApplicantName ?? string.Empty, + ValidationMessages = [] + }); + } + + return bulkEmailNotifications; + } + + private static bool ValidCount(Guid[] applicationGuids) + { + return applicationGuids.Length <= BatchApprovalConsts.MaxBatchCount; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css new file mode 100644 index 0000000000..cb52f03d8c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css @@ -0,0 +1,2 @@ +/* Row/summary styling (.batch-approval-container, .approval-details-header, .approval-note-prefix, etc.) + is shared with the Approve Applications modal via ApproveApplicationsModal.css, loaded in the same bundle. */ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js new file mode 100644 index 0000000000..d95a991b20 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js @@ -0,0 +1,67 @@ +function removeApplicationEmail(applicationId) { + $('#' + applicationId).remove(); + let applicationsCount = $('#ApplicationsCount').val(); + $('#ApplicationsCount').val(applicationsCount - 1); + runValidations(); +} + +function runValidations() { + let isValid = true; + let itemCount = 0; + + $('#bulkEmailNotificationForm input[name="BulkEmailNotifications.Index"]').each(function () { + itemCount++; + let index = $(this).val(); + let isValidField = $('#bulkEmailNotificationForm input[name="BulkEmailNotifications[' + index + '].IsValid"]').val(); + + if (isValidField.toLowerCase() !== 'true') { + isValid = false; + } + }); + + if (itemCount === 0) { + isValid = false; + } + + if (!validBatchCount()) { + isValid = false; + setMaxCountError(true); + } else { + setMaxCountError(false); + } + + if (isValid) { + enableBulkEmailSubmit(); + } else { + disableBulkEmailSubmit(); + } +} + +function setMaxCountError(visible) { + const summary = $('#batch-approval-summary'); + if (visible) { + summary.css('display', 'block'); + } else { + summary.css('display', 'none'); + } +} + +function validBatchCount() { + let applicationsCount = $('#ApplicationsCount').val(); + let maxBatchCount = $('#MaxBatchCount').val(); + return applicationsCount <= maxBatchCount; +} + +function enableBulkEmailSubmit() { + $("#sendEmailNotificationModal") + .find('#btnSubmitBulkEmail').prop("disabled", false); +} + +function disableBulkEmailSubmit() { + $("#sendEmailNotificationModal") + .find('#btnSubmitBulkEmail').prop("disabled", true); +} + +function closeEmailNotifications() { + $('#sendEmailNotificationModal').modal('hide'); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml new file mode 100644 index 0000000000..acbf2cc241 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml @@ -0,0 +1,50 @@ +@page +@model Unity.GrantManager.Web.Pages.BulkEmailNotifications.SendEmailNotificationSummaryModalModel +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@inject IStringLocalizer L + +@{ + Layout = null; +} + + + + Send Email Summary + + +
+ + + + + + + + @for (var i = 0; i < Model.BulkEmailNotificationResults?.Count; i++) + { + + + + + + } + +
Reference No:Status:
+ @if (Model.BulkEmailNotificationResults[i].IsSuccess) + { + + } + else + { + + } + + + + +
+
+
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs new file mode 100644 index 0000000000..414956f89a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Unity.GrantManager.GrantApplications; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications +{ + public class SendEmailNotificationSummaryModalModel : PageModel + { + [BindProperty] + public List? BulkEmailNotificationResults { get; set; } + + public void OnGet(string summaryJson) + { + var items = new List(); + + var result = JsonSerializer.Deserialize(summaryJson); + + foreach (var item in result?.Successes ?? []) + { + items.Add(new BulkEmailNotificationItemResult + { + ReferenceNo = item, + Message = "Success", + IsSuccess = true + }); + } + + foreach (var item in result?.Failures ?? []) + { + items.Add(new BulkEmailNotificationItemResult + { + ReferenceNo = item.Key, + Message = item.Value, + IsSuccess = false + }); + } + + BulkEmailNotificationResults = [.. items.OrderBy(s => s.ReferenceNo)]; + } + + public class BulkEmailNotificationItemResult + { + public string ReferenceNo { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public bool IsSuccess { get; set; } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs new file mode 100644 index 0000000000..bae102e278 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels +{ + public class BulkEmailNotificationViewModel + { + public BulkEmailNotificationViewModel() + { + Notes = []; + } + + public Guid ApplicationId { get; set; } + public Guid? EmailId { get; set; } + public string ReferenceNo { get; set; } = string.Empty; + public string? ApplicantName { get; set; } = string.Empty; + public string FormName { get; set; } = string.Empty; + public string ApplicationStatus { get; set; } = string.Empty; + public string? EmailSubject { get; set; } + public bool IsValid { get; set; } + public List Notes { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs new file mode 100644 index 0000000000..b4e4967383 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels +{ + public class EmailNotificationNoteViewModel + { + public EmailNotificationNoteViewModel(string key, bool active, string description, bool isError) + { + Key = key; + Active = active; + Description = description; + IsError = isError; + } + + public string Key { get; set; } + public bool Active { get; set; } + public string Description { get; set; } + public bool IsError { get; set; } + + public static List CreateNotesList(IStringLocalizer localizer) + { + return + [ + new("NO_DRAFT_FOUND", false, localizer.GetString("SendEmailNotificationRequest:NoDraftFound"), true), + new("MULTIPLE_DRAFTS_FOUND", false, localizer.GetString("SendEmailNotificationRequest:MultipleDraftsFound"), true), + new("MISSING_SUBJECT", false, localizer.GetString("SendEmailNotificationRequest:MissingSubject"), true), + new("MISSING_TO_ADDRESS", false, localizer.GetString("SendEmailNotificationRequest:MissingToAddress"), true), + new("MISSING_FROM_ADDRESS", false, localizer.GetString("SendEmailNotificationRequest:MissingFromAddress"), true), + new("MISSING_BODY", false, localizer.GetString("SendEmailNotificationRequest:MissingBody"), true) + ]; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs index 18a1e5edfc..90ac9b3f02 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs @@ -28,6 +28,8 @@ public override void ConfigureBundle(BundleConfigurationContext context) .AddIfNotContains("/Pages/BulkApprovals/ApproveApplicationsModal.css"); context.Files .AddIfNotContains("/Pages/BulkActions/BulkPublishApplications.css"); + context.Files + .AddIfNotContains("/Pages/BulkEmailNotifications/SendEmailNotificationModal.css"); } } @@ -52,6 +54,8 @@ public override void ConfigureBundle(BundleConfigurationContext context) .AddIfNotContains("/Pages/BulkApprovals/ApproveApplicationsModal.js"); context.Files .AddIfNotContains("/Pages/BulkActions/BulkPublishApplications.js"); + context.Files + .AddIfNotContains("/Pages/BulkEmailNotifications/SendEmailNotificationModal.js"); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml index 7843f10950..0e6c1d4114 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml @@ -4,6 +4,7 @@ @using Unity.GrantManager.Permissions; @using Unity.GrantManager.Payments; @using Unity.Modules.Shared; +@using Unity.Notifications.Permissions; @using Unity.Payments.Permissions; @using Volo.Abp.Authorization.Permissions; @using Volo.Abp.Features; @@ -97,6 +98,16 @@ button-type="Secondary" /> } + @if (await FeatureChecker.IsEnabledAsync("Unity.Notifications") + && await PermissionChecker.IsGrantedAsync(NotificationsPermissions.Email.SendBulk)) + { + + } + @if (await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature) && await PermissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.RequestPayment)) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js index f370959240..5d44c2d7e6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js @@ -16,6 +16,12 @@ $(function () { let approveApplicationsSummaryModal = new abp.ModalManager({ viewUrl: 'BulkApprovals/ApproveApplicationsSummaryModal' }); + let sendEmailNotificationModal = new abp.ModalManager({ + viewUrl: 'BulkEmailNotifications/SendEmailNotificationModal' + }); + let sendEmailNotificationSummaryModal = new abp.ModalManager({ + viewUrl: 'BulkEmailNotifications/SendEmailNotificationSummaryModal' + }); let tagApplicationModal = new abp.ModalManager({ viewUrl: 'ApplicationTags/ApplicationTagsSelectionModal', }); @@ -328,6 +334,48 @@ $(function () { }); //#endregion Batch Approval + //#region Send Email Notification + $('#sendEmailNotification').on("click", function () { + // Store application IDs in distributed cache to avoid URL length limits + unity.grantManager.applications.applicationBulkActions + .storeApplicationIds({ applicationIds: selectedApplicationIds }) + .then(function(response) { + // Open modal with cache key instead of application IDs array + sendEmailNotificationModal.open({ + cacheKey: response.cacheKey + }); + }) + .catch(function(error) { + abp.notify.error('Failed to prepare bulk email. Please try again.'); + console.error('Error storing application IDs:', error); + }); + }); + sendEmailNotificationModal.onResult(function (_, response) { + let transformedFailures = response.responseText.failures.map(failure => { + return { + Key: failure.key, + Value: failure.value + }; + }); + let successCount = response.responseText.successes.length; + if (successCount > 0) { + abp.notify.success( + successCount === 1 + ? 'The email has been successfully queued for delivery.' + : successCount + ' emails have been successfully queued for delivery.', + 'Send Email Notification' + ); + } + let summaryJson = JSON.stringify( + { + Successes: response.responseText.successes, + Failures: transformedFailures + }); + sendEmailNotificationSummaryModal.open({ summaryJson: summaryJson }); + PubSub.publish("refresh_application_list"); + }); + //#endregion Send Email Notification + //#region Selection Events PubSub.subscribe("select_application", (msg, data) => { selectedApplicationIds.push(data.id); From fa5768d258a4529af0309b5b6380126733cce35d Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 11 Aug 2026 11:10:34 -0700 Subject: [PATCH 2/4] AB#26913: Add Fields from Bulk Approval Modal --- .../BulkEmailNotificationDto.cs | 4 ++++ .../BulkEmailNotificationAppService.cs | 4 ++++ .../SendEmailNotificationModal.cshtml | 23 +++++++++++++++++++ .../SendEmailNotificationModal.cshtml.cs | 4 ++++ .../SendEmailNotificationModal.css | 10 ++++++++ .../SendEmailNotificationSummaryModal.cshtml | 10 ++++---- ...endEmailNotificationSummaryModal.cshtml.cs | 2 +- .../BulkEmailNotificationViewModel.cs | 14 +++++++++++ 8 files changed, 65 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs index 6ba92460d0..8c127ab1b2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs @@ -24,5 +24,9 @@ public BulkEmailNotificationDto() public string ApplicantName { get; set; } public string FormName { get; set; } public string ApplicationStatus { get; set; } + public decimal RequestedAmount { get; set; } + public decimal RecommendedAmount { get; set; } + public decimal ApprovedAmount { get; set; } + public DateTime? DecisionDate { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs index ba70226559..69b45ad2d9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs @@ -184,6 +184,10 @@ private static BulkEmailNotificationDto MapBulkEmailNotification(Application app ApplicantName = application.Applicant?.ApplicantName ?? string.Empty, ApplicationStatus = application.ApplicationStatus.InternalStatus, FormName = application.ApplicationForm?.ApplicationFormName ?? string.Empty, + RequestedAmount = application.RequestedAmount, + RecommendedAmount = application.RecommendedAmount, + ApprovedAmount = application.ApprovedAmount, + DecisionDate = application.FinalDecisionDate, ValidationMessages = validationMessages, IsValid = validationMessages.Count == 0 }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml index 524984aaff..cc98815d5f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml @@ -55,6 +55,18 @@ + + + + + + + + + + + + @@ -103,3 +115,14 @@ + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs index 300cb41a2d..a4a0fe962f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs @@ -79,6 +79,10 @@ public async Task OnGetAsync(string cacheKey) EmailSubject = application.EmailSubject, ApplicationStatus = application.ApplicationStatus, FormName = application.FormName, + RequestedAmount = application.RequestedAmount, + RecommendedAmount = application.RecommendedAmount, + ApprovedAmount = application.ApprovedAmount, + DecisionDate = application.DecisionDate, IsValid = application.IsValid }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css index cb52f03d8c..efd7f644b8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css @@ -1,2 +1,12 @@ /* Row/summary styling (.batch-approval-container, .approval-details-header, .approval-note-prefix, etc.) is shared with the Approve Applications modal via ApproveApplicationsModal.css, loaded in the same bundle. */ + +/* Wider Submission # column for the send-email summary table: the shared .col-20/.col-75 split from + ApproveApplicationsModal.css is too narrow for this table's smaller (default-size) modal. */ +.custom-table .col-35 { + width: 35%; +} + +.custom-table .col-60 { + width: 60%; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml index acbf2cc241..f1bc54e27f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml @@ -9,7 +9,7 @@ Layout = null; } - + Send Email Summary @@ -18,8 +18,8 @@ - - + + @for (var i = 0; i < Model.BulkEmailNotificationResults?.Count; i++) @@ -35,10 +35,10 @@ } - - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs index 414956f89a..5118e7b1ce 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs @@ -23,7 +23,7 @@ public void OnGet(string summaryJson) items.Add(new BulkEmailNotificationItemResult { ReferenceNo = item, - Message = "Success", + Message = "Queued for delivery", IsSuccess = true }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs index bae102e278..a99396dd3d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels { @@ -16,6 +17,19 @@ public BulkEmailNotificationViewModel() public string? ApplicantName { get; set; } = string.Empty; public string FormName { get; set; } = string.Empty; public string ApplicationStatus { get; set; } = string.Empty; + + [DisplayName("Requested Amount")] + public decimal RequestedAmount { get; set; } + + [DisplayName("Recommended Amount")] + public decimal RecommendedAmount { get; set; } + + [DisplayName("Approved Amount")] + public decimal ApprovedAmount { get; set; } + + [DisplayName("Decision Date")] + public DateTime? DecisionDate { get; set; } + public string? EmailSubject { get; set; } public bool IsValid { get; set; } public List Notes { get; set; } From b4ba5baf51e82374c61f33b650c88906c5a601d3 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 11 Aug 2026 19:20:48 -0700 Subject: [PATCH 3/4] AB#26913: Fix sonarqube issues --- .../BulkEmailNotifications/SendEmailNotificationModal.cshtml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml index cc98815d5f..bc1969c828 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml @@ -39,6 +39,7 @@ + onclick="closeEmailNotifications()" + onkeydown='if(event.key === "Enter" || event.key === " ") { closeEmailNotifications(); event.preventDefault(); }'> From 02475ee68b7bc495b4094e95d038639f52610542 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Tue, 11 Aug 2026 19:28:56 -0700 Subject: [PATCH 4/4] AB#26913: Fix sonarqube and CodeQL findings --- .../SendEmailNotificationModal.cshtml.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs index a4a0fe962f..cd80ac0ce0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs @@ -7,6 +7,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels; +using Unity.Modules.Shared.Utils; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications; @@ -45,7 +46,7 @@ public async Task OnGetAsync(string cacheKey) if (selectedApplicationIds == null || selectedApplicationIds.Count == 0) { - Logger.LogWarning("Cache key expired or invalid: {CacheKey}", cacheKey); + Logger.LogWarning("Cache key expired or invalid: {CacheKey}", cacheKey.SanitizeField()); ViewData["Error"] = "The session has expired. Please try selecting applications again."; Invalid = true; return;
Reference No:Status:Submission #:Status:
+ +