From ec8a4141eacc222b87dd06ff7b9c60a19e065dc7 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 6 Jul 2026 16:25:47 -0700 Subject: [PATCH 01/23] AB#33530 initial commit for tenant details edit --- .../Applicants/ApplicantTenantDto.cs | 3 +- .../ApplicantPortalProgramDetailsDto.cs | 15 ++ .../IApplicationStatusService.cs | 2 + .../UpdateApplicantPortalProgramDetailsDto.cs | 18 ++ ...rantManagerPermissionDefinitionProvider.cs | 3 + .../Queries/ApplicantProfileQueryService.cs | 52 ++++- .../ApplicationStatusAppService.cs | 64 +++++- .../ApplicantPortalProgramDetailsConsts.cs | 9 + .../Localization/GrantManager/en.json | 10 + .../Permissions/GrantManagerPermissions.cs | 6 + .../Permissions/PermissionGrantsDataSeeder.cs | 2 +- .../Identity/PolicyRegistrant.cs | 3 +- .../CharacterCounter.js | 35 +++ .../ApplicantPortalSettings/Index.cshtml | 73 +++++- .../ApplicantPortalSettings/Index.cshtml.cs | 14 +- .../Pages/ApplicantPortalSettings/Index.css | 35 +++ .../Pages/ApplicantPortalSettings/Index.js | 208 ++++++++++++++---- 17 files changed, 496 insertions(+), 56 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantTenantDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantTenantDto.cs index e9e5b5c24a..efed16a462 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantTenantDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantTenantDto.cs @@ -7,6 +7,7 @@ public class ApplicantTenantDto { public Guid TenantId { get; set; } public string TenantName { get; set; } = string.Empty; - public Dictionary Metadata { get; set; } = []; + public string? DisplayName { get; set; } + public string? DefaultFromAddress { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs new file mode 100644 index 0000000000..9415fe0f88 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace Unity.GrantManager.GrantApplications; + +[Serializable] +public class ApplicantPortalProgramDetailsDto +{ + public string DisplayName { get; set; } = string.Empty; + + public string Division { get; set; } = string.Empty; + + public string Branch { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs index de39f48b06..548997362c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs @@ -8,5 +8,7 @@ public interface IApplicationStatusService : IApplicationService { Task> GetListAsync(); Task> GetApplicantPortalStatusListAsync(); + Task GetApplicantPortalProgramDetailsAsync(); Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatusExternalLabelsDto input); + Task UpdateApplicantPortalProgramDetailsAsync(UpdateApplicantPortalProgramDetailsDto input); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs new file mode 100644 index 0000000000..b5b955778c --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.GrantApplications; + +public class UpdateApplicantPortalProgramDetailsDto +{ + [StringLength(ApplicantPortalProgramDetailsConsts.MaxDisplayNameLength)] + public string? DisplayName { get; set; } + + [StringLength(ApplicantPortalProgramDetailsConsts.MaxDivisionLength)] + public string? Division { get; set; } + + [StringLength(ApplicantPortalProgramDetailsConsts.MaxBranchLength)] + public string? Branch { get; set; } + + [StringLength(ApplicantPortalProgramDetailsConsts.MaxDescriptionLength)] + public string? Description { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs index 53d584174d..3194972cf9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs @@ -20,6 +20,9 @@ public override void Define(IPermissionDefinitionContext context) grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.ApplicationForms.Default, L("Permission:GrantManagerManagement.ApplicationForms.Default")); + var applicantPortalPermissions = grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.ApplicantPortal.Default, L("Permission:GrantManagerManagement.ApplicantPortal.Default")); + applicantPortalPermissions.AddChild(GrantManagerPermissions.ApplicantPortal.EditProgramDetails, L("Permission:GrantManagerManagement.ApplicantPortal.EditProgramDetails")); + var notificationSchedulerPermissions = grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.NotificationScheduler.Default, L("Permission:GrantManagerManagement.NotificationScheduler.Default")); notificationSchedulerPermissions.AddChild(GrantManagerPermissions.NotificationScheduler.ManageSchedules, L("Permission:GrantManagerManagement.NotificationScheduler.ManageSchedules")); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs index 99bb33d239..0b1a6ad134 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs @@ -10,6 +10,7 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Volo.Abp.Settings; +using Volo.Abp.TenantManagement; namespace Unity.GrantManager.ApplicantProfile { @@ -22,6 +23,7 @@ namespace Unity.GrantManager.ApplicantProfile public class ApplicantProfileQueryService( ICurrentTenant currentTenant, IRepository applicantTenantMapRepository, + ITenantRepository tenantRepository, IEnumerable dataProviders, ISettingProvider settingProvider, ILogger logger) @@ -63,7 +65,7 @@ public async Task GetApplicantProfileAsync(ApplicantProfile /// /// The method extracts the username portion from the subject identifier in the request /// to match tenant mappings. This operation is asynchronous and queries the host database for relevant tenant - /// associations. + /// associations. Tenant display names are batch-loaded for efficiency, falling back to the tenant name if not set. /// An object containing applicant profile information, including the subject identifier used to locate tenant /// mappings. /// A list of objects representing the tenants linked to the applicant. The @@ -75,7 +77,7 @@ public async Task> GetApplicantTenantsAsync(ApplicantPr if (subUsername is null) return []; List mappings = []; - // Query the ApplicantTenantMaps table in the host database + // Query 1: Get all tenant mappings for the applicant using (currentTenant.Change(null)) { var queryable = await applicantTenantMapRepository.GetQueryableAsync(); @@ -89,6 +91,34 @@ public async Task> GetApplicantTenantsAsync(ApplicantPr .ToListAsync(); } + if (mappings.Count == 0) + { + return mappings; + } + + // Query 2: Batch-load all tenants by their IDs to get display names + using (currentTenant.Change(null)) + { + var tenantIds = mappings.Select(m => m.TenantId).ToHashSet(); + var allTenants = await tenantRepository.GetListAsync(); + var tenants = allTenants.Where(t => tenantIds.Contains(t.Id)).ToList(); + + // Create a map of tenant ID to DisplayName (with fallback to Name) + var tenantDisplayNames = tenants.ToDictionary( + t => t.Id, + t => GetTenantDisplayName(t) + ); + + // Populate DisplayName for each mapping + foreach (var mapping in mappings) + { + if (tenantDisplayNames.TryGetValue(mapping.TenantId, out var displayName)) + { + mapping.DisplayName = displayName; + } + } + } + // Apply tenant specific metadata foreach (var map in mappings) { @@ -107,8 +137,24 @@ private async Task AddTenantMetadataAsync(ApplicantTenantDto tenantMap) using (currentTenant.Change(tenantMap.TenantId)) { var defaultEmailAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); - tenantMap.Metadata[ApplicantTenantMetadataKeys.DefaultFromAddress] = defaultEmailAddress ?? "NoReply@gov.bc.ca"; + tenantMap.DefaultFromAddress = defaultEmailAddress ?? "NoReply@gov.bc.ca"; + } + } + + /// + /// Extracts the DisplayName from the tenant's extra properties, falling back to the tenant's Name. + /// + private static string GetTenantDisplayName(Volo.Abp.TenantManagement.Tenant tenant) + { + if (tenant.ExtraProperties.TryGetValue("DisplayName", out var displayNameObj)) + { + var displayName = displayNameObj?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(displayName)) + { + return displayName; + } } + return tenant.Name; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs index 9fd421dae6..6ed8427b8a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs @@ -1,10 +1,16 @@ using Microsoft.AspNetCore.Authorization; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.Applications; +using Unity.GrantManager.Permissions; +using Volo.Abp.Authorization; using Volo.Abp.Application.Services; +using Volo.Abp.Data; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.TenantManagement; namespace Unity.GrantManager.GrantApplications; @@ -14,9 +20,14 @@ namespace Unity.GrantManager.GrantApplications; public class ApplicationStatusAppService : ApplicationService, IApplicationStatusService { private readonly IApplicationStatusRepository _applicationStatusRepository; - public ApplicationStatusAppService(IApplicationStatusRepository repository) + private readonly ITenantRepository _tenantRepository; + + public ApplicationStatusAppService( + IApplicationStatusRepository repository, + ITenantRepository tenantRepository) { _applicationStatusRepository = repository; + _tenantRepository = tenantRepository; } public virtual async Task> GetListAsync() @@ -33,6 +44,20 @@ public virtual async Task> GetApplicantPortalSta return ObjectMapper.Map, List>(statuses.OrderBy(s => s.StatusCode).ToList()); } + [Authorize(GrantManagerPermissions.ApplicantPortal.EditProgramDetails)] + public virtual async Task GetApplicantPortalProgramDetailsAsync() + { + var tenant = await GetCurrentTenantForProgramDetailsAsync(); + + return new ApplicantPortalProgramDetailsDto + { + DisplayName = GetExtraPropertyValue(tenant, "DisplayName"), + Division = GetExtraPropertyValue(tenant, "Division"), + Branch = GetExtraPropertyValue(tenant, "Branch"), + Description = GetExtraPropertyValue(tenant, "Description") + }; + } + public virtual async Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatusExternalLabelsDto input) { // Load all statuses in a single query by IDs @@ -50,4 +75,41 @@ public virtual async Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatu } } } + + [Authorize(GrantManagerPermissions.ApplicantPortal.EditProgramDetails)] + public virtual async Task UpdateApplicantPortalProgramDetailsAsync(UpdateApplicantPortalProgramDetailsDto input) + { + var tenant = await GetCurrentTenantForProgramDetailsAsync(); + + tenant.SetProperty("DisplayName", NormalizeProgramDetail(input.DisplayName)); + tenant.SetProperty("Division", NormalizeProgramDetail(input.Division)); + tenant.SetProperty("Branch", NormalizeProgramDetail(input.Branch)); + tenant.SetProperty("Description", NormalizeProgramDetail(input.Description)); + + await _tenantRepository.UpdateAsync(tenant); + } + + protected virtual async Task GetCurrentTenantForProgramDetailsAsync() + { + if (!CurrentTenant.Id.HasValue) + { + throw new AbpAuthorizationException("Program details can only be managed within a tenant context."); + } + + var tenantId = CurrentTenant.Id.Value; + using (CurrentTenant.Change(null)) + { + return await _tenantRepository.FindAsync(tenantId) ?? throw new EntityNotFoundException(typeof(Tenant), tenantId); + } + } + + protected virtual string GetExtraPropertyValue(Tenant tenant, string propertyName) + { + return tenant.GetProperty(propertyName)?.ToString() ?? string.Empty; + } + + protected virtual string NormalizeProgramDetail(string? value) + { + return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs new file mode 100644 index 0000000000..38588646a8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs @@ -0,0 +1,9 @@ +namespace Unity.GrantManager.GrantApplications; + +public static class ApplicantPortalProgramDetailsConsts +{ + public const int MaxDisplayNameLength = 120; + public const int MaxDivisionLength = 120; + public const int MaxBranchLength = 120; + public const int MaxDescriptionLength = 500; +} 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 392682daea..7269d66f91 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 @@ -163,6 +163,8 @@ "Permission:GrantApplicationManagement.Organizations.ManageProfiles": "Manage Organization Profiles", "Permission:GrantManagerManagement.Intakes.Default": "Manage Intakes", "Permission:GrantManagerManagement.ApplicationForms.Default": "Manage Forms", + "Permission:GrantManagerManagement.ApplicantPortal.Default": "Applicant Portal", + "Permission:GrantManagerManagement.ApplicantPortal.EditProgramDetails": "Edit Program Details", "Permission:GrantManagerManagement.NotificationScheduler.Default": "Notification Scheduler", "Permission:GrantManagerManagement.NotificationScheduler.ManageSchedules": "Manage Notification Schedules", "Permission:GrantApplicationManagement.Approvals.BulkApplicationApproval": "Bulk Application Approval", @@ -521,16 +523,24 @@ "ApplicantPortalSettings:Title": "Applicant Portal Configuration", "ApplicantPortalSettings:ManageStatuses": "Manage Statuses", + "ApplicantPortalSettings:ProgramDetails": "Program Details", "ApplicantPortalSettings:PortalStatusHeading": "Applicant Portal Status", + "ApplicantPortalSettings:ProgramDetailsHeading": "Program Details", "ApplicantPortalSettings:InternalStatus": "Internal Status", "ApplicantPortalSettings:PortalStatusLabel": "External Status", "ApplicantPortalSettings:PortalNotifiedStatusLabel": "Notified Status", + "ApplicantPortalSettings:DisplayName": "Display Name", + "ApplicantPortalSettings:Division": "Division", + "ApplicantPortalSettings:Branch": "Branch", + "ApplicantPortalSettings:Description": "Description", "ApplicantPortalSettings:SaveChanges": "Save Changes", "ApplicantPortalSettings:ResetChanges": "Reset", "ApplicantPortalSettings:NoChanges": "No changes to save.", "ApplicantPortalSettings:ChangesReset": "Changes have been reset to original values.", "ApplicantPortalSettings:SaveSuccess": "Portal status labels updated successfully.", + "ApplicantPortalSettings:ProgramDetailsSaveSuccess": "Program details updated successfully.", "ApplicantPortalSettings:SaveError": "An error occurred while saving portal status labels.", + "ApplicantPortalSettings:ProgramDetailsSaveError": "An error occurred while saving program details.", "ApplicantPortalSettings:ValidationRequired": "Portal status label cannot be empty.", "Common:Command:Cancel": "Cancel", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs index 31c447af0c..ba2ac63b24 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs @@ -23,6 +23,12 @@ public static class ApplicationForms { public const string Default = GroupName + ".ApplicationForms"; } + + public static class ApplicantPortal + { + public const string Default = GroupName + ".ApplicantPortal"; + public const string EditProgramDetails = Default + ".EditProgramDetails"; + } public static class Endpoints { 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 833ac0b40c..6a2f1a381e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -132,6 +132,7 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, IdentitySeedPermissions.Roles.ManagePermissions, GrantManagerPermissions.Intakes.Default, GrantManagerPermissions.ApplicationForms.Default, + GrantManagerPermissions.ApplicantPortal.EditProgramDetails, .. SettingManagement_Tags_CommonPermissions, .. ReviewAndAssessment_CommonPermissions, @@ -345,4 +346,3 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, } } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs index 777d5f6066..01efba0ef7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs @@ -53,6 +53,8 @@ internal static void Register(ServiceConfigurationContext context) policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.Intakes.Default)); authorizationBuilder.AddPolicy(GrantManagerPermissions.ApplicationForms.Default, policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.ApplicationForms.Default)); + authorizationBuilder.AddPolicy(GrantManagerPermissions.ApplicantPortal.EditProgramDetails, + policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.ApplicantPortal.EditProgramDetails)); // Grant Application Policies authorizationBuilder.AddPolicy(GrantApplicationPermissions.Applications.Default, @@ -269,4 +271,3 @@ internal static void Register(ServiceConfigurationContext context) policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Delete)); } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js new file mode 100644 index 0000000000..746595e1e6 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js @@ -0,0 +1,35 @@ +(function ($) { + window.UnityCharacterCounter = { + init: function (containerSelector) { + const $container = $(containerSelector || document); + + $container.find('[data-char-count]').each(function () { + const $input = $(this); + const maxLength = parseInt($input.attr('data-char-count'), 10); + + if (isNaN(maxLength)) { + return; + } + + // Create wrapper with relative positioning + const $wrapper = $('
'); + $input.wrap($wrapper); + + const $counter = $('
') + .insertAfter($input); + + function updateCounter() { + const currentLength = $input.val().length; + $counter.text(currentLength + '/' + maxLength); + + const percentageUsed = (currentLength / maxLength) * 100; + $counter.toggleClass('char-counter-warning', percentageUsed >= 80); + $counter.toggleClass('char-counter-critical', percentageUsed >= 95); + } + + $input.on('input', updateCounter); + updateCounter(); + }); + } + }; +})(jQuery); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml index d9c8912392..32d9371935 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml @@ -18,6 +18,7 @@ } @section scripts { + } @@ -30,10 +31,16 @@ + @if (Model.CanEditProgramDetails) + { + + }
-
+
@L["ApplicantPortalSettings:PortalStatusHeading"]
@@ -95,5 +102,69 @@
+ + @if (Model.CanEditProgramDetails) + { +
+
+
+
@L["ApplicantPortalSettings:ProgramDetailsHeading"]
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ }
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs index e51c79beb1..517e6ebe33 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs @@ -2,16 +2,28 @@ using System.Collections.Generic; using System.Threading.Tasks; using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.Permissions; +using Volo.Abp.Authorization.Permissions; namespace Unity.GrantManager.Web.Pages.ApplicantPortalSettings; [Authorize] -public class IndexModel(IApplicationStatusService applicationStatusService) : GrantManagerPageModel +public class IndexModel( + IApplicationStatusService applicationStatusService, + IPermissionChecker permissionChecker) : GrantManagerPageModel { public IList Statuses { get; set; } = []; + public bool CanEditProgramDetails { get; set; } + public ApplicantPortalProgramDetailsDto ProgramDetails { get; set; } = new(); public async Task OnGetAsync() { Statuses = await applicationStatusService.GetApplicantPortalStatusListAsync(); + CanEditProgramDetails = await permissionChecker.IsGrantedAsync(GrantManagerPermissions.ApplicantPortal.EditProgramDetails); + + if (CanEditProgramDetails) + { + ProgramDetails = await applicationStatusService.GetApplicantPortalProgramDetailsAsync(); + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css index 4800e83354..6c17985f7b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css @@ -33,6 +33,14 @@ padding: 1rem; } +.portal-settings-config-panel { + display: none; +} + +.portal-settings-config-panel.active { + display: block; +} + .portal-settings-actions { background: #fff; } @@ -61,4 +69,31 @@ padding: 0.75rem 1rem; display: flex; margin-top: 1px; +} + +.char-counter-wrapper { + position: relative; + display: block; +} + +.char-counter { + position: absolute; + top: 0; + right: 8px; + font-size: 0.65rem; + font-style: italic; + color: #6c757d; + white-space: nowrap; + padding: 4px 0; + pointer-events: none; +} + +.char-counter-warning { + color: #ffc107; + font-weight: 500; +} + +.char-counter-critical { + color: #dc3545; + font-weight: 500; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js index 38be4cbc7b..79285ed529 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js @@ -1,59 +1,79 @@ (function ($) { - const l = abp.localization.getResource('GrantManager'); - const $form = $('#PortalStatusForm'); - const $saveButton = $('#SaveButton'); - const $resetButton = $('#ResetButton'); + const $manageStatusesMenuItem = $('#manage-statuses-menu-item'); + const $programDetailsMenuItem = $('#program-details-menu-item'); + const $portalStatusPanel = $('#portal-status-div'); + const $programDetailsPanel = $('#program-details-div'); + + function setActiveSection(sectionName) { + const isManageStatuses = sectionName === 'manage-statuses'; - // Store original values on page load - const originalValues = new Map(); + $manageStatusesMenuItem.toggleClass('active', isManageStatuses); + $portalStatusPanel.toggleClass('active', isManageStatuses).toggleClass('d-none', !isManageStatuses); + + if ($programDetailsMenuItem.length && $programDetailsPanel.length) { + $programDetailsMenuItem.toggleClass('active', !isManageStatuses); + $programDetailsPanel.toggleClass('active', !isManageStatuses).toggleClass('d-none', isManageStatuses); + } + } + + $manageStatusesMenuItem.on('click', function () { + setActiveSection('manage-statuses'); + }); - let portalStatusTable = new DataTable("#PortalStatusTable", { + $programDetailsMenuItem.on('click', function () { + setActiveSection('program-details'); + }); + + setActiveSection('manage-statuses'); + + const $statusForm = $('#PortalStatusForm'); + const $statusSaveButton = $('#SaveButton'); + const $statusResetButton = $('#ResetButton'); + const originalStatusValues = new Map(); + + const portalStatusTable = new DataTable('#PortalStatusTable', { paging: false, info: false, order: [[0, 'asc']], - "columnDefs": [{ + columnDefs: [{ targets: [1, 2], - orderable: false, + orderable: false }] }); - // Capture original values after DataTable initialization - // Use DataTable's rows().every() to ensure all rows are captured, even if dynamically loaded portalStatusTable.rows().every(function () { const $row = $(this.node()); const id = $row.find('input[type="hidden"]').val(); const externalStatus = $row.find('input[id$=".ExternalStatus"]').val().trim(); const notifiedStatus = $row.find('input[id$=".NotifiedStatus"]').val().trim(); - originalValues.set(id, { externalStatus, notifiedStatus }); + originalStatusValues.set(id, { externalStatus: externalStatus, notifiedStatus: notifiedStatus }); }); - // Check if any values have changed - function hasChanges() { + function hasStatusChanges() { let changed = false; portalStatusTable.$('tbody tr').each(function () { const $row = $(this); const id = $row.find('input[type="hidden"]').val(); const externalStatus = $row.find('input[id$=".ExternalStatus"]').val().trim(); const notifiedStatus = $row.find('input[id$=".NotifiedStatus"]').val().trim(); - const originalValue = originalValues.get(id); + const originalValue = originalStatusValues.get(id); if (!originalValue || originalValue.externalStatus !== externalStatus || originalValue.notifiedStatus !== notifiedStatus) { changed = true; return false; } }); + return changed; } - // Update button states - function updateButtonStates() { - const changed = hasChanges(); - $saveButton.prop('disabled', !changed); - $resetButton.prop('disabled', !changed); + function updateStatusButtonStates() { + const changed = hasStatusChanges(); + $statusSaveButton.prop('disabled', !changed); + $statusResetButton.prop('disabled', !changed); } - // Debounce utility to limit how often updateButtonStates is called function debounce(func, wait) { let timeout; return function () { @@ -62,34 +82,30 @@ }; } - // Debounced version of updateButtonStates - const debouncedUpdateButtonStates = debounce(updateButtonStates, 150); + const debouncedStatusButtonUpdate = debounce(updateStatusButtonStates, 150); - // Listen for input changes, using debounced handler - $form.on('input', '#PortalStatusTable input[type="text"]', function () { - debouncedUpdateButtonStates(); + $statusForm.on('input', '#PortalStatusTable input[type="text"]', function () { + debouncedStatusButtonUpdate(); }); - // Reset button handler - $resetButton.on('click', function (e) { + $statusResetButton.on('click', function (e) { e.preventDefault(); portalStatusTable.$('tbody tr').each(function () { const $row = $(this); const id = $row.find('input[type="hidden"]').val(); - const originalValue = originalValues.get(id); + const originalValue = originalStatusValues.get(id); $row.find('input[id$=".ExternalStatus"]').val(originalValue.externalStatus); $row.find('input[id$=".NotifiedStatus"]').val(originalValue.notifiedStatus); }); - updateButtonStates(); + updateStatusButtonStates(); abp.notify.info(l('ApplicantPortalSettings:ChangesReset')); }); - // Initialize button states - updateButtonStates(); + updateStatusButtonStates(); - $form.on('submit', function (e) { + $statusForm.on('submit', function (e) { e.preventDefault(); const statuses = []; @@ -98,7 +114,7 @@ portalStatusTable.$('tbody tr').each(function () { const $row = $(this); const id = $row.find('input[type="hidden"]').val(); - const externalStatus = $row.find('input[type="text"]').val().trim(); + const externalStatus = $row.find('input[id$=".ExternalStatus"]').val().trim(); if (!externalStatus) { abp.notify.warn(l('ApplicantPortalSettings:ValidationRequired')); @@ -106,9 +122,8 @@ return false; } - // Only include if value has changed const notifiedStatus = $row.find('input[id$=".NotifiedStatus"]').val().trim(); - const originalValue = originalValues.get(id); + const originalValue = originalStatusValues.get(id); if (originalValue.externalStatus !== externalStatus || originalValue.notifiedStatus !== notifiedStatus) { statuses.push({ id: id, @@ -122,33 +137,132 @@ return; } - // Check if there are any changes if (statuses.length === 0) { abp.notify.info(l('ApplicantPortalSettings:NoChanges')); return; } - abp.ui.setBusy($form); - + abp.ui.setBusy($statusForm); unity.grantManager.grantApplications.applicationStatus .updateExternalStatusLabels({ statuses: statuses }) .then(function () { abp.notify.success(l('ApplicantPortalSettings:SaveSuccess')); - - // Update original values after successful save - statuses.forEach(function(status) { - originalValues.set(status.id, { externalStatus: status.externalStatus, notifiedStatus: status.notifiedStatus }); + statuses.forEach(function (status) { + originalStatusValues.set(status.id, { + externalStatus: status.externalStatus, + notifiedStatus: status.notifiedStatus + }); }); - - // Update button states after save - updateButtonStates(); + updateStatusButtonStates(); }) .catch(function (error) { abp.notify.error(error.message || l('ApplicantPortalSettings:SaveError')); }) .always(function () { - abp.ui.clearBusy($form); + abp.ui.clearBusy($statusForm); }); }); + const $programDetailsForm = $('#ProgramDetailsForm'); + if (!$programDetailsForm.length) { + return; + } + + UnityCharacterCounter.init('#ProgramDetailsForm'); + + const $programDetailsSaveButton = $('#ProgramDetailsSaveButton'); + const $programDetailsResetButton = $('#ProgramDetailsResetButton'); + const $displayNameInput = $('#ProgramDetailsDisplayName'); + const $divisionInput = $('#ProgramDetailsDivision'); + const $branchInput = $('#ProgramDetailsBranch'); + const $descriptionInput = $('#ProgramDetailsDescription'); + + const originalProgramDetailsValues = { + displayName: $displayNameInput.val().trim(), + division: $divisionInput.val().trim(), + branch: $branchInput.val().trim(), + description: $descriptionInput.val().trim() + }; + + function getProgramDetailsValues() { + return { + displayName: $displayNameInput.val().trim(), + division: $divisionInput.val().trim(), + branch: $branchInput.val().trim(), + description: $descriptionInput.val().trim() + }; + } + + function hasProgramDetailsChanges() { + const currentValues = getProgramDetailsValues(); + return currentValues.displayName !== originalProgramDetailsValues.displayName + || currentValues.division !== originalProgramDetailsValues.division + || currentValues.branch !== originalProgramDetailsValues.branch + || currentValues.description !== originalProgramDetailsValues.description; + } + + function updateProgramDetailsButtonStates() { + const changed = hasProgramDetailsChanges(); + $programDetailsSaveButton.prop('disabled', !changed); + $programDetailsResetButton.prop('disabled', !changed); + } + + const debouncedProgramDetailsButtonUpdate = debounce(updateProgramDetailsButtonStates, 150); + + $programDetailsForm.on('input', 'input, textarea', function () { + debouncedProgramDetailsButtonUpdate(); + }); + + $programDetailsResetButton.on('click', function (e) { + e.preventDefault(); + $displayNameInput.val(originalProgramDetailsValues.displayName); + $divisionInput.val(originalProgramDetailsValues.division); + $branchInput.val(originalProgramDetailsValues.branch); + $descriptionInput.val(originalProgramDetailsValues.description); + + // Trigger input events to update character counters + $displayNameInput.trigger('input'); + $divisionInput.trigger('input'); + $branchInput.trigger('input'); + $descriptionInput.trigger('input'); + + updateProgramDetailsButtonStates(); + abp.notify.info(l('ApplicantPortalSettings:ChangesReset')); + }); + + updateProgramDetailsButtonStates(); + + $programDetailsForm.on('submit', function (e) { + e.preventDefault(); + + if (!hasProgramDetailsChanges()) { + abp.notify.info(l('ApplicantPortalSettings:NoChanges')); + return; + } + + const currentValues = getProgramDetailsValues(); + + abp.ui.setBusy($programDetailsForm); + unity.grantManager.grantApplications.applicationStatus + .updateApplicantPortalProgramDetails({ + displayName: currentValues.displayName, + division: currentValues.division, + branch: currentValues.branch, + description: currentValues.description + }) + .then(function () { + originalProgramDetailsValues.displayName = currentValues.displayName; + originalProgramDetailsValues.division = currentValues.division; + originalProgramDetailsValues.branch = currentValues.branch; + originalProgramDetailsValues.description = currentValues.description; + updateProgramDetailsButtonStates(); + abp.notify.success(l('ApplicantPortalSettings:ProgramDetailsSaveSuccess')); + }) + .catch(function (error) { + abp.notify.error(error.message || l('ApplicantPortalSettings:ProgramDetailsSaveError')); + }) + .always(function () { + abp.ui.clearBusy($programDetailsForm); + }); + }); })(jQuery); From 0b603384db9f03d4409e4d201035f886990afe9b Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:15:19 -0700 Subject: [PATCH 02/23] [AB#33559] Add Bulk Publish Service Endpoints --- .../GrantApplications/BulkPublishDto.cs | 24 ++++++++ .../IBulkApprovalsAppService.cs | 2 + .../BulkApprovalsAppService.cs | 57 ++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkPublishDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkPublishDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkPublishDto.cs new file mode 100644 index 0000000000..f0461e734b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkPublishDto.cs @@ -0,0 +1,24 @@ +using System; + +namespace Unity.GrantManager.GrantApplications; + +public class BulkPublishDto +{ + public BulkPublishDto() + { + ReferenceNo = string.Empty; + ApplicantName = string.Empty; + FormName = string.Empty; + ApplicationStatus = string.Empty; + } + + public Guid ApplicationId { get; set; } + public string ReferenceNo { get; set; } + public string ApplicantName { get; set; } + public string FormName { get; set; } + public DateTime? FinalDecisionDate { get; set; } + public string ApplicationStatus { get; set; } + public bool ExternalStatusVisibility { get; set; } + public string ExternalStatus { get; set; } = string.Empty; + public string PublishedStatus { get; set; } = string.Empty; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs index 1340a7c730..3f63ccaae1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs @@ -8,5 +8,7 @@ public interface IBulkApprovalsAppService { Task BulkApproveApplications(List batchApplicationsToApprove); Task> GetApplicationsForBulkApproval(Guid[] applicationGuids); + Task> GetApplicationsForBulkPublish(Guid[] applicationGuids); + Task BulkPublishApplications(Guid[] applicationGuids); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs index be2ee1672e..885d1600e0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -14,7 +15,10 @@ namespace Unity.GrantManager.GrantApplications { [Authorize] - public class BulkApprovalsAppService(IApplicationRepository applicationRepository, + public class BulkApprovalsAppService( + IApplicationRepository applicationRepository, + IApplicationStatusRepository applicationStatusRepository, + IApplicationFormRepository applicationFormRepository, IApplicationManager applicationManager, ILocalEventBus localEventBus, IUnitOfWorkManager unitofWorkManager) : GrantManagerAppService, IBulkApprovalsAppService @@ -243,5 +247,56 @@ private static OperationAuthorizationRequirement GetActionAuthorizationRequireme return new OperationAuthorizationRequirement { Name = $"{GrantApplicationPermissions.Applications.Default}.{triggerAction}" }; // this should allow anyone for now, but needs to change to a specific permission } + + /// + /// Get applications for bulk publish with internal and external status information + /// + /// + /// + public async Task> GetApplicationsForBulkPublish(Guid[] applicationGuids) + { + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + var statusesQuery = await applicationStatusRepository.GetQueryableAsync(); + var formsQuery = await applicationFormRepository.GetQueryableAsync(); + + var results = await ( + from application in applicationsQuery + join form in formsQuery on application.ApplicationFormId equals form.Id + join status in statusesQuery on application.ApplicationStatusId equals status.Id + where applicationGuids.Contains(application.Id) + select new BulkPublishDto + { + ApplicationId = application.Id, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.Applicant.ApplicantName ?? string.Empty, + ApplicationStatus = application.ApplicationStatus.InternalStatus, + FormName = form.ApplicationFormName ?? string.Empty, + FinalDecisionDate = application.FinalDecisionDate, + ExternalStatusVisibility = application.ExternalStatusVisibility, + ExternalStatus = application.ExternalStatusVisibility + ? status.NotifiedStatus ?? status.ExternalStatus + : status.ExternalStatus, + PublishedStatus = status.NotifiedStatus ?? status.ExternalStatus, + }).ToListAsync(); + + return results; + } + + /// + /// Bulk publish applications by setting the ExternalStatusVisibility to true for the specified application GUIDs. + /// + /// The GUIDs of the applications to be bulk published. + /// A task representing the asynchronous operation. + public async Task BulkPublishApplications(Guid[] applicationGuids) + { + var applications = await applicationRepository + .GetListAsync(x => applicationGuids.Contains(x.Id) && !x.ExternalStatusVisibility); + + foreach (var application in applications) + { + application.ExternalStatusVisibility = true; + await applicationRepository.UpdateAsync(application); + } + } } } From b7bc5aef9e83ea24bd26db67b89af16cd329544b Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:53:22 -0700 Subject: [PATCH 03/23] [AB#33559] Add bulk publish to action bar --- .../Mapping/GrantManagerWebMapperlyProfile.cs | 11 +++ .../Shared/Components/ActionBar/ActionBar.cs | 4 ++ .../Components/ActionBar/Default.cshtml | 6 ++ .../Shared/Components/ActionBar/Default.js | 70 ++++++++++++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs index ad9bd5b7ab..14a0eb5c26 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs @@ -13,6 +13,7 @@ using Unity.GrantManager.Web.Pages.ApplicantContact; using Unity.GrantManager.Web.Pages.ApplicationContact; using Unity.GrantManager.Web.Pages.ApplicationForms.ViewModels; +using Unity.GrantManager.Web.Pages.BulkActions.PublishApplications; using Unity.GrantManager.Web.Pages.Sites.ViewModels; using Unity.GrantManager.Web.Views.Shared.Components.ApplicantInfo; using Unity.GrantManager.Web.Views.Shared.Components.SummaryWidget; @@ -320,3 +321,13 @@ public partial class ApplicantContactModalViewModelToUpdateApplicantContactDtoMa [MapperIgnoreTarget(nameof(UpdateApplicantContactDto.WorkPhoneExtension))] public override partial void Map(ApplicantContactModalViewModel source, UpdateApplicantContactDto destination); } + +[Mapper] +public partial class BulkPublishDtoToViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(BulkPublishApplicationViewModel.IsValid))] + public override partial BulkPublishApplicationViewModel Map(BulkPublishDto source); + + [MapperIgnoreTarget(nameof(BulkPublishApplicationViewModel.IsValid))] + public override partial void Map(BulkPublishDto source, BulkPublishApplicationViewModel destination); +} 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 97183d98f8..18a1e5edfc 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 @@ -26,6 +26,8 @@ public override void ConfigureBundle(BundleConfigurationContext context) .AddIfNotContains("/Views/Shared/Components/ActionBar/Default.css"); context.Files .AddIfNotContains("/Pages/BulkApprovals/ApproveApplicationsModal.css"); + context.Files + .AddIfNotContains("/Pages/BulkActions/BulkPublishApplications.css"); } } @@ -48,6 +50,8 @@ public override void ConfigureBundle(BundleConfigurationContext context) .AddIfNotContains("/libs/jquery-maskmoney/dist/jquery.maskMoney.min.js"); context.Files .AddIfNotContains("/Pages/BulkApprovals/ApproveApplicationsModal.js"); + context.Files + .AddIfNotContains("/Pages/BulkActions/BulkPublishApplications.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 7aaf21086f..6fce97e151 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 @@ -79,6 +79,12 @@ text="@L["ApplicationList:ApproveButton"].Value" /> } + + @if(await PermissionChecker.IsGrantedAsync(UnitySelector.Application.Tags.Create) || await PermissionChecker.IsGrantedAsync(UnitySelector.Application.Tags.Delete)) { { + return { + Key: failure.key, + Value: failure.value + }; + }); + let summaryJson = JSON.stringify( + { + Successes: response.responseText.successes, + Failures: transformedFailures + }); + publishApplicationsModal.open({ summaryJson: summaryJson }); + PubSub.publish("refresh_application_list"); + }); + //#endregion Batch Publish + + //#region Batch Approval $('#approveApplications').on("click", function () { // Store application IDs in distributed cache to avoid URL length limits unity.grantManager.applications.applicationBulkActions @@ -282,8 +340,9 @@ $(function () { approveApplicationsSummaryModal.open({ summaryJson: summaryJson }); PubSub.publish("refresh_application_list"); }); - // Batch Approval End + //#endregion Batch Approval + //#region Selection Events PubSub.subscribe("select_application", (msg, data) => { selectedApplicationIds.push(data.id); manageActionButtons(); @@ -302,7 +361,9 @@ $(function () { selectedApplicationIds = []; manageActionButtons(); }); + //#endregion Selection Events + //#region Action Button Click Events $('#assignApplication').on('click', function () { // Store application IDs in distributed cache to avoid URL length limits unity.grantManager.applications.applicationBulkActions @@ -344,7 +405,9 @@ $(function () { '/GrantApplications/Details?ApplicationId=' + selectedApplicationIds[0]; }); + //#endregion Action Button Click Events + //#region Action Bar State let summaryWidgetManager = new abp.WidgetManager({ wrapper: '#summaryWidgetArea', filterCallback: function () { @@ -379,7 +442,9 @@ $(function () { } } } + //#endregion Action Bar State + //#region Tags and Payments $('#tagApplication').on('click', function () { // Store application IDs in distributed cache to avoid URL length limits @@ -442,5 +507,6 @@ $(function () { abp.notify.success('The historical payment has been successfully recorded.', 'Historical Payment'); PubSub.publish("refresh_application_list"); }); + //#endregion Tags and Payments }); From 44b7438079dc3be95e73f2346cfa55dbb4930af1 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:43:30 -0700 Subject: [PATCH 04/23] [AB#33559] Add Status Visible column to Applications DataTable --- .../GrantApplicationAppService.cs | 1 + .../Localization/GrantManager/en.json | 1 + .../Applications/ApplicationListRecord.cs | 1 + .../Repositories/ApplicationRepository.cs | 2 ++ .../Pages/GrantApplications/Index.js | 14 ++++++++++++++ 5 files changed, 19 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 538db8e0c2..c6839203a7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -162,6 +162,7 @@ public async Task> GetListAsync(GrantApplica ContractExecutionDate = rec.ContractExecutionDate, RiskRanking = rec.RiskRanking, UnityApplicationId = rec.UnityApplicationId, + ExternalStatusVisibility = rec.ExternalStatusVisibility, // From ApplicationStatus Status = rec.Status, 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 92c7fefd64..f65eb41ae5 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 @@ -67,6 +67,7 @@ "Assignee": "Assignee", "Probability": "Probability", "GrantApplicationStatus": "Status", + "ExternalStatusVisibility": "Status Visibility", "ProposalDate": "Proposal Date", "SubmissionDate": "Submission Date", "GrantApplications": "Grant Applications", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs index 999ad5fa13..7b4f4e11ca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ApplicationListRecord.cs @@ -57,6 +57,7 @@ public class ApplicationListRecord public DateTime? ContractExecutionDate { get; init; } public string? RiskRanking { get; init; } public string? UnityApplicationId { get; init; } + public bool ExternalStatusVisibility { get; init; } // ApplicationStatus (always joined) public string Status { get; init; } = string.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs index 15ec1b58b8..80432f764a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicationRepository.cs @@ -322,6 +322,7 @@ public async Task> GetApplicationListRecordsAsync( a.RiskRanking, a.UnityApplicationId, Status = a.ApplicationStatus.InternalStatus, // ApplicationStatus (always joined) + a.ExternalStatusVisibility, Category = a.ApplicationForm.Category ?? string.Empty, // ApplicationForm (always joined) a.ApplicantId, ApplicantName = a.Applicant.ApplicantName, // Applicant (always joined) @@ -506,6 +507,7 @@ public async Task> GetApplicationListRecordsAsync( RiskRanking = a.RiskRanking, UnityApplicationId = a.UnityApplicationId, Status = a.Status, + ExternalStatusVisibility = a.ExternalStatusVisibility, Category = a.Category, ApplicantId = a.ApplicantId, ApplicantName = a.ApplicantName, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js index 1e24a3cf75..442ec9b777 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js @@ -569,6 +569,7 @@ $(function () { getTotalProjectBudgetColumn(columnIndex++), getAssigneesColumn(columnIndex++), getStatusColumn(columnIndex++), + getExternalStatusVisibilityColumn(columnIndex++), getRequestedAmountColumn(columnIndex++), getApprovedAmountColumn(columnIndex++), getEconomicRegionColumn(columnIndex++), @@ -789,6 +790,19 @@ $(function () { } } + function getExternalStatusVisibilityColumn(columnIndex) { + return { + title: l('ExternalStatusVisibility'), + data: 'externalStatusVisibility', + name: 'externalStatusVisibility', + className: 'data-table-header', + render: function (data) { + return data ? 'Published' : 'Unpublished'; + }, + index: columnIndex + } + } + function getRequestedAmountColumn(columnIndex) { return { title: l('RequestedAmount'), From 8260c68139c8aca447e373ef78a9a8f027ca70de Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:30:09 -0700 Subject: [PATCH 05/23] [AB#33559] Add publish filter to GetApplicationsForBulkPublish --- .../GrantApplications/IBulkApprovalsAppService.cs | 2 +- .../GrantApplications/BulkApprovalsAppService.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs index 3f63ccaae1..458098da7b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs @@ -8,7 +8,7 @@ public interface IBulkApprovalsAppService { Task BulkApproveApplications(List batchApplicationsToApprove); Task> GetApplicationsForBulkApproval(Guid[] applicationGuids); - Task> GetApplicationsForBulkPublish(Guid[] applicationGuids); + Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true); Task BulkPublishApplications(Guid[] applicationGuids); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs index 885d1600e0..d061a6703a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs @@ -252,8 +252,9 @@ private static OperationAuthorizationRequirement GetActionAuthorizationRequireme /// Get applications for bulk publish with internal and external status information /// /// + /// /// - public async Task> GetApplicationsForBulkPublish(Guid[] applicationGuids) + public async Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true) { var applicationsQuery = await applicationRepository.GetQueryableAsync(); var statusesQuery = await applicationStatusRepository.GetQueryableAsync(); @@ -263,7 +264,7 @@ public async Task> GetApplicationsForBulkPublish(Guid[] app from application in applicationsQuery join form in formsQuery on application.ApplicationFormId equals form.Id join status in statusesQuery on application.ApplicationStatusId equals status.Id - where applicationGuids.Contains(application.Id) + where applicationGuids.Contains(application.Id) && (!excludePublished || !application.ExternalStatusVisibility) select new BulkPublishDto { ApplicationId = application.Id, From a677e69540d4efe18958911c3fc81f07ed022eaa Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:52:12 -0700 Subject: [PATCH 06/23] [AB#33559] Add BulkPublishApplications Modal --- .../BulkPublishApplicationViewModel.cs | 24 ++++ .../BulkPublishApplications.cshtml | 125 ++++++++++++++++++ .../BulkPublishApplications.cshtml.cs | 105 +++++++++++++++ .../BulkActions/BulkPublishApplications.css | 110 +++++++++++++++ .../BulkActions/BulkPublishApplications.js | 69 ++++++++++ .../Shared/Components/ActionBar/Default.js | 16 +-- 6 files changed, 436 insertions(+), 13 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplicationViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplicationViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplicationViewModel.cs new file mode 100644 index 0000000000..b7f6ab6e48 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplicationViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel; + +namespace Unity.GrantManager.Web.Pages.BulkActions; + +public class BulkPublishApplicationViewModel +{ + public Guid ApplicationId { get; set; } + public string? ApplicantName { get; set; } = string.Empty; + public string ReferenceNo { get; set; } = string.Empty; + public bool ExternalStatusVisibility { get; set; } + public string ApplicationStatus { get; set; } = string.Empty; + public string FormName { get; set; } = string.Empty; + + [DisplayName("External Status (Current)")] + public string ExternalStatus { get; set; } = string.Empty; + + [DisplayName("Published Status (Future)")] + public string PublishedStatus { get; set; } = string.Empty; + + [DisplayName("Decision Date")] + public DateTime? FinalDecisionDate { get; set; } + public bool IsValid { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml new file mode 100644 index 0000000000..7caf8e7e69 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml @@ -0,0 +1,125 @@ +@page +@model Unity.GrantManager.Web.Pages.BulkActions.BulkPublishApplicationsModel +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization + +@inject IStringLocalizer L +@{ + Layout = null; +} + + +
+ + + + @if (ViewData["Error"] != null) + { + + } + else + { + + + + + @for (var i = 0; i < Model.BulkApplications?.Count; i++) + { +
+ + +
+
@Model.BulkApplications[i].ReferenceNo
+
+ @(string.IsNullOrWhiteSpace(Model.BulkApplications[i].ApplicantName) ? "Applicant Name" : @Model.BulkApplications[i].ApplicantName) +
+
+ @(string.IsNullOrWhiteSpace(Model.BulkApplications[i].FormName) ? "(Form Name)" : string.Format("({0})", @Model.BulkApplications[i].FormName)) +
+
@Model.BulkApplications[i].ApplicationStatus
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + @if (Model.BulkApplications[i].FinalDecisionDate.HasValue) + { + + } + else + { +
+ + +
+ } +
+
+
+ } +
+
+ + +

+ Error You have exceeded the maximum number of items for bulk publishing. Please reduce the number to 50 or fewer +

+

+ Error @Model.MaxBatchCountExceededError +

+
+
+ + +

+ Note: By confirming, the selected application statuses will be published and made visible to applicants in the portal. +

+
+
+ } +
+ + + + + + +
+
\ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs new file mode 100644 index 0000000000..814fe50928 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs @@ -0,0 +1,105 @@ +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 Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.BulkActions; + +public class BulkPublishApplicationsModel( + ApplicationIdsCacheService cacheService, + IBulkApprovalsAppService bulkApprovalsAppService) : AbpPageModel +{ + [BindProperty] + public List? BulkApplications { get; set; } + + [BindProperty] + public string SelectedApplicationIds { get; set; } = string.Empty; + + [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; + BulkApplications = []; + MaxBatchCountExceededError = L["ApplicationBatchApprovalRequest: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; + } + + // Convert List to Guid[] for existing code compatibility + Guid[] applicationGuids = [.. selectedApplicationIds]; + + // Clean up cache after retrieval (one-time use) + await cacheService.RemoveAsync(cacheKey); + + if (MaxBatchCount <= applicationGuids.Length) + { + MaxBatchCountExceeded = true; + } + + if (applicationGuids.Length == 0) + { + return; + } + + // Fetch application details for the selected IDs + var bulkApplications = await bulkApprovalsAppService.GetApplicationsForBulkPublish(applicationGuids); + + BulkApplications = ObjectMapper.Map, List>(bulkApplications); + ApplicationsCount = BulkApplications.Count; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading bulk publish applications modal"); + ViewData["Error"] = "An error occurred while loading the applications. Please try again."; + Invalid = true; + return; + } + } + + public async Task OnPostAsync() + { + try + { + if (BulkApplications == null) return NoContent(); + var applicationsToPublish = BulkApplications.Select(y => y.ApplicationId).ToArray(); + await bulkApprovalsAppService.BulkPublishApplications(applicationsToPublish); + + return new OkResult(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error updating application status"); + } + + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.css new file mode 100644 index 0000000000..fe7b184776 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.css @@ -0,0 +1,110 @@ +.batch-action-card { + max-height: 65vh; + overflow: auto; +} + +.batch-action-container { + border: var(--bs-border-width) solid; + border-color: var(--bs-border-color); +} + +.bulk-action-row-header { + padding-bottom: 0 !important; +} + +.bulk-action-remove-row { + min-width: 55px +} + + +/* This is for the summary modal*/ + +.table-container { + max-height: 65vh; + overflow-y: auto; +} + +.batch-action-container input.form-control:disabled { + color: var(--bs-body-color); +} + +.custom-table { + width: 100%; + border-collapse: collapse; +} + +.centered-icon { + text-align: center; + padding-bottom: 0.85rem; +} + +.custom-table thead th { + position: sticky; + top: 0; + background-color: white; + z-index: 1; + font-size: 1rem; + padding-bottom: 1rem; +} + +.custom-table .col-5 { + width: 5%; +} + +.custom-table .col-20 { + width: 20%; +} + +.custom-table .col-75 { + width: 75%; +} + +.action-details-header { + display: flex; + align-items: center; + gap: 0.75em; +} + + .action-details-header + .header-data { + color: var(--bs-body-color); + -webkit-font-smoothing: antialiased; + } + + .action-details-header + .reference-no { + font-weight: 700; + } + + .action-details-header + .application-status { + font-weight: 700; + color: var(--bc-colors-blue-text-links); + text-transform: uppercase; + border: 3px solid var(--bc-colors-blue-text-links); + border-radius: 1rem; + font-size: 0.8rem; + padding: 0.125rem 0.5rem; + margin-top: 2px; + } + + +.action-note-prefix { + font-weight: 700; + color: var(--bc-colors-blue-text-links); + text-transform: uppercase; + border: 3px solid var(--bc-colors-blue-text-links); + border-radius: 1rem; + font-size: 0.8rem; + padding: 0.025rem 0.5rem; + line-height: 1.75rem; +} + +.action-note-error { + color: var(--lpx-danger); + border-color: var(--lpx-danger); +} + +.batch-direct-action-indicator { + padding-left: 1rem; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js new file mode 100644 index 0000000000..4a4d5f2e61 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js @@ -0,0 +1,69 @@ +function removeApplicationRow(applicationId) { + $('#' + applicationId).remove(); + let applicationsCount = $('#ApplicationsCount').val(); + $('#ApplicationsCount').val(applicationsCount - 1); + runValidations(); +} + +function setMaxCountError(visible) { + const summary = $('#batch-action-summary'); + if (visible) { + summary.css('display', 'block'); + } else { + summary.css('display', 'none'); + } +} + +function enableBulkPublishSubmit() { + $("#bulkPublishApplicationsModal") + .find('#btnSubmitBatchPublish').prop("disabled", false); +} + +function disableBulkPublishSubmit() { + $("#bulkPublishApplicationsModal") + .find('#btnSubmitBatchPublish').prop("disabled", true); +} + +function closePublish() { + $('#bulkPublishApplicationsModal').modal('hide'); +} + +function runValidations() { + let isValid = true; + let itemCount = 0; + + $('#bulkPublishForm input[name="BulkApplications.Index"]').each(function () { + itemCount++; + }); + + isValid = validBatchCount(); + $('#batch-action-summary').toggleClass('d-none', isValid); + + if (isValid) { + enableBulkPublishSubmit(); + } else { + disableBulkPublishSubmit(); + } +} + +function validBatchCount() { + let applicationsCount = $('#ApplicationsCount').val(); + let maxBatchCount = $('#MaxBatchCount').val(); + let validationMaxValid = true; + let validationMinValid = true; + + if (maxBatchCount <= applicationsCount) { + validationMaxValid = false; + } else if (applicationsCount === 0) { + validationMinValid = false; + } + + $('#maxCountWarning').toggleClass('d-none', validationMaxValid); + $('#minCountWarning').toggleClass('d-none', validationMinValid); + + return validationMaxValid && validationMinValid; +} + +$(function () { + runValidations(); +}); \ No newline at end of file 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 a2c87cb4bd..f8ff634475 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 @@ -292,19 +292,9 @@ $(function () { }); }); - publishApplicationsModal.onResult(function (_, response) { - let transformedFailures = response.responseText.failures.map(failure => { - return { - Key: failure.key, - Value: failure.value - }; - }); - let summaryJson = JSON.stringify( - { - Successes: response.responseText.successes, - Failures: transformedFailures - }); - publishApplicationsModal.open({ summaryJson: summaryJson }); + publishApplicationsModal.onResult(function (_, response) { + // This is here for future validation or additional processing if needed + publishApplicationsModal.close(); PubSub.publish("refresh_application_list"); }); //#endregion Batch Publish From b4f1a0f31edac0f39d1391be4f69325777f3e02b Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 9 Jul 2026 15:00:03 -0700 Subject: [PATCH 07/23] AB#33530 program area metadata configuration --- .../IApplicationStatusService.cs | 4 +- ...gramDetailsDto.cs => ProgramDetailsDto.cs} | 2 +- .../UpdateApplicantPortalProgramDetailsDto.cs | 18 -- .../UpdateProgramDetailsDto.cs | 18 ++ ...ApplicationPermissionDefinitionProvider.cs | 1 + ...rantManagerPermissionDefinitionProvider.cs | 3 - .../ApplicationStatusAppService.cs | 10 +- .../ApplicantPortalProgramDetailsConsts.cs | 9 - .../GrantApplications/ProgramDetailsConsts.cs | 9 + .../Localization/GrantManager/en.json | 22 +- .../Permissions/GrantManagerPermissions.cs | 6 - .../UnitySettingManagementPermissions.cs | 2 + .../Permissions/PermissionGrantsDataSeeder.cs | 4 +- .../Identity/PolicyRegistrant.cs | 2 - .../ApplicantPortalSettings/Index.cshtml | 73 +----- .../ApplicantPortalSettings/Index.cshtml.cs | 14 +- .../Pages/ApplicantPortalSettings/Index.css | 35 --- .../Pages/ApplicantPortalSettings/Index.js | 208 ++++-------------- .../ConfigurationManagement/Index.cshtml | 23 ++ .../ConfigurationManagement/Index.cshtml.cs | 3 + .../ProgramDetails}/CharacterCounter.js | 8 +- .../ProgramDetails/ProgramDetails.css | 26 +++ .../Settings/ProgramDetails/ProgramDetails.js | 114 ++++++++++ .../ProgramDetailsViewComponent.cs | 17 ++ .../ProgramDetailsViewComponent.cshtml | 65 ++++++ 25 files changed, 355 insertions(+), 341 deletions(-) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/{ApplicantPortalProgramDetailsDto.cs => ProgramDetailsDto.cs} (87%) delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Web/{Pages/ApplicantPortalSettings => Views/Settings/ProgramDetails}/CharacterCounter.js (95%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs index 548997362c..d1ee48ccf9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs @@ -8,7 +8,7 @@ public interface IApplicationStatusService : IApplicationService { Task> GetListAsync(); Task> GetApplicantPortalStatusListAsync(); - Task GetApplicantPortalProgramDetailsAsync(); + Task GetProgramDetailsAsync(); Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatusExternalLabelsDto input); - Task UpdateApplicantPortalProgramDetailsAsync(UpdateApplicantPortalProgramDetailsDto input); + Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto input); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ProgramDetailsDto.cs similarity index 87% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ProgramDetailsDto.cs index 9415fe0f88..fb231c3a3a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ApplicantPortalProgramDetailsDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ProgramDetailsDto.cs @@ -3,7 +3,7 @@ namespace Unity.GrantManager.GrantApplications; [Serializable] -public class ApplicantPortalProgramDetailsDto +public class ProgramDetailsDto { public string DisplayName { get; set; } = string.Empty; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs deleted file mode 100644 index b5b955778c..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateApplicantPortalProgramDetailsDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Unity.GrantManager.GrantApplications; - -public class UpdateApplicantPortalProgramDetailsDto -{ - [StringLength(ApplicantPortalProgramDetailsConsts.MaxDisplayNameLength)] - public string? DisplayName { get; set; } - - [StringLength(ApplicantPortalProgramDetailsConsts.MaxDivisionLength)] - public string? Division { get; set; } - - [StringLength(ApplicantPortalProgramDetailsConsts.MaxBranchLength)] - public string? Branch { get; set; } - - [StringLength(ApplicantPortalProgramDetailsConsts.MaxDescriptionLength)] - public string? Description { get; set; } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs new file mode 100644 index 0000000000..775b75b2dc --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.GrantApplications; + +public class UpdateProgramDetailsDto +{ + [StringLength(ProgramDetailsConsts.MaxDisplayNameLength)] + public string? DisplayName { get; set; } + + [StringLength(ProgramDetailsConsts.MaxDivisionLength)] + public string? Division { get; set; } + + [StringLength(ProgramDetailsConsts.MaxBranchLength)] + public string? Branch { get; set; } + + [StringLength(ProgramDetailsConsts.MaxDescriptionLength)] + public string? Description { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index b9ef4f56e3..c69b8790de 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -87,6 +87,7 @@ public override void Define(IPermissionDefinitionContext context) settingManagement.AddPermission(UnitySettingManagementPermissions.UserInterface, L("Permission:UnitySettingManagementPermissions.UserInterface")); settingManagement.AddPermission(UnitySettingManagementPermissions.BackgroundJobSettings, L("Permission:UnitySettingManagementPermissions.BackgroundJobs")); settingManagement.AddPermission(UnitySettingManagementPermissions.ConfigurePayments, L("Permission:UnitySettingManagementPermissions.ConfigurePayments")); + settingManagement.AddPermission(UnitySettingManagementPermissions.EditProgramDetails, L("Permission:UnitySettingManagementPermissions.EditProgramDetails")); var emailingPermission = context.GetPermissionOrNull(SettingManagementPermissions.Emailing); if (emailingPermission != null) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs index 3194972cf9..53d584174d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantManagerPermissionDefinitionProvider.cs @@ -20,9 +20,6 @@ public override void Define(IPermissionDefinitionContext context) grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.ApplicationForms.Default, L("Permission:GrantManagerManagement.ApplicationForms.Default")); - var applicantPortalPermissions = grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.ApplicantPortal.Default, L("Permission:GrantManagerManagement.ApplicantPortal.Default")); - applicantPortalPermissions.AddChild(GrantManagerPermissions.ApplicantPortal.EditProgramDetails, L("Permission:GrantManagerManagement.ApplicantPortal.EditProgramDetails")); - var notificationSchedulerPermissions = grantManagerPermissionsGroup.AddPermission(GrantManagerPermissions.NotificationScheduler.Default, L("Permission:GrantManagerManagement.NotificationScheduler.Default")); notificationSchedulerPermissions.AddChild(GrantManagerPermissions.NotificationScheduler.ManageSchedules, L("Permission:GrantManagerManagement.NotificationScheduler.ManageSchedules")); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs index 6ed8427b8a..ff67f30dca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs @@ -44,12 +44,12 @@ public virtual async Task> GetApplicantPortalSta return ObjectMapper.Map, List>(statuses.OrderBy(s => s.StatusCode).ToList()); } - [Authorize(GrantManagerPermissions.ApplicantPortal.EditProgramDetails)] - public virtual async Task GetApplicantPortalProgramDetailsAsync() + [Authorize(UnitySettingManagementPermissions.EditProgramDetails)] + public virtual async Task GetProgramDetailsAsync() { var tenant = await GetCurrentTenantForProgramDetailsAsync(); - return new ApplicantPortalProgramDetailsDto + return new ProgramDetailsDto { DisplayName = GetExtraPropertyValue(tenant, "DisplayName"), Division = GetExtraPropertyValue(tenant, "Division"), @@ -76,8 +76,8 @@ public virtual async Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatu } } - [Authorize(GrantManagerPermissions.ApplicantPortal.EditProgramDetails)] - public virtual async Task UpdateApplicantPortalProgramDetailsAsync(UpdateApplicantPortalProgramDetailsDto input) + [Authorize(UnitySettingManagementPermissions.EditProgramDetails)] + public virtual async Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto input) { var tenant = await GetCurrentTenantForProgramDetailsAsync(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs deleted file mode 100644 index 38588646a8..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ApplicantPortalProgramDetailsConsts.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Unity.GrantManager.GrantApplications; - -public static class ApplicantPortalProgramDetailsConsts -{ - public const int MaxDisplayNameLength = 120; - public const int MaxDivisionLength = 120; - public const int MaxBranchLength = 120; - public const int MaxDescriptionLength = 500; -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs new file mode 100644 index 0000000000..86e8df49d4 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs @@ -0,0 +1,9 @@ +namespace Unity.GrantManager.GrantApplications; + +public static class ProgramDetailsConsts +{ + public const int MaxDisplayNameLength = 30; + public const int MaxDivisionLength = 50; + public const int MaxBranchLength = 50; + public const int MaxDescriptionLength = 500; +} 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 ec582bc805..a802c6649b 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 @@ -163,8 +163,6 @@ "Permission:GrantApplicationManagement.Organizations.ManageProfiles": "Manage Organization Profiles", "Permission:GrantManagerManagement.Intakes.Default": "Manage Intakes", "Permission:GrantManagerManagement.ApplicationForms.Default": "Manage Forms", - "Permission:GrantManagerManagement.ApplicantPortal.Default": "Applicant Portal", - "Permission:GrantManagerManagement.ApplicantPortal.EditProgramDetails": "Edit Program Details", "Permission:GrantManagerManagement.NotificationScheduler.Default": "Notification Scheduler", "Permission:GrantManagerManagement.NotificationScheduler.ManageSchedules": "Manage Notification Schedules", "Permission:GrantApplicationManagement.Approvals.BulkApplicationApproval": "Bulk Application Approval", @@ -333,6 +331,7 @@ "Permission:UnitySettingManagementPermissions.BackgroundJobs": "Background Jobs", "Permission:UnitySettingManagementPermissions.UserInterface": "Application Tabs", "Permission:UnitySettingManagementPermissions.ConfigurePayments": "Configure Payments", + "Permission:UnitySettingManagementPermissions.EditProgramDetails": "Edit Program Details", "Setting:GrantManager.UI.PageTitle": "Application Tabs Configuration", "Setting:GrantManager.UI.Tabs.Applicant.Description": "Toggle visibility of the Applicant Info tab for the tenant", @@ -536,24 +535,29 @@ "ApplicantPortalSettings:ManageStatuses": "Manage Statuses", "ApplicantPortalSettings:ProgramDetails": "Program Details", "ApplicantPortalSettings:PortalStatusHeading": "Applicant Portal Status", - "ApplicantPortalSettings:ProgramDetailsHeading": "Program Details", "ApplicantPortalSettings:InternalStatus": "Internal Status", "ApplicantPortalSettings:PortalStatusLabel": "External Status", "ApplicantPortalSettings:PortalPublishedStatusLabel": "Published Status", - "ApplicantPortalSettings:DisplayName": "Display Name", - "ApplicantPortalSettings:Division": "Division", - "ApplicantPortalSettings:Branch": "Branch", - "ApplicantPortalSettings:Description": "Description", "ApplicantPortalSettings:SaveChanges": "Save Changes", "ApplicantPortalSettings:ResetChanges": "Reset", "ApplicantPortalSettings:NoChanges": "No changes to save.", "ApplicantPortalSettings:ChangesReset": "Changes have been reset to original values.", "ApplicantPortalSettings:SaveSuccess": "Portal status labels updated successfully.", - "ApplicantPortalSettings:ProgramDetailsSaveSuccess": "Program details updated successfully.", "ApplicantPortalSettings:SaveError": "An error occurred while saving portal status labels.", - "ApplicantPortalSettings:ProgramDetailsSaveError": "An error occurred while saving program details.", "ApplicantPortalSettings:ValidationRequired": "Portal status label cannot be empty.", + "ProgramDetails:Heading": "Program Details", + "ProgramDetails:DisplayName": "Display Name", + "ProgramDetails:Division": "Division", + "ProgramDetails:Branch": "Branch", + "ProgramDetails:Description": "Description", + "ProgramDetails:SaveChanges": "Save Changes", + "ProgramDetails:ResetChanges": "Reset", + "ProgramDetails:NoChanges": "No changes to save.", + "ProgramDetails:ChangesReset": "Changes have been reset to original values.", + "ProgramDetails:SaveSuccess": "Program details updated successfully.", + "ProgramDetails:SaveError": "An error occurred while saving program details.", + "Common:Command:Cancel": "Cancel", "Common:Command:Save": "Save", "Common:Command:Filter": "Filter", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs index ba2ac63b24..c38b9205fb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/GrantManagerPermissions.cs @@ -24,12 +24,6 @@ public static class ApplicationForms public const string Default = GroupName + ".ApplicationForms"; } - public static class ApplicantPortal - { - public const string Default = GroupName + ".ApplicantPortal"; - public const string EditProgramDetails = Default + ".EditProgramDetails"; - } - public static class Endpoints { public const string Default = GroupName + ".Endpoints"; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs index 84a8d195cd..03405bdd5e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs @@ -8,4 +8,6 @@ public static class UnitySettingManagementPermissions public const string BackgroundJobSettings = "SettingManagement.GrantManager"; public const string ConfigurePayments = "SettingManagement.ConfigurePayments"; + + public const string EditProgramDetails = "SettingManagement.EditProgramDetails"; } 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 6a2f1a381e..98403df532 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -132,7 +132,8 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, IdentitySeedPermissions.Roles.ManagePermissions, GrantManagerPermissions.Intakes.Default, GrantManagerPermissions.ApplicationForms.Default, - GrantManagerPermissions.ApplicantPortal.EditProgramDetails, + UnitySettingManagementPermissions.UserInterface, + UnitySettingManagementPermissions.EditProgramDetails, .. SettingManagement_Tags_CommonPermissions, .. ReviewAndAssessment_CommonPermissions, @@ -237,6 +238,7 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, [ GrantManagerPermissions.Default, UnitySettingManagementPermissions.UserInterface, + UnitySettingManagementPermissions.EditProgramDetails, GrantManagerPermissions.Organizations.Default, GrantManagerPermissions.Organizations.ManageProfiles, GrantManagerPermissions.Intakes.Default, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs index 01efba0ef7..3c55663031 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs @@ -53,8 +53,6 @@ internal static void Register(ServiceConfigurationContext context) policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.Intakes.Default)); authorizationBuilder.AddPolicy(GrantManagerPermissions.ApplicationForms.Default, policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.ApplicationForms.Default)); - authorizationBuilder.AddPolicy(GrantManagerPermissions.ApplicantPortal.EditProgramDetails, - policy => policy.RequireClaim(PermissionConstant, GrantManagerPermissions.ApplicantPortal.EditProgramDetails)); // Grant Application Policies authorizationBuilder.AddPolicy(GrantApplicationPermissions.Applications.Default, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml index 30e45a31a3..1bb47de3d3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml @@ -18,7 +18,6 @@ } @section scripts { - } @@ -31,16 +30,10 @@ - @if (Model.CanEditProgramDetails) - { - - }
-
+
@L["ApplicantPortalSettings:PortalStatusHeading"]
@@ -102,69 +95,5 @@
- - @if (Model.CanEditProgramDetails) - { -
-
-
-
@L["ApplicantPortalSettings:ProgramDetailsHeading"]
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
- }
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs index 517e6ebe33..e51c79beb1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.cshtml.cs @@ -2,28 +2,16 @@ using System.Collections.Generic; using System.Threading.Tasks; using Unity.GrantManager.GrantApplications; -using Unity.GrantManager.Permissions; -using Volo.Abp.Authorization.Permissions; namespace Unity.GrantManager.Web.Pages.ApplicantPortalSettings; [Authorize] -public class IndexModel( - IApplicationStatusService applicationStatusService, - IPermissionChecker permissionChecker) : GrantManagerPageModel +public class IndexModel(IApplicationStatusService applicationStatusService) : GrantManagerPageModel { public IList Statuses { get; set; } = []; - public bool CanEditProgramDetails { get; set; } - public ApplicantPortalProgramDetailsDto ProgramDetails { get; set; } = new(); public async Task OnGetAsync() { Statuses = await applicationStatusService.GetApplicantPortalStatusListAsync(); - CanEditProgramDetails = await permissionChecker.IsGrantedAsync(GrantManagerPermissions.ApplicantPortal.EditProgramDetails); - - if (CanEditProgramDetails) - { - ProgramDetails = await applicationStatusService.GetApplicantPortalProgramDetailsAsync(); - } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css index 6c17985f7b..4800e83354 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.css @@ -33,14 +33,6 @@ padding: 1rem; } -.portal-settings-config-panel { - display: none; -} - -.portal-settings-config-panel.active { - display: block; -} - .portal-settings-actions { background: #fff; } @@ -69,31 +61,4 @@ padding: 0.75rem 1rem; display: flex; margin-top: 1px; -} - -.char-counter-wrapper { - position: relative; - display: block; -} - -.char-counter { - position: absolute; - top: 0; - right: 8px; - font-size: 0.65rem; - font-style: italic; - color: #6c757d; - white-space: nowrap; - padding: 4px 0; - pointer-events: none; -} - -.char-counter-warning { - color: #ffc107; - font-weight: 500; -} - -.char-counter-critical { - color: #dc3545; - font-weight: 500; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js index 79285ed529..38be4cbc7b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/Index.js @@ -1,79 +1,59 @@ (function ($) { - const l = abp.localization.getResource('GrantManager'); - - const $manageStatusesMenuItem = $('#manage-statuses-menu-item'); - const $programDetailsMenuItem = $('#program-details-menu-item'); - const $portalStatusPanel = $('#portal-status-div'); - const $programDetailsPanel = $('#program-details-div'); - - function setActiveSection(sectionName) { - const isManageStatuses = sectionName === 'manage-statuses'; - - $manageStatusesMenuItem.toggleClass('active', isManageStatuses); - $portalStatusPanel.toggleClass('active', isManageStatuses).toggleClass('d-none', !isManageStatuses); - - if ($programDetailsMenuItem.length && $programDetailsPanel.length) { - $programDetailsMenuItem.toggleClass('active', !isManageStatuses); - $programDetailsPanel.toggleClass('active', !isManageStatuses).toggleClass('d-none', isManageStatuses); - } - } - $manageStatusesMenuItem.on('click', function () { - setActiveSection('manage-statuses'); - }); - - $programDetailsMenuItem.on('click', function () { - setActiveSection('program-details'); - }); + const l = abp.localization.getResource('GrantManager'); - setActiveSection('manage-statuses'); + const $form = $('#PortalStatusForm'); + const $saveButton = $('#SaveButton'); + const $resetButton = $('#ResetButton'); - const $statusForm = $('#PortalStatusForm'); - const $statusSaveButton = $('#SaveButton'); - const $statusResetButton = $('#ResetButton'); - const originalStatusValues = new Map(); + // Store original values on page load + const originalValues = new Map(); - const portalStatusTable = new DataTable('#PortalStatusTable', { + let portalStatusTable = new DataTable("#PortalStatusTable", { paging: false, info: false, order: [[0, 'asc']], - columnDefs: [{ + "columnDefs": [{ targets: [1, 2], - orderable: false + orderable: false, }] }); + // Capture original values after DataTable initialization + // Use DataTable's rows().every() to ensure all rows are captured, even if dynamically loaded portalStatusTable.rows().every(function () { const $row = $(this.node()); const id = $row.find('input[type="hidden"]').val(); const externalStatus = $row.find('input[id$=".ExternalStatus"]').val().trim(); const notifiedStatus = $row.find('input[id$=".NotifiedStatus"]').val().trim(); - originalStatusValues.set(id, { externalStatus: externalStatus, notifiedStatus: notifiedStatus }); + originalValues.set(id, { externalStatus, notifiedStatus }); }); - function hasStatusChanges() { + // Check if any values have changed + function hasChanges() { let changed = false; portalStatusTable.$('tbody tr').each(function () { const $row = $(this); const id = $row.find('input[type="hidden"]').val(); const externalStatus = $row.find('input[id$=".ExternalStatus"]').val().trim(); const notifiedStatus = $row.find('input[id$=".NotifiedStatus"]').val().trim(); - const originalValue = originalStatusValues.get(id); + const originalValue = originalValues.get(id); if (!originalValue || originalValue.externalStatus !== externalStatus || originalValue.notifiedStatus !== notifiedStatus) { changed = true; return false; } }); - return changed; } - function updateStatusButtonStates() { - const changed = hasStatusChanges(); - $statusSaveButton.prop('disabled', !changed); - $statusResetButton.prop('disabled', !changed); + // Update button states + function updateButtonStates() { + const changed = hasChanges(); + $saveButton.prop('disabled', !changed); + $resetButton.prop('disabled', !changed); } + // Debounce utility to limit how often updateButtonStates is called function debounce(func, wait) { let timeout; return function () { @@ -82,30 +62,34 @@ }; } - const debouncedStatusButtonUpdate = debounce(updateStatusButtonStates, 150); + // Debounced version of updateButtonStates + const debouncedUpdateButtonStates = debounce(updateButtonStates, 150); - $statusForm.on('input', '#PortalStatusTable input[type="text"]', function () { - debouncedStatusButtonUpdate(); + // Listen for input changes, using debounced handler + $form.on('input', '#PortalStatusTable input[type="text"]', function () { + debouncedUpdateButtonStates(); }); - $statusResetButton.on('click', function (e) { + // Reset button handler + $resetButton.on('click', function (e) { e.preventDefault(); portalStatusTable.$('tbody tr').each(function () { const $row = $(this); const id = $row.find('input[type="hidden"]').val(); - const originalValue = originalStatusValues.get(id); + const originalValue = originalValues.get(id); $row.find('input[id$=".ExternalStatus"]').val(originalValue.externalStatus); $row.find('input[id$=".NotifiedStatus"]').val(originalValue.notifiedStatus); }); - updateStatusButtonStates(); + updateButtonStates(); abp.notify.info(l('ApplicantPortalSettings:ChangesReset')); }); - updateStatusButtonStates(); + // Initialize button states + updateButtonStates(); - $statusForm.on('submit', function (e) { + $form.on('submit', function (e) { e.preventDefault(); const statuses = []; @@ -114,7 +98,7 @@ portalStatusTable.$('tbody tr').each(function () { const $row = $(this); const id = $row.find('input[type="hidden"]').val(); - const externalStatus = $row.find('input[id$=".ExternalStatus"]').val().trim(); + const externalStatus = $row.find('input[type="text"]').val().trim(); if (!externalStatus) { abp.notify.warn(l('ApplicantPortalSettings:ValidationRequired')); @@ -122,8 +106,9 @@ return false; } + // Only include if value has changed const notifiedStatus = $row.find('input[id$=".NotifiedStatus"]').val().trim(); - const originalValue = originalStatusValues.get(id); + const originalValue = originalValues.get(id); if (originalValue.externalStatus !== externalStatus || originalValue.notifiedStatus !== notifiedStatus) { statuses.push({ id: id, @@ -137,132 +122,33 @@ return; } + // Check if there are any changes if (statuses.length === 0) { abp.notify.info(l('ApplicantPortalSettings:NoChanges')); return; } - abp.ui.setBusy($statusForm); + abp.ui.setBusy($form); + unity.grantManager.grantApplications.applicationStatus .updateExternalStatusLabels({ statuses: statuses }) .then(function () { abp.notify.success(l('ApplicantPortalSettings:SaveSuccess')); - statuses.forEach(function (status) { - originalStatusValues.set(status.id, { - externalStatus: status.externalStatus, - notifiedStatus: status.notifiedStatus - }); + + // Update original values after successful save + statuses.forEach(function(status) { + originalValues.set(status.id, { externalStatus: status.externalStatus, notifiedStatus: status.notifiedStatus }); }); - updateStatusButtonStates(); + + // Update button states after save + updateButtonStates(); }) .catch(function (error) { abp.notify.error(error.message || l('ApplicantPortalSettings:SaveError')); }) .always(function () { - abp.ui.clearBusy($statusForm); + abp.ui.clearBusy($form); }); }); - const $programDetailsForm = $('#ProgramDetailsForm'); - if (!$programDetailsForm.length) { - return; - } - - UnityCharacterCounter.init('#ProgramDetailsForm'); - - const $programDetailsSaveButton = $('#ProgramDetailsSaveButton'); - const $programDetailsResetButton = $('#ProgramDetailsResetButton'); - const $displayNameInput = $('#ProgramDetailsDisplayName'); - const $divisionInput = $('#ProgramDetailsDivision'); - const $branchInput = $('#ProgramDetailsBranch'); - const $descriptionInput = $('#ProgramDetailsDescription'); - - const originalProgramDetailsValues = { - displayName: $displayNameInput.val().trim(), - division: $divisionInput.val().trim(), - branch: $branchInput.val().trim(), - description: $descriptionInput.val().trim() - }; - - function getProgramDetailsValues() { - return { - displayName: $displayNameInput.val().trim(), - division: $divisionInput.val().trim(), - branch: $branchInput.val().trim(), - description: $descriptionInput.val().trim() - }; - } - - function hasProgramDetailsChanges() { - const currentValues = getProgramDetailsValues(); - return currentValues.displayName !== originalProgramDetailsValues.displayName - || currentValues.division !== originalProgramDetailsValues.division - || currentValues.branch !== originalProgramDetailsValues.branch - || currentValues.description !== originalProgramDetailsValues.description; - } - - function updateProgramDetailsButtonStates() { - const changed = hasProgramDetailsChanges(); - $programDetailsSaveButton.prop('disabled', !changed); - $programDetailsResetButton.prop('disabled', !changed); - } - - const debouncedProgramDetailsButtonUpdate = debounce(updateProgramDetailsButtonStates, 150); - - $programDetailsForm.on('input', 'input, textarea', function () { - debouncedProgramDetailsButtonUpdate(); - }); - - $programDetailsResetButton.on('click', function (e) { - e.preventDefault(); - $displayNameInput.val(originalProgramDetailsValues.displayName); - $divisionInput.val(originalProgramDetailsValues.division); - $branchInput.val(originalProgramDetailsValues.branch); - $descriptionInput.val(originalProgramDetailsValues.description); - - // Trigger input events to update character counters - $displayNameInput.trigger('input'); - $divisionInput.trigger('input'); - $branchInput.trigger('input'); - $descriptionInput.trigger('input'); - - updateProgramDetailsButtonStates(); - abp.notify.info(l('ApplicantPortalSettings:ChangesReset')); - }); - - updateProgramDetailsButtonStates(); - - $programDetailsForm.on('submit', function (e) { - e.preventDefault(); - - if (!hasProgramDetailsChanges()) { - abp.notify.info(l('ApplicantPortalSettings:NoChanges')); - return; - } - - const currentValues = getProgramDetailsValues(); - - abp.ui.setBusy($programDetailsForm); - unity.grantManager.grantApplications.applicationStatus - .updateApplicantPortalProgramDetails({ - displayName: currentValues.displayName, - division: currentValues.division, - branch: currentValues.branch, - description: currentValues.description - }) - .then(function () { - originalProgramDetailsValues.displayName = currentValues.displayName; - originalProgramDetailsValues.division = currentValues.division; - originalProgramDetailsValues.branch = currentValues.branch; - originalProgramDetailsValues.description = currentValues.description; - updateProgramDetailsButtonStates(); - abp.notify.success(l('ApplicantPortalSettings:ProgramDetailsSaveSuccess')); - }) - .catch(function (error) { - abp.notify.error(error.message || l('ApplicantPortalSettings:ProgramDetailsSaveError')); - }) - .always(function () { - abp.ui.clearBusy($programDetailsForm); - }); - }); })(jQuery); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml index bfc372b165..93cc895573 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml @@ -25,6 +25,10 @@ { } + @if (Model.ShowProgramDetails) + { + + } } @section scripts { @@ -46,6 +50,11 @@ { } + @if (Model.ShowProgramDetails) + { + + + } }
@@ -88,6 +97,12 @@ AI } + @if (Model.ShowProgramDetails) + { + + }
@@ -226,5 +241,13 @@ @await Component.InvokeAsync(typeof(Unity.AI.Web.Views.Settings.AISettingGroup.AISettingViewComponent)) } + + + @if (Model.ShowProgramDetails) + { +
+ @await Component.InvokeAsync(typeof(Unity.GrantManager.Web.Views.Settings.ProgramDetails.ProgramDetailsViewComponent)) +
+ } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs index b60b82d183..52fc1d3282 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs @@ -36,6 +36,7 @@ public class IndexModel( public bool ShowScoresheets { get; set; } public bool ShowTags { get; set; } public bool ShowAI { get; set; } + public bool ShowProgramDetails { get; set; } public async Task OnGetAsync() { @@ -62,6 +63,8 @@ public async Task OnGetAsync() ShowAI = await featureChecker.IsEnabledAsync("Unity.AI.Scoring") && await permissionChecker.IsGrantedAsync(AIPermissions.Configuration.ConfigureAI); + ShowProgramDetails = await permissionChecker.IsGrantedAsync(UnitySettingManagementPermissions.EditProgramDetails); + // Load payment data only if visible if (ShowPayments) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js similarity index 95% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js rename to applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js index 746595e1e6..864d23830c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantPortalSettings/CharacterCounter.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js @@ -2,11 +2,11 @@ window.UnityCharacterCounter = { init: function (containerSelector) { const $container = $(containerSelector || document); - + $container.find('[data-char-count]').each(function () { const $input = $(this); const maxLength = parseInt($input.attr('data-char-count'), 10); - + if (isNaN(maxLength)) { return; } @@ -14,14 +14,14 @@ // Create wrapper with relative positioning const $wrapper = $('
'); $input.wrap($wrapper); - + const $counter = $('
') .insertAfter($input); function updateCounter() { const currentLength = $input.val().length; $counter.text(currentLength + '/' + maxLength); - + const percentageUsed = (currentLength / maxLength) * 100; $counter.toggleClass('char-counter-warning', percentageUsed >= 80); $counter.toggleClass('char-counter-critical', percentageUsed >= 95); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.css new file mode 100644 index 0000000000..d6e9ad6991 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.css @@ -0,0 +1,26 @@ +.char-counter-wrapper { + position: relative; + display: block; +} + +.char-counter { + position: absolute; + top: 0; + right: 8px; + font-size: 0.65rem; + font-style: italic; + color: #6c757d; + white-space: nowrap; + padding: 4px 0; + pointer-events: none; +} + +.char-counter-warning { + color: #ffc107; + font-weight: 500; +} + +.char-counter-critical { + color: #dc3545; + font-weight: 500; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js new file mode 100644 index 0000000000..5023d186ba --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js @@ -0,0 +1,114 @@ +(function ($) { + const l = abp.localization.getResource('GrantManager'); + + function debounce(func, wait) { + let timeout; + return function () { + clearTimeout(timeout); + timeout = setTimeout(func, wait); + }; + } + + const $programDetailsForm = $('#ProgramDetailsForm'); + if (!$programDetailsForm.length) { + return; + } + + UnityCharacterCounter.init('#ProgramDetailsForm'); + + const $programDetailsSaveButton = $('#ProgramDetailsSaveButton'); + const $programDetailsResetButton = $('#ProgramDetailsResetButton'); + const $displayNameInput = $('#ProgramDetailsDisplayName'); + const $divisionInput = $('#ProgramDetailsDivision'); + const $branchInput = $('#ProgramDetailsBranch'); + const $descriptionInput = $('#ProgramDetailsDescription'); + + const originalProgramDetailsValues = { + displayName: $displayNameInput.val().trim(), + division: $divisionInput.val().trim(), + branch: $branchInput.val().trim(), + description: $descriptionInput.val().trim() + }; + + function getProgramDetailsValues() { + return { + displayName: $displayNameInput.val().trim(), + division: $divisionInput.val().trim(), + branch: $branchInput.val().trim(), + description: $descriptionInput.val().trim() + }; + } + + function hasProgramDetailsChanges() { + const currentValues = getProgramDetailsValues(); + return currentValues.displayName !== originalProgramDetailsValues.displayName + || currentValues.division !== originalProgramDetailsValues.division + || currentValues.branch !== originalProgramDetailsValues.branch + || currentValues.description !== originalProgramDetailsValues.description; + } + + function updateProgramDetailsButtonStates() { + const changed = hasProgramDetailsChanges(); + $programDetailsSaveButton.prop('disabled', !changed); + $programDetailsResetButton.prop('disabled', !changed); + } + + const debouncedProgramDetailsButtonUpdate = debounce(updateProgramDetailsButtonStates, 150); + + $programDetailsForm.on('input', 'input, textarea', function () { + debouncedProgramDetailsButtonUpdate(); + }); + + $programDetailsResetButton.on('click', function (e) { + e.preventDefault(); + $displayNameInput.val(originalProgramDetailsValues.displayName); + $divisionInput.val(originalProgramDetailsValues.division); + $branchInput.val(originalProgramDetailsValues.branch); + $descriptionInput.val(originalProgramDetailsValues.description); + + // Trigger input events to update character counters + $displayNameInput.trigger('input'); + $divisionInput.trigger('input'); + $branchInput.trigger('input'); + $descriptionInput.trigger('input'); + + updateProgramDetailsButtonStates(); + abp.notify.info(l('ProgramDetails:ChangesReset')); + }); + + updateProgramDetailsButtonStates(); + + $programDetailsForm.on('submit', function (e) { + e.preventDefault(); + + if (!hasProgramDetailsChanges()) { + abp.notify.info(l('ProgramDetails:NoChanges')); + return; + } + + const currentValues = getProgramDetailsValues(); + + abp.ui.setBusy($programDetailsForm); + unity.grantManager.grantApplications.applicationStatus + .updateProgramDetails({ + displayName: currentValues.displayName, + division: currentValues.division, + branch: currentValues.branch, + description: currentValues.description + }) + .then(function () { + originalProgramDetailsValues.displayName = currentValues.displayName; + originalProgramDetailsValues.division = currentValues.division; + originalProgramDetailsValues.branch = currentValues.branch; + originalProgramDetailsValues.description = currentValues.description; + updateProgramDetailsButtonStates(); + abp.notify.success(l('ProgramDetails:SaveSuccess')); + }) + .catch(function (error) { + abp.notify.error(error.message || l('ProgramDetails:SaveError')); + }) + .always(function () { + abp.ui.clearBusy($programDetailsForm); + }); + }); +})(jQuery); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs new file mode 100644 index 0000000000..14154d1757 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Unity.GrantManager.Web.Views.Settings.ProgramDetails; + +[Widget(AutoInitialize = true)] +public class ProgramDetailsViewComponent(IApplicationStatusService applicationStatusService) : AbpViewComponent +{ + public virtual async Task InvokeAsync() + { + var model = await applicationStatusService.GetProgramDetailsAsync(); + return View("~/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml", model); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml new file mode 100644 index 0000000000..a90f04e9c1 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml @@ -0,0 +1,65 @@ +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.GrantApplications +@using Unity.GrantManager.Localization +@model Unity.GrantManager.GrantApplications.ProgramDetailsDto + +@inject IStringLocalizer L + +
+
+

@L["ProgramDetails:Heading"]

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
From 715cfdf83782607260c28985e63af14538c21f61 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:02:30 -0700 Subject: [PATCH 08/23] [AB#33559] Build Fixes --- .../Mapping/GrantManagerWebMapperlyProfile.cs | 4 ++-- .../Pages/BulkActions/BulkPublishApplications.cshtml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs index 14a0eb5c26..18a72ebc69 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Mapping/GrantManagerWebMapperlyProfile.cs @@ -1,5 +1,5 @@ -using System; using Riok.Mapperly.Abstractions; +using System; using Unity.GrantManager.ApplicantProfile; using Unity.GrantManager.ApplicantProfile.ProfileData; using Unity.GrantManager.ApplicationForms; @@ -13,7 +13,7 @@ using Unity.GrantManager.Web.Pages.ApplicantContact; using Unity.GrantManager.Web.Pages.ApplicationContact; using Unity.GrantManager.Web.Pages.ApplicationForms.ViewModels; -using Unity.GrantManager.Web.Pages.BulkActions.PublishApplications; +using Unity.GrantManager.Web.Pages.BulkActions; using Unity.GrantManager.Web.Pages.Sites.ViewModels; using Unity.GrantManager.Web.Views.Shared.Components.ApplicantInfo; using Unity.GrantManager.Web.Views.Shared.Components.SummaryWidget; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml index 7caf8e7e69..48ee8deb53 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml @@ -8,7 +8,6 @@ Layout = null; } -
From 0ed6cf7f1ff74501c7c3e59fcd044df3669b45d1 Mon Sep 17 00:00:00 2001 From: Andre Goncalves <98196495+AndreGAot@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:52:19 -0700 Subject: [PATCH 09/23] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Queries/ApplicantProfileQueryService.cs | 5 ++--- .../ApplicationStatusAppService.cs | 6 ++++-- .../UnitySettingManagementPermissions.cs | 4 ++-- .../ProgramDetails/CharacterCounter.js | 19 +++++++++++++------ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs index 0b1a6ad134..8ec9a9a378 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs @@ -100,9 +100,8 @@ public async Task> GetApplicantTenantsAsync(ApplicantPr using (currentTenant.Change(null)) { var tenantIds = mappings.Select(m => m.TenantId).ToHashSet(); - var allTenants = await tenantRepository.GetListAsync(); - var tenants = allTenants.Where(t => tenantIds.Contains(t.Id)).ToList(); - + var queryable = await tenantRepository.GetQueryableAsync(); + var tenants = await queryable.Where(t => tenantIds.Contains(t.Id)).ToListAsync(); // Create a map of tenant ID to DisplayName (with fallback to Name) var tenantDisplayNames = tenants.ToDictionary( t => t.Id, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs index ff67f30dca..cf64090d8f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs @@ -86,8 +86,10 @@ public virtual async Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto inpu tenant.SetProperty("Branch", NormalizeProgramDetail(input.Branch)); tenant.SetProperty("Description", NormalizeProgramDetail(input.Description)); - await _tenantRepository.UpdateAsync(tenant); - } + using (CurrentTenant.Change(null)) + { + await _tenantRepository.UpdateAsync(tenant); + } protected virtual async Task GetCurrentTenantForProgramDetailsAsync() { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs index 03405bdd5e..18d1637c55 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Permissions/UnitySettingManagementPermissions.cs @@ -7,7 +7,7 @@ public static class UnitySettingManagementPermissions public const string BackgroundJobSettings = "SettingManagement.GrantManager"; - public const string ConfigurePayments = "SettingManagement.ConfigurePayments"; + public const string ConfigurePayments = GroupName + ".ConfigurePayments"; - public const string EditProgramDetails = "SettingManagement.EditProgramDetails"; + public const string EditProgramDetails = GroupName + ".EditProgramDetails"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js index 864d23830c..61134ae09b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js @@ -11,20 +11,27 @@ return; } - // Create wrapper with relative positioning - const $wrapper = $('
'); - $input.wrap($wrapper); + // Create wrapper with relative positioning (idempotent) + if ($input.data('unityCharCounterInitialized')) { + return; + } + $input.data('unityCharCounterInitialized', true); - const $counter = $('
') - .insertAfter($input); + if (!$input.parent().hasClass('char-counter-wrapper')) { + $input.wrap('
'); + } + let $counter = $input.siblings('.char-counter'); + if (!$counter.length) { + $counter = $('
').insertAfter($input); + } function updateCounter() { const currentLength = $input.val().length; $counter.text(currentLength + '/' + maxLength); const percentageUsed = (currentLength / maxLength) * 100; - $counter.toggleClass('char-counter-warning', percentageUsed >= 80); $counter.toggleClass('char-counter-critical', percentageUsed >= 95); + $counter.toggleClass('char-counter-warning', percentageUsed >= 80 && percentageUsed < 95); } $input.on('input', updateCounter); From 45ab75d21afdf95b90a93d67fb8fc4b73e2d505e Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 9 Jul 2026 16:26:57 -0700 Subject: [PATCH 10/23] AB#33530 code review fixes --- .../ApplicantProfile/Queries/ApplicantProfileQueryService.cs | 2 +- .../GrantApplications/ApplicationStatusAppService.cs | 1 + .../Views/Settings/ProgramDetails/CharacterCounter.js | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs index 8ec9a9a378..f9571442f7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs @@ -23,7 +23,7 @@ namespace Unity.GrantManager.ApplicantProfile public class ApplicantProfileQueryService( ICurrentTenant currentTenant, IRepository applicantTenantMapRepository, - ITenantRepository tenantRepository, + IRepository tenantRepository, IEnumerable dataProviders, ISettingProvider settingProvider, ILogger logger) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs index cf64090d8f..d90072d0e2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs @@ -90,6 +90,7 @@ public virtual async Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto inpu { await _tenantRepository.UpdateAsync(tenant); } + } protected virtual async Task GetCurrentTenantForProgramDetailsAsync() { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js index 61134ae09b..573b8e6e0d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/CharacterCounter.js @@ -5,9 +5,9 @@ $container.find('[data-char-count]').each(function () { const $input = $(this); - const maxLength = parseInt($input.attr('data-char-count'), 10); + const maxLength = Number.parseInt($input.attr('data-char-count'), 10); - if (isNaN(maxLength)) { + if (Number.isNaN(maxLength)) { return; } From 4af03aa56ad215b86160f581b0063b2b1e2d30c1 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:05:25 -0700 Subject: [PATCH 11/23] [AB#33559] Implement bulk publish modal functionality --- .../BulkPublishApplications.cshtml | 19 ++++--- .../BulkActions/BulkPublishApplications.js | 50 ++++++++++++------- .../Components/ActionBar/Default.cshtml | 2 +- .../Shared/Components/ActionBar/Default.js | 10 ++-- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml index 48ee8deb53..e46dad4b65 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml @@ -31,7 +31,7 @@
@Model.BulkApplications[i].ReferenceNo
-
+
@(string.IsNullOrWhiteSpace(Model.BulkApplications[i].ApplicantName) ? "Applicant Name" : @Model.BulkApplications[i].ApplicantName)
@@ -46,7 +46,7 @@ size="Small" icon-type="Other" icon="fa fa-times" - class="m-0 p-0" + class="removeRow m-0 p-0" data-parameter="@Model.BulkApplications[i].ApplicationId" /> @@ -87,10 +87,10 @@

- Error You have exceeded the maximum number of items for bulk publishing. Please reduce the number to 50 or fewer + Error You have exceeded the maximum number of items for bulk status publishing. Please reduce the number to 50 or fewer

- Error @Model.MaxBatchCountExceededError + Error You have no items selected for bulk status publishing. Please close this prompt to continue.

@@ -104,20 +104,19 @@ } - - + data-dismiss="modal" + text="@L["ApplicationBatchApprovalRequest:CancelButtonText"].Value"> diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js index 4a4d5f2e61..e7e5e40b7c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js @@ -17,12 +17,12 @@ function setMaxCountError(visible) { function enableBulkPublishSubmit() { $("#bulkPublishApplicationsModal") .find('#btnSubmitBatchPublish').prop("disabled", false); -} + } function disableBulkPublishSubmit() { $("#bulkPublishApplicationsModal") .find('#btnSubmitBatchPublish').prop("disabled", true); -} + } function closePublish() { $('#bulkPublishApplicationsModal').modal('hide'); @@ -37,33 +37,45 @@ function runValidations() { }); isValid = validBatchCount(); - $('#batch-action-summary').toggleClass('d-none', isValid); + $('#batch-action-summary').toggleClass('d-none', isValid); - if (isValid) { + if (isValid) { enableBulkPublishSubmit(); - } else { + } else { disableBulkPublishSubmit(); + } } -} function validBatchCount() { - let applicationsCount = $('#ApplicationsCount').val(); - let maxBatchCount = $('#MaxBatchCount').val(); - let validationMaxValid = true; - let validationMinValid = true; + let applicationsCount = Number.parseInt(String($('#ApplicationsCount').val() ?? ''), 10); + let maxBatchCount = Number.parseInt(String($('#MaxBatchCount').val() ?? ''), 10); + let validationMaxValid = true; + let validationMinValid = true; - if (maxBatchCount <= applicationsCount) { - validationMaxValid = false; - } else if (applicationsCount === 0) { - validationMinValid = false; - } + const hasInvalidNumber = Number.isNaN(applicationsCount) || Number.isNaN(maxBatchCount); + if (hasInvalidNumber) { + return false; + } + + if (maxBatchCount <= applicationsCount) { + validationMaxValid = false; + } else if (applicationsCount == 0) { + validationMinValid = false; + } - $('#maxCountWarning').toggleClass('d-none', validationMaxValid); - $('#minCountWarning').toggleClass('d-none', validationMinValid); + $('#maxCountWarning').toggleClass('d-none', validationMaxValid); + $('#minCountWarning').toggleClass('d-none', validationMinValid); + $('.batch-action-card').toggleClass('d-none', !validationMinValid); - return validationMaxValid && validationMinValid; + return validationMaxValid && validationMinValid; } $(function () { - runValidations(); + class BulkPublishModal { + initModal(publicApi, args) { + runValidations(); + } + } + + abp.modals.BulkPublishModal = BulkPublishModal; }); \ No newline at end of file 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 6fce97e151..aae517a44b 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 @@ -79,7 +79,7 @@ text="@L["ApplicationList:ApproveButton"].Value" /> } - Date: Fri, 10 Jul 2026 09:18:23 -0700 Subject: [PATCH 12/23] AB#33530 extract Program Details into its own application service Program Details management (Tenant DisplayName/Division/Branch/Description) was bolted onto ApplicationStatusAppService despite being unrelated to application status labels. Moved it to a dedicated IProgramDetailsAppService in the SettingManagement namespace, mirroring the existing WorksheetConfigurationAppService pattern, with unit test coverage. Co-Authored-By: Claude Sonnet 5 --- .../IApplicationStatusService.cs | 2 - .../IProgramDetailsAppService.cs | 10 ++ .../ProgramDetailsDto.cs | 2 +- .../UpdateProgramDetailsDto.cs | 2 +- .../ApplicationStatusAppService.cs | 66 +------ .../ProgramDetailsAppService.cs | 66 +++++++ .../ProgramDetailsConsts.cs | 2 +- .../Settings/ProgramDetails/ProgramDetails.js | 2 +- .../ProgramDetailsViewComponent.cs | 6 +- .../ProgramDetailsViewComponent.cshtml | 4 +- .../ProgramDetailsAppServiceTests.cs | 161 ++++++++++++++++++ 11 files changed, 247 insertions(+), 76 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IProgramDetailsAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{GrantApplications => SettingManagement}/ProgramDetailsDto.cs (86%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/{GrantApplications => SettingManagement}/UpdateProgramDetailsDto.cs (91%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/SettingManagement/ProgramDetailsAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/{GrantApplications => SettingManagement}/ProgramDetailsConsts.cs (82%) create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/SettingManagement/ProgramDetailsAppServiceTests.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs index d1ee48ccf9..de39f48b06 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationStatusService.cs @@ -8,7 +8,5 @@ public interface IApplicationStatusService : IApplicationService { Task> GetListAsync(); Task> GetApplicantPortalStatusListAsync(); - Task GetProgramDetailsAsync(); Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatusExternalLabelsDto input); - Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto input); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IProgramDetailsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IProgramDetailsAppService.cs new file mode 100644 index 0000000000..589462dae0 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IProgramDetailsAppService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.SettingManagement; + +public interface IProgramDetailsAppService : IApplicationService +{ + Task GetProgramDetailsAsync(); + Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto input); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/ProgramDetailsDto.cs similarity index 86% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ProgramDetailsDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/ProgramDetailsDto.cs index fb231c3a3a..7ca2ea34d4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/ProgramDetailsDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/ProgramDetailsDto.cs @@ -1,6 +1,6 @@ using System; -namespace Unity.GrantManager.GrantApplications; +namespace Unity.GrantManager.SettingManagement; [Serializable] public class ProgramDetailsDto diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/UpdateProgramDetailsDto.cs similarity index 91% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/UpdateProgramDetailsDto.cs index 775b75b2dc..25eb70a238 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/UpdateProgramDetailsDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/UpdateProgramDetailsDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Unity.GrantManager.GrantApplications; +namespace Unity.GrantManager.SettingManagement; public class UpdateProgramDetailsDto { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs index d90072d0e2..648088d64b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/ApplicationStatusAppService.cs @@ -1,16 +1,10 @@ using Microsoft.AspNetCore.Authorization; -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Unity.GrantManager.Applications; -using Unity.GrantManager.Permissions; -using Volo.Abp.Authorization; using Volo.Abp.Application.Services; -using Volo.Abp.Data; using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities; -using Volo.Abp.TenantManagement; namespace Unity.GrantManager.GrantApplications; @@ -20,14 +14,10 @@ namespace Unity.GrantManager.GrantApplications; public class ApplicationStatusAppService : ApplicationService, IApplicationStatusService { private readonly IApplicationStatusRepository _applicationStatusRepository; - private readonly ITenantRepository _tenantRepository; - public ApplicationStatusAppService( - IApplicationStatusRepository repository, - ITenantRepository tenantRepository) + public ApplicationStatusAppService(IApplicationStatusRepository repository) { _applicationStatusRepository = repository; - _tenantRepository = tenantRepository; } public virtual async Task> GetListAsync() @@ -44,20 +34,6 @@ public virtual async Task> GetApplicantPortalSta return ObjectMapper.Map, List>(statuses.OrderBy(s => s.StatusCode).ToList()); } - [Authorize(UnitySettingManagementPermissions.EditProgramDetails)] - public virtual async Task GetProgramDetailsAsync() - { - var tenant = await GetCurrentTenantForProgramDetailsAsync(); - - return new ProgramDetailsDto - { - DisplayName = GetExtraPropertyValue(tenant, "DisplayName"), - Division = GetExtraPropertyValue(tenant, "Division"), - Branch = GetExtraPropertyValue(tenant, "Branch"), - Description = GetExtraPropertyValue(tenant, "Description") - }; - } - public virtual async Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatusExternalLabelsDto input) { // Load all statuses in a single query by IDs @@ -75,44 +51,4 @@ public virtual async Task UpdateExternalStatusLabelsAsync(UpdateApplicationStatu } } } - - [Authorize(UnitySettingManagementPermissions.EditProgramDetails)] - public virtual async Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto input) - { - var tenant = await GetCurrentTenantForProgramDetailsAsync(); - - tenant.SetProperty("DisplayName", NormalizeProgramDetail(input.DisplayName)); - tenant.SetProperty("Division", NormalizeProgramDetail(input.Division)); - tenant.SetProperty("Branch", NormalizeProgramDetail(input.Branch)); - tenant.SetProperty("Description", NormalizeProgramDetail(input.Description)); - - using (CurrentTenant.Change(null)) - { - await _tenantRepository.UpdateAsync(tenant); - } - } - - protected virtual async Task GetCurrentTenantForProgramDetailsAsync() - { - if (!CurrentTenant.Id.HasValue) - { - throw new AbpAuthorizationException("Program details can only be managed within a tenant context."); - } - - var tenantId = CurrentTenant.Id.Value; - using (CurrentTenant.Change(null)) - { - return await _tenantRepository.FindAsync(tenantId) ?? throw new EntityNotFoundException(typeof(Tenant), tenantId); - } - } - - protected virtual string GetExtraPropertyValue(Tenant tenant, string propertyName) - { - return tenant.GetProperty(propertyName)?.ToString() ?? string.Empty; - } - - protected virtual string NormalizeProgramDetail(string? value) - { - return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); - } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/SettingManagement/ProgramDetailsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/SettingManagement/ProgramDetailsAppService.cs new file mode 100644 index 0000000000..632d7c7a00 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/SettingManagement/ProgramDetailsAppService.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Unity.GrantManager.Permissions; +using Volo.Abp.Authorization; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities; +using Volo.Abp.TenantManagement; + +namespace Unity.GrantManager.SettingManagement; + +[Authorize(UnitySettingManagementPermissions.EditProgramDetails)] +public class ProgramDetailsAppService(ITenantRepository tenantRepository) + : GrantManagerAppService, IProgramDetailsAppService +{ + public virtual async Task GetProgramDetailsAsync() + { + var tenant = await GetCurrentTenantAsync(); + + return new ProgramDetailsDto + { + DisplayName = GetExtraPropertyValue(tenant, "DisplayName"), + Division = GetExtraPropertyValue(tenant, "Division"), + Branch = GetExtraPropertyValue(tenant, "Branch"), + Description = GetExtraPropertyValue(tenant, "Description") + }; + } + + public virtual async Task UpdateProgramDetailsAsync(UpdateProgramDetailsDto input) + { + var tenant = await GetCurrentTenantAsync(); + + tenant.SetProperty("DisplayName", NormalizeProgramDetail(input.DisplayName)); + tenant.SetProperty("Division", NormalizeProgramDetail(input.Division)); + tenant.SetProperty("Branch", NormalizeProgramDetail(input.Branch)); + tenant.SetProperty("Description", NormalizeProgramDetail(input.Description)); + + using (CurrentTenant.Change(null)) + { + await tenantRepository.UpdateAsync(tenant); + } + } + + protected virtual async Task GetCurrentTenantAsync() + { + if (!CurrentTenant.Id.HasValue) + { + throw new AbpAuthorizationException("Program details can only be managed within a tenant context."); + } + + var tenantId = CurrentTenant.Id.Value; + using (CurrentTenant.Change(null)) + { + return await tenantRepository.FindAsync(tenantId) ?? throw new EntityNotFoundException(typeof(Tenant), tenantId); + } + } + + protected virtual string GetExtraPropertyValue(Tenant tenant, string propertyName) + { + return tenant.GetProperty(propertyName)?.ToString() ?? string.Empty; + } + + protected virtual string NormalizeProgramDetail(string? value) + { + return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/SettingManagement/ProgramDetailsConsts.cs similarity index 82% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/SettingManagement/ProgramDetailsConsts.cs index 86e8df49d4..f8d16973f0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/ProgramDetailsConsts.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/SettingManagement/ProgramDetailsConsts.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.GrantApplications; +namespace Unity.GrantManager.SettingManagement; public static class ProgramDetailsConsts { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js index 5023d186ba..58335f06a6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetails.js @@ -89,7 +89,7 @@ const currentValues = getProgramDetailsValues(); abp.ui.setBusy($programDetailsForm); - unity.grantManager.grantApplications.applicationStatus + unity.grantManager.settingManagement.programDetails .updateProgramDetails({ displayName: currentValues.displayName, division: currentValues.division, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs index 14154d1757..54f16500cf 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cs @@ -1,17 +1,17 @@ using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; -using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.SettingManagement; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; namespace Unity.GrantManager.Web.Views.Settings.ProgramDetails; [Widget(AutoInitialize = true)] -public class ProgramDetailsViewComponent(IApplicationStatusService applicationStatusService) : AbpViewComponent +public class ProgramDetailsViewComponent(IProgramDetailsAppService programDetailsAppService) : AbpViewComponent { public virtual async Task InvokeAsync() { - var model = await applicationStatusService.GetProgramDetailsAsync(); + var model = await programDetailsAppService.GetProgramDetailsAsync(); return View("~/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml", model); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml index a90f04e9c1..ec0636f133 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml @@ -1,7 +1,7 @@ @using Microsoft.Extensions.Localization -@using Unity.GrantManager.GrantApplications +@using Unity.GrantManager.SettingManagement @using Unity.GrantManager.Localization -@model Unity.GrantManager.GrantApplications.ProgramDetailsDto +@model Unity.GrantManager.SettingManagement.ProgramDetailsDto @inject IStringLocalizer L diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/SettingManagement/ProgramDetailsAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/SettingManagement/ProgramDetailsAppServiceTests.cs new file mode 100644 index 0000000000..d498190cda --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/SettingManagement/ProgramDetailsAppServiceTests.cs @@ -0,0 +1,161 @@ +using System; +using System.Threading.Tasks; +using NSubstitute; +using Shouldly; +using Volo.Abp.Authorization; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TenantManagement; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.SettingManagement; + +public class ProgramDetailsAppServiceTests(ITestOutputHelper outputHelper) : GrantManagerApplicationTestBase(outputHelper) +{ + [Fact] + public async Task GetProgramDetailsAsync_WithinTenantContext_ShouldReturnMappedExtraProperties() + { + var tenantId = Guid.NewGuid(); + var tenant = CreateTenant(tenantId); + tenant.SetProperty("DisplayName", "My Program"); + tenant.SetProperty("Division", "Housing Division"); + tenant.SetProperty("Branch", "Grants Branch"); + tenant.SetProperty("Description", "Sample description"); + + var tenantRepository = Substitute.For(); + tenantRepository.FindAsync(tenantId).Returns(tenant); + + var appService = CreateAppService(tenantRepository); + var currentTenant = GetRequiredService(); + + ProgramDetailsDto result; + using (currentTenant.Change(tenantId)) + { + result = await appService.GetProgramDetailsAsync(); + } + + result.DisplayName.ShouldBe("My Program"); + result.Division.ShouldBe("Housing Division"); + result.Branch.ShouldBe("Grants Branch"); + result.Description.ShouldBe("Sample description"); + } + + [Fact] + public async Task GetProgramDetailsAsync_WithMissingExtraProperties_ShouldReturnEmptyStrings() + { + var tenantId = Guid.NewGuid(); + var tenant = CreateTenant(tenantId); + + var tenantRepository = Substitute.For(); + tenantRepository.FindAsync(tenantId).Returns(tenant); + + var appService = CreateAppService(tenantRepository); + var currentTenant = GetRequiredService(); + + ProgramDetailsDto result; + using (currentTenant.Change(tenantId)) + { + result = await appService.GetProgramDetailsAsync(); + } + + result.DisplayName.ShouldBe(string.Empty); + result.Division.ShouldBe(string.Empty); + result.Branch.ShouldBe(string.Empty); + result.Description.ShouldBe(string.Empty); + } + + [Fact] + public async Task GetProgramDetailsAsync_WithoutTenantContext_ShouldThrowAbpAuthorizationException() + { + var tenantRepository = Substitute.For(); + var appService = CreateAppService(tenantRepository); + var currentTenant = GetRequiredService(); + + using (currentTenant.Change(null)) + { + await Should.ThrowAsync(() => appService.GetProgramDetailsAsync()); + } + } + + [Fact] + public async Task GetProgramDetailsAsync_WithUnknownTenant_ShouldThrowEntityNotFoundException() + { + var tenantId = Guid.NewGuid(); + var tenantRepository = Substitute.For(); + tenantRepository.FindAsync(tenantId).Returns((Tenant?)null); + + var appService = CreateAppService(tenantRepository); + var currentTenant = GetRequiredService(); + + using (currentTenant.Change(tenantId)) + { + await Should.ThrowAsync(() => appService.GetProgramDetailsAsync()); + } + } + + [Fact] + public async Task UpdateProgramDetailsAsync_ShouldNormalizeAndPersistTrimmedValues() + { + var tenantId = Guid.NewGuid(); + var tenant = CreateTenant(tenantId); + + var tenantRepository = Substitute.For(); + tenantRepository.FindAsync(tenantId).Returns(tenant); + + var appService = CreateAppService(tenantRepository); + var currentTenant = GetRequiredService(); + + var input = new UpdateProgramDetailsDto + { + DisplayName = " My Program ", + Division = " Housing Division ", + Branch = null, + Description = " " + }; + + using (currentTenant.Change(tenantId)) + { + await appService.UpdateProgramDetailsAsync(input); + } + + tenant.GetProperty("DisplayName")?.ToString().ShouldBe("My Program"); + tenant.GetProperty("Division")?.ToString().ShouldBe("Housing Division"); + tenant.GetProperty("Branch")?.ToString().ShouldBe(string.Empty); + tenant.GetProperty("Description")?.ToString().ShouldBe(string.Empty); + await tenantRepository.Received(1).UpdateAsync(tenant); + } + + [Fact] + public async Task UpdateProgramDetailsAsync_WithoutTenantContext_ShouldThrowAbpAuthorizationException() + { + var tenantRepository = Substitute.For(); + var appService = CreateAppService(tenantRepository); + var currentTenant = GetRequiredService(); + + var input = new UpdateProgramDetailsDto { DisplayName = "Test" }; + + using (currentTenant.Change(null)) + { + await Should.ThrowAsync(() => appService.UpdateProgramDetailsAsync(input)); + } + } + + private ProgramDetailsAppService CreateAppService(ITenantRepository tenantRepository) + { + var appService = new ProgramDetailsAppService(tenantRepository) + { + LazyServiceProvider = GetRequiredService() + }; + return appService; + } + + private static Tenant CreateTenant(Guid id) + { + var tenant = (Tenant)Activator.CreateInstance(typeof(Tenant), nonPublic: true)!; + EntityHelper.TrySetId(tenant, () => id); + return tenant; + } +} From eb38200d0694551fde22a8a36e699177b8cf5ad6 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:18:28 -0700 Subject: [PATCH 13/23] [AB#33769] Publish button and notification text update --- .../Localization/GrantManager/en.json | 9 +++++---- .../Views/Shared/Components/DetailsActionBar/Default.js | 4 +++- .../Components/PaymentConfiguration/Default.cshtml | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) 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 f65eb41ae5..f15e307a88 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 @@ -287,15 +287,16 @@ "AssessmentResultsView:AssessmentResultsForm.NotificationDate": "Notification Date", "AssessmentResultsView:AssessmentResultsForm.RiskRanking": "Risk Ranking", "AssessmentResultsView:AssessmentResultsForm.Published": "Published", - "DetailsActionBar:PublishButton": "Publish", - "DetailsActionBar:UnpublishButton": "Unpublish", + "DetailsActionBar:PublishButton": "Publish Status", + "DetailsActionBar:UnpublishButton": "Unpublish Status", "DetailsActionBar:ConfirmActionTitle": "Confirm Action", "DetailsActionBar:PublishConfirmationText": "Are you sure you want to publish this application status?", "DetailsActionBar:UnpublishConfirmationText": "Are you sure you want to unpublish this application status?", "DetailsActionBar:ConfirmButton": "Confirm", "DetailsActionBar:CancelButton": "Cancel", - "DetailsActionBar:PublishStatusUpdated": "Publish status has been updated.", - "DetailsActionBar:PublishStatusUpdateFailed": "Failed to update publish status.", + "DetailsActionBar:PublishStatusUpdatedToast": "Application status has been published.", + "DetailsActionBar:UnpublishStatusUpdatedToast": "Application status has been unpublished.", + "DetailsActionBar:PublishStatusUpdateFailed": "Failed to update status.", "GrantApplication:ActionButtonName": "Status Actions", "GrantApplication:ActionButton.RedStopWarning": "This application is from an applicant with a Red-Stop. Status actions are not permitted.", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js index b0f02c82ad..3085043c48 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js @@ -290,7 +290,9 @@ $(function () { ? l('DetailsActionBar:UnpublishButton') : l('DetailsActionBar:PublishButton')); - abp.notify.success(l('DetailsActionBar:PublishStatusUpdated')); + abp.notify.success(nextPublishedState + ? l('DetailsActionBar:PublishStatusUpdatedToast') + : l('DetailsActionBar:UnpublishStatusUpdatedToast')); PubSub.publish('application_status_changed', nextPublishedState ? 'Publish' : 'Unpublish'); PubSub.publish('refresh_detail_panel_summary'); }) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.cshtml index d3a0219eaf..a8653662a7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.cshtml @@ -75,7 +75,7 @@ The individual Approval Thresholds assigned to each l2_approver will still take precedence if they are lower than the threshold specified in this form.

- +
From 63f0950200bb1d47b0e9bd0d384582cda852f5a9 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:40:17 -0700 Subject: [PATCH 14/23] [AB#33567] Modular permission to publish and bulk publish status --- .../Constants/UnitySelector.Override.cs | 11 +++++++++++ .../GrantApplicationPermissionDefinitionProvider.cs | 6 ++++++ .../Localization/GrantManager/en.json | 5 +++++ .../Permissions/PermissionGrantsDataSeeder.cs | 9 +++++++++ .../Identity/PolicyRegistrant.cs | 12 +++++++++++- 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.Override.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.Override.cs index 913fe23187..b61eba84f8 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.Override.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Constants/UnitySelector.Override.cs @@ -49,4 +49,15 @@ public static partial class Update } } } + + public static partial class Application + { + public static partial class Status + { + public const string Default = "Unity.GrantManager.ApplicationManagement.Application.Status"; + public const string Publish = "Unity.GrantManager.ApplicationManagement.Application.Status.Publish"; + public const string Unpublish = "Unity.GrantManager.ApplicationManagement.Application.Status.Unpublish"; + public const string BulkPublish = "Unity.GrantManager.ApplicationManagement.Application.Status.BulkPublish"; + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index b9ef4f56e3..91ed907d96 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -70,6 +70,12 @@ public override void Define(IPermissionDefinitionContext context) approvalPermissions.AddChild(GrantApplicationPermissions.Approvals.DeferAfterApproval, L("Permission:GrantApplicationManagement.Approvals.DeferAfterApproval")); approvalPermissions.AddChild(GrantApplicationPermissions.Approvals.BulkApplicationApproval, L("Permission:GrantApplicationManagement.Approvals.BulkApplicationApproval")); + // External Status Visibility + var statusVisibilityPermissions = grantApplicationPermissionsGroup.AddPermission(UnitySelector.Application.Status.Default, L(UnitySelector.Application.Status.Default)); + statusVisibilityPermissions.AddUnityChild(UnitySelector.Application.Status.Publish); + statusVisibilityPermissions.AddUnityChild(UnitySelector.Application.Status.Unpublish); + statusVisibilityPermissions.AddUnityChild(UnitySelector.Application.Status.BulkPublish); + // Comments var appCommentPermissions = grantApplicationPermissionsGroup.AddPermission(GrantApplicationPermissions.Comments.Default, L("Permission:GrantApplicationManagement.Comments.Default")); appCommentPermissions.AddChild(GrantApplicationPermissions.Comments.Add, L("Permission:GrantApplicationManagement.Comments.Add")); 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 f15e307a88..543468f01c 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 @@ -246,6 +246,11 @@ "Unity.GrantManager.ApplicationManagement.Payment.Supplier.Update": "Edit Supplier Info", "Unity.GrantManager.ApplicationManagement.Payment.PaymentList": "Payment List", + "Unity.GrantManager.ApplicationManagement.Application.Status": "External Status Visibility", + "Unity.GrantManager.ApplicationManagement.Application.Status.Publish": "Publish", + "Unity.GrantManager.ApplicationManagement.Application.Status.Unpublish": "Unpublish", + "Unity.GrantManager.ApplicationManagement.Application.Status.BulkPublish": "Bulk Publish", + "Permission:GrantApplicationManagement.ProjectInfo": "Project Info", "Permission:GrantApplicationManagement.ProjectInfo.Update": "Project Info Update", "Permission:GrantApplicationManagement.ProjectInfo.Update.UpdateFinalStateFields": "Edit Project Info Fields After Approval", 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 833ac0b40c..686fc0c8df 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -105,6 +105,13 @@ public PermissionGrantsDataSeeder(IPermissionDataSeeder permissionDataSeeder) UnitySelector.Payment.Tags.Delete, ]; + public readonly List ExternalStatusVisibility_CommonPermissions = [ + UnitySelector.Application.Status.Default, + UnitySelector.Application.Status.Publish, + UnitySelector.Application.Status.Unpublish, + UnitySelector.Application.Status.BulkPublish + ]; + public async Task SeedAsync(DataSeedContext context) { // Default permission grants based on role @@ -143,6 +150,7 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, .. NotificationsScheduling_CommonPermissions, .. Dashboard_CommonPermissions, .. Tags_CommonPermissions, + .. ExternalStatusVisibility_CommonPermissions, AIPermissions.Configuration.ConfigureAI, FlexPermissions.Worksheets.Default, FlexPermissions.Worksheets.Delete @@ -208,6 +216,7 @@ await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, .. NotificationsScheduling_CommonPermissions, .. Dashboard_CommonPermissions, .. Tags_CommonPermissions, + .. ExternalStatusVisibility_CommonPermissions, // Role Specific Permissions UnitySelector.Project.Summary.Update.UpdateFinalStateFields, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs index 777d5f6066..817b59cade 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs @@ -266,7 +266,17 @@ internal static void Register(ServiceConfigurationContext context) authorizationBuilder.AddPolicy(ReportingPermissions.Configuration.Update, policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Update)); authorizationBuilder.AddPolicy(ReportingPermissions.Configuration.Delete, - policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Delete)); + policy => policy.RequireClaim(PermissionConstant, ReportingPermissions.Configuration.Delete)); + + // Application - External Status Visibility + authorizationBuilder.AddPolicy(UnitySelector.Application.Status.Default, + policy => policy.RequireClaim(PermissionConstant, UnitySelector.Application.Status.Default)); + authorizationBuilder.AddPolicy(UnitySelector.Application.Status.Publish, + policy => policy.RequireClaim(PermissionConstant, UnitySelector.Application.Status.Publish)); + authorizationBuilder.AddPolicy(UnitySelector.Application.Status.Unpublish, + policy => policy.RequireClaim(PermissionConstant, UnitySelector.Application.Status.Unpublish)); + authorizationBuilder.AddPolicy(UnitySelector.Application.Status.BulkPublish, + policy => policy.RequireClaim(PermissionConstant, UnitySelector.Application.Status.BulkPublish)); } } From 153234f3914b6c41f0763b4fdd5983962220c8ab Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:01:08 -0700 Subject: [PATCH 15/23] [AB#33567] Add authorization for Publish UI elements --- .../GrantApplications/BulkApprovalsAppService.cs | 3 +++ .../Views/Shared/Components/ActionBar/Default.cshtml | 3 +++ .../Views/Shared/Components/DetailsActionBar/Default.js | 6 ++++++ .../Shared/Components/DetailsActionBar/DetailsActionBar.cs | 5 +++-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs index d061a6703a..eeb5e4c5e9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs @@ -9,6 +9,7 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Events; using Unity.GrantManager.Permissions; +using Unity.Modules.Shared; using Volo.Abp.EventBus.Local; using Volo.Abp.Uow; @@ -254,6 +255,7 @@ private static OperationAuthorizationRequirement GetActionAuthorizationRequireme /// /// /// + [Authorize(UnitySelector.Application.Status.BulkPublish)] public async Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true) { var applicationsQuery = await applicationRepository.GetQueryableAsync(); @@ -288,6 +290,7 @@ where applicationGuids.Contains(application.Id) && (!excludePublished || !applic /// /// The GUIDs of the applications to be bulk published. /// A task representing the asynchronous operation. + [Authorize(UnitySelector.Application.Status.BulkPublish)] public async Task BulkPublishApplications(Guid[] applicationGuids) { var applications = await applicationRepository 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 aae517a44b..274ae5d1a7 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 @@ -79,11 +79,14 @@ text="@L["ApplicationList:ApproveButton"].Value" /> } + @if (await PermissionChecker.IsGrantedAsync(UnitySelector.Application.Status.BulkPublish)) + { + } @if(await PermissionChecker.IsGrantedAsync(UnitySelector.Application.Tags.Create) || await PermissionChecker.IsGrantedAsync(UnitySelector.Application.Tags.Delete)) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js index 3085043c48..3df9968bc0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js @@ -293,6 +293,12 @@ $(function () { abp.notify.success(nextPublishedState ? l('DetailsActionBar:PublishStatusUpdatedToast') : l('DetailsActionBar:UnpublishStatusUpdatedToast')); + + let canPublishStatus = abp.auth.isGranted("Unity.GrantManager.ApplicationManagement.Application.Status.Publish"); + let canUnpublishStatus = abp.auth.isGranted("Unity.GrantManager.ApplicationManagement.Application.Status.Unpublish"); + let canUpdateExternalStatusVisibility = (!nextPublishedState && canPublishStatus) || (nextPublishedState && canUnpublishStatus); + $('#togglePublishStatusBtn').prop('disabled', !canUpdateExternalStatusVisibility); + PubSub.publish('application_status_changed', nextPublishedState ? 'Publish' : 'Unpublish'); PubSub.publish('refresh_detail_panel_summary'); }) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/DetailsActionBar.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/DetailsActionBar.cs index 34c4a29f37..dd99427fc3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/DetailsActionBar.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/DetailsActionBar.cs @@ -23,13 +23,14 @@ public async Task InvokeAsync(Guid applicationId) SelectedApplicationId = applicationId; var application = await grantApplicationAppService.GetBasicAsync(SelectedApplicationId); - var canUpdateExternalStatusVisibility = await authorizationService.IsGrantedAsync(UnitySelector.Review.AssessmentResults.Update.Default); + var canPublishStatus = await authorizationService.IsGrantedAnyAsync(UnitySelector.Application.Status.Publish); + var canUnpublishStatus = await authorizationService.IsGrantedAnyAsync(UnitySelector.Application.Status.Unpublish); return View(new DetailsActionBarViewModel { ApplicationId = SelectedApplicationId, ExternalStatusVisibility = application.ExternalStatusVisibility, - CanUpdateExternalStatusVisibility = canUpdateExternalStatusVisibility + CanUpdateExternalStatusVisibility = (!application.ExternalStatusVisibility && canPublishStatus) || (application.ExternalStatusVisibility && canUnpublishStatus), }); } } \ No newline at end of file From 8c2f3b364bb7865c10b6fbbb38f665a031057797 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:44:14 -0700 Subject: [PATCH 16/23] [AB#33559] Bulk Publish Code Quality Fixes --- .../BulkApprovalsAppService.cs | 2 +- .../Localization/GrantManager/en.json | 6 ++++- .../BulkPublishApplications.cshtml | 6 ++--- .../BulkPublishApplications.cshtml.cs | 22 ++++++++++++++++--- .../BulkActions/BulkPublishApplications.js | 4 ++-- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs index eeb5e4c5e9..5205476213 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs @@ -272,7 +272,7 @@ where applicationGuids.Contains(application.Id) && (!excludePublished || !applic ApplicationId = application.Id, ReferenceNo = application.ReferenceNo, ApplicantName = application.Applicant.ApplicantName ?? string.Empty, - ApplicationStatus = application.ApplicationStatus.InternalStatus, + ApplicationStatus = status.InternalStatus, FormName = form.ApplicationFormName ?? string.Empty, FinalDecisionDate = application.FinalDecisionDate, ExternalStatusVisibility = application.ExternalStatusVisibility, 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 edef4c3e01..b2cfbdce21 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 @@ -160,7 +160,7 @@ "Permission:GrantApplicationManagement.Approvals.DeferAfterApproval": "Defer After Approval", "Permission:GrantApplicationManagement.Comments.Default": "Comments", "Permission:GrantApplicationManagement.Comments.Add": "Add Application Comment", - "Permission:GrantApplicationManagement.Organizations.Default": "Organizations", + "Permission:GrantApplicationManagement.Organizations.Default": "Organizations", "Permission:GrantApplicationManagement.Organizations.ManageProfiles": "Manage Organization Profiles", "Permission:GrantManagerManagement.Intakes.Default": "Manage Intakes", "Permission:GrantManagerManagement.ApplicationForms.Default": "Manage Forms", @@ -533,6 +533,10 @@ "ApplicationBatchApprovalRequest:InvalidApprovedAmount": "Invalid Approved Amount, it must be greater than 0.00", "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00", + "ApplicationBatchPublishRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk approval. 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.", + "ApplicationLinks:Category": "Category", "ApplicationLinks:ID": "ID", "ApplicationLinks:Status": "Status", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml index e46dad4b65..4167de2eb8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml @@ -87,17 +87,17 @@

- Error You have exceeded the maximum number of items for bulk status publishing. Please reduce the number to 50 or fewer + Error @Model.MaxBatchCountExceededError

- Error You have no items selected for bulk status publishing. Please close this prompt to continue. + Error @L["ApplicationBatchPublishRequest:MinCountExceeded"]

- Note: By confirming, the selected application statuses will be published and made visible to applicants in the portal. + Note: @L["ApplicationBatchPublishRequest:ConfirmationNote"]

diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs index 814fe50928..b22faa53d0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; @@ -6,10 +7,13 @@ using System.Threading.Tasks; using Unity.GrantManager.Applications; using Unity.GrantManager.GrantApplications; +using Unity.Modules.Shared; +using Unity.Modules.Shared.Utils; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; namespace Unity.GrantManager.Web.Pages.BulkActions; +[Authorize(UnitySelector.Application.Status.BulkPublish)] public class BulkPublishApplicationsModel( ApplicationIdsCacheService cacheService, IBulkApprovalsAppService bulkApprovalsAppService) : AbpPageModel @@ -37,9 +41,14 @@ public class BulkPublishApplicationsModel( public async Task OnGetAsync(string cacheKey) { + if (!await AuthorizationService.IsGrantedAsync(UnitySelector.Application.Status.BulkPublish)) + { + return; + } + MaxBatchCount = BatchApprovalConsts.MaxBatchCount; BulkApplications = []; - MaxBatchCountExceededError = L["ApplicationBatchApprovalRequest:MaxCountExceeded", BatchApprovalConsts.MaxBatchCount.ToString()].Value; + MaxBatchCountExceededError = L["ApplicationBatchPublishRequest:MaxCountExceeded", BatchApprovalConsts.MaxBatchCount.ToString()].Value; try { @@ -48,7 +57,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; @@ -60,13 +69,14 @@ public async Task OnGetAsync(string cacheKey) // Clean up cache after retrieval (one-time use) await cacheService.RemoveAsync(cacheKey); - if (MaxBatchCount <= applicationGuids.Length) + if (applicationGuids.Length > MaxBatchCount) { MaxBatchCountExceeded = true; } if (applicationGuids.Length == 0) { + Invalid = true; return; } @@ -75,6 +85,7 @@ public async Task OnGetAsync(string cacheKey) BulkApplications = ObjectMapper.Map, List>(bulkApplications); ApplicationsCount = BulkApplications.Count; + Invalid = MaxBatchCountExceeded || ApplicationsCount == 0; } catch (Exception ex) { @@ -90,6 +101,11 @@ public async Task OnPostAsync() try { if (BulkApplications == null) return NoContent(); + if (BulkApplications.Count == 0 || BulkApplications.Count > BatchApprovalConsts.MaxBatchCount) + { + return BadRequest(); + } + var applicationsToPublish = BulkApplications.Select(y => y.ApplicationId).ToArray(); await bulkApprovalsAppService.BulkPublishApplications(applicationsToPublish); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js index e7e5e40b7c..3e2711c24e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js @@ -57,9 +57,9 @@ function validBatchCount() { return false; } - if (maxBatchCount <= applicationsCount) { + if (applicationsCount > maxBatchCount) { validationMaxValid = false; - } else if (applicationsCount == 0) { + } else if (applicationsCount === 0) { validationMinValid = false; } From ec0d42f676de5deb8a3e6b28b4bbd09040ad0020 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:03:16 -0700 Subject: [PATCH 17/23] [AB#33559] Move bulk publish methods to ApplicationBulkActionService --- .../IApplicationBulkActionsAppService.cs | 3 + .../IBulkApprovalsAppService.cs | 2 - .../ApplicationBulkActionsAppService.cs | 130 +++++++++++++----- .../BulkApprovalsAppService.cs | 58 -------- .../BulkPublishApplications.cshtml.cs | 6 +- 5 files changed, 99 insertions(+), 100 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applications/IApplicationBulkActionsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applications/IApplicationBulkActionsAppService.cs index 20b6988185..b7f21dadab 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applications/IApplicationBulkActionsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applications/IApplicationBulkActionsAppService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; using Volo.Abp.Application.Services; namespace Unity.GrantManager.Applications @@ -13,6 +14,8 @@ public interface IApplicationBulkActionsAppService : IApplicationService /// Request containing list of application IDs /// Cache key to retrieve the stored IDs Task StoreApplicationIdsAsync(StoreApplicationIdsRequestDto input); + Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true); + Task BulkPublishApplications(Guid[] applicationGuids); } public class StoreApplicationIdsRequestDto diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs index 458098da7b..1340a7c730 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkApprovalsAppService.cs @@ -8,7 +8,5 @@ public interface IBulkApprovalsAppService { Task BulkApproveApplications(List batchApplicationsToApprove); Task> GetApplicationsForBulkApproval(Guid[] applicationGuids); - Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true); - Task BulkPublishApplications(Guid[] applicationGuids); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs index f8290e94ad..9c6ade6755 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs @@ -1,56 +1,112 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Unity.Modules.Shared; using Volo.Abp; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.Applications +namespace Unity.GrantManager.Applications; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(ApplicationBulkActionsAppService), typeof(IApplicationBulkActionsAppService))] +[Authorize] +public class ApplicationBulkActionsAppService( + ApplicationIdsCacheService cacheService, + IApplicationRepository applicationRepository, + IApplicationStatusRepository applicationStatusRepository, + IApplicationFormRepository applicationFormRepository) : GrantManagerAppService, IApplicationBulkActionsAppService { - [Dependency(ReplaceServices = true)] - [ExposeServices(typeof(ApplicationBulkActionsAppService), typeof(IApplicationBulkActionsAppService))] - [Authorize] - public class ApplicationBulkActionsAppService : GrantManagerAppService, IApplicationBulkActionsAppService - { - private readonly ApplicationIdsCacheService _cacheService; - public ApplicationBulkActionsAppService(ApplicationIdsCacheService cacheService) + /// + /// Stores application IDs in distributed cache for bulk operations + /// + /// Request containing list of application IDs + /// Cache key to retrieve the stored IDs + public async Task StoreApplicationIdsAsync(StoreApplicationIdsRequestDto input) + { + if (input == null || input.ApplicationIds == null || input.ApplicationIds.Count == 0) { - _cacheService = cacheService; + throw new UserFriendlyException("No application IDs provided"); } - /// - /// Stores application IDs in distributed cache for bulk operations - /// - /// Request containing list of application IDs - /// Cache key to retrieve the stored IDs - public async Task StoreApplicationIdsAsync(StoreApplicationIdsRequestDto input) + try { - if (input == null || input.ApplicationIds == null || input.ApplicationIds.Count == 0) - { - throw new UserFriendlyException("No application IDs provided"); - } + var cacheKey = await cacheService.StoreApplicationIdsAsync(input.ApplicationIds); + + Logger.LogInformation( + "User {UserId} stored {Count} application IDs for bulk operation with cache key: {CacheKey}", + CurrentUser?.Id, + input.ApplicationIds.Count, + cacheKey); - try + return new StoreApplicationIdsResultDto { - var cacheKey = await _cacheService.StoreApplicationIdsAsync(input.ApplicationIds); - - Logger.LogInformation( - "User {UserId} stored {Count} application IDs for bulk operation with cache key: {CacheKey}", - CurrentUser?.Id, - input.ApplicationIds.Count, - cacheKey); - - return new StoreApplicationIdsResultDto - { - CacheKey = cacheKey - }; - } - catch (Exception ex) + CacheKey = cacheKey + }; + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to store application IDs for bulk operation"); + throw new UserFriendlyException("Failed to prepare bulk operation. Please try again."); + } + } + + /// + /// Get applications for bulk publish with internal and external status information + /// + /// + /// + /// + [Authorize(UnitySelector.Application.Status.BulkPublish)] + public async Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true) + { + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + var statusesQuery = await applicationStatusRepository.GetQueryableAsync(); + var formsQuery = await applicationFormRepository.GetQueryableAsync(); + + var results = await ( + from application in applicationsQuery + join form in formsQuery on application.ApplicationFormId equals form.Id + join status in statusesQuery on application.ApplicationStatusId equals status.Id + where applicationGuids.Contains(application.Id) && (!excludePublished || !application.ExternalStatusVisibility) + select new BulkPublishDto { - Logger.LogError(ex, "Failed to store application IDs for bulk operation"); - throw new UserFriendlyException("Failed to prepare bulk operation. Please try again."); - } + ApplicationId = application.Id, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.Applicant.ApplicantName ?? string.Empty, + ApplicationStatus = status.InternalStatus, + FormName = form.ApplicationFormName ?? string.Empty, + FinalDecisionDate = application.FinalDecisionDate, + ExternalStatusVisibility = application.ExternalStatusVisibility, + ExternalStatus = application.ExternalStatusVisibility + ? status.NotifiedStatus ?? status.ExternalStatus + : status.ExternalStatus, + PublishedStatus = status.NotifiedStatus ?? status.ExternalStatus, + }).ToListAsync(); + + return results; + } + + /// + /// Bulk publish applications by setting the ExternalStatusVisibility to true for the specified application GUIDs. + /// + /// The GUIDs of the applications to be bulk published. + /// A task representing the asynchronous operation. + [Authorize(UnitySelector.Application.Status.BulkPublish)] + public async Task BulkPublishApplications(Guid[] applicationGuids) + { + var applications = await applicationRepository + .GetListAsync(x => applicationGuids.Contains(x.Id) && !x.ExternalStatusVisibility); + + foreach (var application in applications) + { + application.ExternalStatusVisibility = true; + await applicationRepository.UpdateAsync(application); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs index 5205476213..60d663f617 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkApprovalsAppService.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -9,7 +8,6 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Events; using Unity.GrantManager.Permissions; -using Unity.Modules.Shared; using Volo.Abp.EventBus.Local; using Volo.Abp.Uow; @@ -18,8 +16,6 @@ namespace Unity.GrantManager.GrantApplications [Authorize] public class BulkApprovalsAppService( IApplicationRepository applicationRepository, - IApplicationStatusRepository applicationStatusRepository, - IApplicationFormRepository applicationFormRepository, IApplicationManager applicationManager, ILocalEventBus localEventBus, IUnitOfWorkManager unitofWorkManager) : GrantManagerAppService, IBulkApprovalsAppService @@ -248,59 +244,5 @@ private static OperationAuthorizationRequirement GetActionAuthorizationRequireme return new OperationAuthorizationRequirement { Name = $"{GrantApplicationPermissions.Applications.Default}.{triggerAction}" }; // this should allow anyone for now, but needs to change to a specific permission } - - /// - /// Get applications for bulk publish with internal and external status information - /// - /// - /// - /// - [Authorize(UnitySelector.Application.Status.BulkPublish)] - public async Task> GetApplicationsForBulkPublish(Guid[] applicationGuids, bool excludePublished = true) - { - var applicationsQuery = await applicationRepository.GetQueryableAsync(); - var statusesQuery = await applicationStatusRepository.GetQueryableAsync(); - var formsQuery = await applicationFormRepository.GetQueryableAsync(); - - var results = await ( - from application in applicationsQuery - join form in formsQuery on application.ApplicationFormId equals form.Id - join status in statusesQuery on application.ApplicationStatusId equals status.Id - where applicationGuids.Contains(application.Id) && (!excludePublished || !application.ExternalStatusVisibility) - select new BulkPublishDto - { - ApplicationId = application.Id, - ReferenceNo = application.ReferenceNo, - ApplicantName = application.Applicant.ApplicantName ?? string.Empty, - ApplicationStatus = status.InternalStatus, - FormName = form.ApplicationFormName ?? string.Empty, - FinalDecisionDate = application.FinalDecisionDate, - ExternalStatusVisibility = application.ExternalStatusVisibility, - ExternalStatus = application.ExternalStatusVisibility - ? status.NotifiedStatus ?? status.ExternalStatus - : status.ExternalStatus, - PublishedStatus = status.NotifiedStatus ?? status.ExternalStatus, - }).ToListAsync(); - - return results; - } - - /// - /// Bulk publish applications by setting the ExternalStatusVisibility to true for the specified application GUIDs. - /// - /// The GUIDs of the applications to be bulk published. - /// A task representing the asynchronous operation. - [Authorize(UnitySelector.Application.Status.BulkPublish)] - public async Task BulkPublishApplications(Guid[] applicationGuids) - { - var applications = await applicationRepository - .GetListAsync(x => applicationGuids.Contains(x.Id) && !x.ExternalStatusVisibility); - - foreach (var application in applications) - { - application.ExternalStatusVisibility = true; - await applicationRepository.UpdateAsync(application); - } - } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs index b22faa53d0..1525ffbac1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.cshtml.cs @@ -16,7 +16,7 @@ namespace Unity.GrantManager.Web.Pages.BulkActions; [Authorize(UnitySelector.Application.Status.BulkPublish)] public class BulkPublishApplicationsModel( ApplicationIdsCacheService cacheService, - IBulkApprovalsAppService bulkApprovalsAppService) : AbpPageModel + IApplicationBulkActionsAppService bulkActionsAppService) : AbpPageModel { [BindProperty] public List? BulkApplications { get; set; } @@ -81,7 +81,7 @@ public async Task OnGetAsync(string cacheKey) } // Fetch application details for the selected IDs - var bulkApplications = await bulkApprovalsAppService.GetApplicationsForBulkPublish(applicationGuids); + var bulkApplications = await bulkActionsAppService.GetApplicationsForBulkPublish(applicationGuids); BulkApplications = ObjectMapper.Map, List>(bulkApplications); ApplicationsCount = BulkApplications.Count; @@ -107,7 +107,7 @@ public async Task OnPostAsync() } var applicationsToPublish = BulkApplications.Select(y => y.ApplicationId).ToArray(); - await bulkApprovalsAppService.BulkPublishApplications(applicationsToPublish); + await bulkActionsAppService.BulkPublishApplications(applicationsToPublish); return new OkResult(); } From 606766f77bdfddf6656e8ca0faccac3ad75960a8 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:43:56 -0700 Subject: [PATCH 18/23] [AB#33559] Update Permission Labels --- .../Localization/GrantManager/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b2cfbdce21..306c66930d 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 @@ -247,9 +247,9 @@ "Unity.GrantManager.ApplicationManagement.Payment.PaymentList": "Payment List", "Unity.GrantManager.ApplicationManagement.Application.Status": "External Status Visibility", - "Unity.GrantManager.ApplicationManagement.Application.Status.Publish": "Publish", - "Unity.GrantManager.ApplicationManagement.Application.Status.Unpublish": "Unpublish", - "Unity.GrantManager.ApplicationManagement.Application.Status.BulkPublish": "Bulk Publish", + "Unity.GrantManager.ApplicationManagement.Application.Status.Publish": "Publish Status", + "Unity.GrantManager.ApplicationManagement.Application.Status.Unpublish": "Unpublish Status", + "Unity.GrantManager.ApplicationManagement.Application.Status.BulkPublish": "Bulk Publish Status", "Permission:GrantApplicationManagement.ProjectInfo": "Project Info", "Permission:GrantApplicationManagement.ProjectInfo.Update": "Project Info Update", From 399b392c6686a4f1de3be62d9bc67559969853ea Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:09:12 -0700 Subject: [PATCH 19/23] [AB#33559] Code Quality Cleanup --- .../ApplicationBulkActionsAppService.cs | 3 +- .../Localization/GrantManager/en.json | 2 +- .../BulkActions/BulkPublishApplications.js | 66 +++++++------------ 3 files changed, 25 insertions(+), 46 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs index 9c6ade6755..669d03e32c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applications/ApplicationBulkActionsAppService.cs @@ -21,6 +21,7 @@ public class ApplicationBulkActionsAppService( IApplicationStatusRepository applicationStatusRepository, IApplicationFormRepository applicationFormRepository) : GrantManagerAppService, IApplicationBulkActionsAppService { + private readonly ApplicationIdsCacheService _cacheService = cacheService; /// /// Stores application IDs in distributed cache for bulk operations @@ -36,7 +37,7 @@ public async Task StoreApplicationIdsAsync(StoreAp try { - var cacheKey = await cacheService.StoreApplicationIdsAsync(input.ApplicationIds); + var cacheKey = await _cacheService.StoreApplicationIdsAsync(input.ApplicationIds); Logger.LogInformation( "User {UserId} stored {Count} application IDs for bulk operation with cache key: {CacheKey}", 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 306c66930d..aa81531138 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 @@ -533,7 +533,7 @@ "ApplicationBatchApprovalRequest:InvalidApprovedAmount": "Invalid Approved Amount, it must be greater than 0.00", "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00", - "ApplicationBatchPublishRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk approval. Please reduce the number to {0} or fewer", + "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.Web/Pages/BulkActions/BulkPublishApplications.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js index 3e2711c24e..685d121930 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkActions/BulkPublishApplications.js @@ -14,60 +14,38 @@ function setMaxCountError(visible) { } } -function enableBulkPublishSubmit() { - $("#bulkPublishApplicationsModal") - .find('#btnSubmitBatchPublish').prop("disabled", false); - } - -function disableBulkPublishSubmit() { - $("#bulkPublishApplicationsModal") - .find('#btnSubmitBatchPublish').prop("disabled", true); - } - function closePublish() { $('#bulkPublishApplicationsModal').modal('hide'); } function runValidations() { - let isValid = true; - let itemCount = 0; - - $('#bulkPublishForm input[name="BulkApplications.Index"]').each(function () { - itemCount++; - }); - - isValid = validBatchCount(); - $('#batch-action-summary').toggleClass('d-none', isValid); - - if (isValid) { - enableBulkPublishSubmit(); - } else { - disableBulkPublishSubmit(); - } - } + const isValid = validBatchCount(); + $('#batch-action-summary').toggleClass('d-none', isValid); + $("#bulkPublishApplicationsModal").find('#btnSubmitBatchPublish').prop("disabled", !isValid); +} function validBatchCount() { - let applicationsCount = Number.parseInt(String($('#ApplicationsCount').val() ?? ''), 10); - let maxBatchCount = Number.parseInt(String($('#MaxBatchCount').val() ?? ''), 10); - let validationMaxValid = true; - let validationMinValid = true; - - const hasInvalidNumber = Number.isNaN(applicationsCount) || Number.isNaN(maxBatchCount); - if (hasInvalidNumber) { - return false; - } + const applicationsCount = Number.parseInt(String($('#ApplicationsCount').val() ?? ''), 10); + const maxBatchCount = Number.parseInt(String($('#MaxBatchCount').val() ?? ''), 10); + let validationMaxValid = true; + let validationMinValid = true; + + const hasInvalidNumber = Number.isNaN(applicationsCount) || Number.isNaN(maxBatchCount); + if (hasInvalidNumber) { + return false; + } - if (applicationsCount > maxBatchCount) { - validationMaxValid = false; - } else if (applicationsCount === 0) { - validationMinValid = false; - } + if (applicationsCount > maxBatchCount) { + validationMaxValid = false; + } else if (applicationsCount === 0) { + validationMinValid = false; + } - $('#maxCountWarning').toggleClass('d-none', validationMaxValid); - $('#minCountWarning').toggleClass('d-none', validationMinValid); - $('.batch-action-card').toggleClass('d-none', !validationMinValid); + $('#maxCountWarning').toggleClass('d-none', validationMaxValid); + $('#minCountWarning').toggleClass('d-none', validationMinValid); + $('.batch-action-card').toggleClass('d-none', !validationMinValid); - return validationMaxValid && validationMinValid; + return validationMaxValid && validationMinValid; } $(function () { From 871fe19024e17e5f00bfd95645776897f6dca256 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 10 Jul 2026 13:40:53 -0700 Subject: [PATCH 20/23] AB#33530 add missing divider --- .../Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml index ec0636f133..f87ad8c49f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/ProgramDetails/ProgramDetailsViewComponent.cshtml @@ -51,6 +51,7 @@ data-char-count="@ProgramDetailsConsts.MaxDescriptionLength">@Model.Description
+
Date: Fri, 10 Jul 2026 14:04:47 -0700 Subject: [PATCH 21/23] [AB#33665] Fix Form Mapping Page Dropdowns --- .../Pages/ApplicationForms/Mapping.css | 4 ++++ .../Components/ApplicationFormConfigWidget/Default.css | 6 +++++- .../Views/Shared/Components/CustomFields/Default.cshtml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.css index f4ff0510f2..f6d49826d5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.css @@ -6,6 +6,10 @@ background-color: white; } +.unt-tab-content .form-select { + width: auto; +} + .configuration-action-bar { align-items: center; border-bottom: 1px solid #dee2e6; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css index 1388466ab7..4a9ddf92ef 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationFormConfigWidget/Default.css @@ -1,4 +1,8 @@ -.configuration-warning { +#otherConfigForm .form-select { + width: 400px !important; +} + +.configuration-warning { color: var(--lpx-danger); border-color: var(--lpx-danger); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml index 03e84b9a4f..7cf334679f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.cshtml @@ -39,7 +39,7 @@ }
-
+
From 3db52aeac733e2bedd70e7a30507740db4438321 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:20:48 -0700 Subject: [PATCH 22/23] [AB#33660] Fix Applicant Details FYE Control Styling --- .../ApplicantOrganizationInfo/Default.css | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css index a18a578ab2..f56ee09e68 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.css @@ -26,9 +26,18 @@ z-index: 999; } -.applicant-info-container .save-button-container .floating-save-btn { - position: static; -} + .applicant-info-container input[readonly] { + color: var(--bc-colors-grey-text-200); + pointer-events: none; + background-color: var(--bc-colors-grey-hover) !important; + opacity: var(--bs-btn-disabled-opacity); + background-blend-mode: difference; + border: var(--bs-border-width) solid var(--bs-border-color); + } + + .applicant-info-container .save-button-container .floating-save-btn { + position: static; + } .applicant-organization-info { border-radius: 8px; From a10784fc7444cec70dbe96504a08734644e92cde Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 10 Jul 2026 16:42:27 -0700 Subject: [PATCH 23/23] feature/AB#32580-TemplateAttachments-FixIssues --- .../IEmailGroupUsersAppService.cs | 2 + .../EmailGroups/IEmailGroupsAppService.cs | 2 + ...tificationsPermissionDefinitionProvider.cs | 4 -- .../Permissions/NotificationsPermissions.cs | 1 - .../EmailGroupUsersAppService.cs | 1 + .../EmailGroups/EmailGroupsAppService.cs | 7 +-- .../NotificationsSettingGroup/Default.cshtml | 5 ++ .../NotificationsSettingGroup/Default.css | 5 ++ .../NotificationsSettingGroup/Default.js | 54 +++++++++++++------ .../NotificationsSettingViewComponent.cs | 14 +++-- .../NotificationsSettingViewModel.cs | 5 ++ .../_Templates.cshtml | 9 ---- .../ApplicationAssignmentsAppService.cs | 4 +- .../Events/ScheduledNotificationHelper.cs | 2 +- .../Intakes/IntakeFormSubmissionManager.cs | 5 +- .../Permissions/PermissionGrantsDataSeeder.cs | 1 - .../Components/Notifications/Default.cshtml | 6 --- .../Components/Notifications/Default.js | 37 ------------- 18 files changed, 80 insertions(+), 84 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroupUsers/IEmailGroupUsersAppService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroupUsers/IEmailGroupUsersAppService.cs index fadfc0c1cb..0ec76ecc36 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroupUsers/IEmailGroupUsersAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroupUsers/IEmailGroupUsersAppService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; namespace Unity.Notifications.EmailGroups @@ -11,6 +12,7 @@ public interface IEmailGroupUsersAppService Task DeleteUserAsync (Guid id); Task DeleteUsersByGroupIdAsync (Guid id); Task DeleteUsersByUserIdAsync(Guid id); + [AllowAnonymous] Task> GetEmailGroupUsersByGroupIdAsync(Guid id); Task> GetListAsync(); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroups/IEmailGroupsAppService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroups/IEmailGroupsAppService.cs index c9616322c5..58ca4fba1f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroups/IEmailGroupsAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/EmailGroups/IEmailGroupsAppService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; namespace Unity.Notifications.EmailGroups @@ -10,6 +11,7 @@ public interface IEmailGroupsAppService Task CreateAsync (EmailGroupDto dto); Task UpdateAsync (EmailGroupDto dto); Task DeleteAsync (Guid id); + [AllowAnonymous] Task> GetListAsync(); Task GetEmailGroupByIdAsync(Guid id); } 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 826ca5789b..ce368255c9 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 @@ -41,10 +41,6 @@ public override void Define(IPermissionDefinitionContext context) NotificationsPermissions.Email.ScheduleCreate, L($"Permission:{NotificationsPermissions.Email.ScheduleCreate}")); - scheduleNotificationsPermissions.AddChild( - NotificationsPermissions.Email.ScheduleDelete, - L($"Permission:{NotificationsPermissions.Email.ScheduleDelete}")); - scheduleNotificationsPermissions.AddChild( NotificationsPermissions.Email.ScheduleCancel, L($"Permission:{NotificationsPermissions.Email.ScheduleCancel}")); 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 65ec30f3d4..20bd2b4ec1 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 @@ -16,7 +16,6 @@ public static class Email public const string Schedule = "Notifications.Email.Schedule"; public const string NotificationsTab = "Notifications.Form.Tab"; public const string ScheduleCreate = "Notifications.Form.Email.Schedule.Create"; - public const string ScheduleDelete = "Notifications.Form.Email.Schedule.Delete"; public const string ScheduleCancel = "Notifications.Form.Email.Schedule.Cancel"; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroupUsers/EmailGroupUsersAppService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroupUsers/EmailGroupUsersAppService.cs index d3a8969c5c..1f5e70ecb9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroupUsers/EmailGroupUsersAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroupUsers/EmailGroupUsersAppService.cs @@ -75,6 +75,7 @@ public async Task DeleteUsersByGroupIdAsync(Guid id) } } + [AllowAnonymous] public async Task> GetEmailGroupUsersByGroupIdAsync(Guid id) { var users = await _emailGroupUsersRepository.GetListAsync(u => u.GroupId == id); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroups/EmailGroupsAppService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroups/EmailGroupsAppService.cs index bec6f37481..42a0eb9beb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroups/EmailGroupsAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailGroups/EmailGroupsAppService.cs @@ -21,7 +21,7 @@ public EmailGroupsAppService(IEmailGroupsRepository emailGroupsRepository) _emailGroupsRepository = emailGroupsRepository; } public async Task CreateAsync(EmailGroupDto dto) - { + { var newGroup = await _emailGroupsRepository.InsertAsync(new EmailGroup { Name = dto.Name, @@ -42,7 +42,7 @@ public async Task UpdateAsync(EmailGroupDto dto) var emailGroup = await _emailGroupsRepository.GetAsync(dto.Id, true); emailGroup.Name = dto.Name; emailGroup.Description = dto.Description; - emailGroup.Type = dto.Type; + emailGroup.Type = dto.Type; await _emailGroupsRepository.UpdateAsync(emailGroup, autoSave: true); return new EmailGroupDto { @@ -66,9 +66,10 @@ public async Task DeleteAsync(Guid id) } } + [AllowAnonymous] public async Task> GetListAsync() { - var groups = await _emailGroupsRepository.GetListAsync(); + var groups = await _emailGroupsRepository.GetListAsync(); return ObjectMapper.Map, List>(groups); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml index bdc9957838..1fc31ed1c2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml @@ -63,6 +63,11 @@
- - - - - - - - - \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assignments/ApplicationAssignmentsAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assignments/ApplicationAssignmentsAppService.cs index 95fc01d378..2656e06db5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assignments/ApplicationAssignmentsAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assignments/ApplicationAssignmentsAppService.cs @@ -10,6 +10,7 @@ using Unity.GrantManager.Identity; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; namespace Unity.GrantManager.Assignments; @@ -18,13 +19,13 @@ namespace Unity.GrantManager.Assignments; [ExposeServices(typeof(ApplicationAssignmentsAppService), typeof(IApplicationAssignmentsService))] public class ApplicationAssignmentsAppService(IApplicationManager applicationManager, IPersonRepository personRepository, + ILocalEventBus localEventBus, IApplicationAssignmentRepository applicationAssignmentRepository) : ApplicationService, IApplicationAssignmentsService { public async Task> GetListWithApplicationIdsAsync(List ids) { var assignments = await applicationAssignmentRepository.GetListAsync(e => ids.Contains(e.ApplicationId)); - return ObjectMapper.Map, List>(assignments.OrderBy(t => t.Id).ToList()); } @@ -41,6 +42,7 @@ public async Task InsertAssigneeAsync(Guid applicationId, Guid assigneeId, strin { await applicationManager.UpdateAssigneeAsync(applicationId, assigneeId, duty); } + await localEventBus.PublishAsync(new Events.ApplicationChangedEvent { Action = GrantApplicationAction.Internal_Assign, ApplicationId = applicationId }); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs index e03f81412d..11addcbd97 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs @@ -68,7 +68,7 @@ public static Dictionary BuildTokenValues(Application applicatio ["contact_full_name"] = applicantAgent?.Name ?? string.Empty, ["contact_title"] = applicantAgent?.Title ?? string.Empty, ["category"] = applicationForm?.Category ?? string.Empty, - ["today_date"] = $"Today, {DateTime.Today.ToString("MMMM d, yyyy")}", + ["today_date"] = $"{DateTime.Today.ToString("MMMM d, yyyy")}", ["unity_application_id"] = application.UnityApplicationId ?? string.Empty }; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs index 822c169ec1..ed497b2a36 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/IntakeFormSubmissionManager.cs @@ -14,6 +14,7 @@ using Volo.Abp.Domain.Services; using Volo.Abp.EventBus.Local; using Volo.Abp.Uow; +using Unity.GrantManager.Events; namespace Unity.GrantManager.Intakes { @@ -93,9 +94,9 @@ await localEventBus.PublishAsync(new ApplicationProcessEvent ApplicationFormSubmission = newSubmission, RawSubmission = formSubmission }); - + + await localEventBus.PublishAsync(new ApplicationChangedEvent { Action = GrantApplicationAction.Submit, ApplicationId = application.Id }); await uow.SaveChangesAsync(); - return application.Id; } 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 833ac0b40c..ebb5cb86c7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -76,7 +76,6 @@ public PermissionGrantsDataSeeder(IPermissionDataSeeder permissionDataSeeder) public readonly List NotificationsScheduling_CommonPermissions = [ NotificationsPermissions.Email.CancelScheduled, NotificationsPermissions.Email.ScheduleCreate, - NotificationsPermissions.Email.ScheduleDelete, NotificationsPermissions.Email.ScheduleCancel ]; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml index 3b42f4dabb..061e8cd0f7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.cshtml @@ -39,12 +39,6 @@ Cancel } - @if (await PermissionChecker.IsGrantedAsync(NotificationsPermissions.Email.ScheduleDelete)) - { - - } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js index 7aa69023e8..a81ab5d530 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Default.js @@ -224,9 +224,6 @@ $('#notifications-list').on('click', '.js-cancel-notification', function () { onCancelNotification($(this).data('id')); }); - $('#notifications-list').on('click', '.js-delete-notification', function () { - onDeleteNotification($(this).data('id')); - }); } function reloadTable() { @@ -265,34 +262,6 @@ }); } - function onDeleteNotification(id) { - if (!id) return; - Swal.fire({ - title: 'Delete Notification?', - text: 'Are you sure you want to delete this scheduled notification?', - showCancelButton: true, - confirmButtonText: 'Confirm', - customClass: { - confirmButton: 'btn btn-primary', - cancelButton: 'btn btn-secondary' - } - }).then((result) => { - if (!result.isConfirmed) return; - fetch('/api/form-notifications/' + encodeURIComponent(formId) + '/' + encodeURIComponent(id), { - method: 'DELETE', - headers: { 'RequestVerificationToken': abp.security.antiForgery.getToken() } - }) - .then(r => { - if (!r.ok) throw new Error('Failed to delete'); - abp.notify.success('Notification deleted'); - reloadTable(); - }) - .catch(err => { - console.error(err); - abp.notify.error('Failed to delete notification'); - }); - }); - } function onEditNotification(id) { if (!id) return; @@ -593,12 +562,6 @@ cancelBtn.remove(); } } - - const deleteBtn = container.querySelector('.js-delete-notification'); - if (deleteBtn) { - deleteBtn.dataset.id = row.id; - } - return container.innerHTML; }