From e42f658453ba5282ffc07c3a498faf7e164f8656 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 10 Nov 2025 14:18:29 -0800 Subject: [PATCH 1/8] feature/AB#27735-RefreshSiteList --- .../Handlers/UpsertSupplierHandler.cs | 25 +---- .../Integrations/Cas/ISupplierService.cs | 1 + .../Integrations/Cas/SupplierService.cs | 5 +- .../Suppliers/ISupplierAppService.cs | 5 +- .../Suppliers/SupplierAppService.cs | 100 +++++++++++++++++- .../Components/SupplierInfo/SupplierInfo.js | 96 ++++++++++++----- 6 files changed, 176 insertions(+), 56 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Handlers/UpsertSupplierHandler.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Handlers/UpsertSupplierHandler.cs index 4d86379e9..1224dc4e6 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Handlers/UpsertSupplierHandler.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Handlers/UpsertSupplierHandler.cs @@ -45,7 +45,7 @@ private async Task> UpsertSitesFromEventDtoAsync( { foreach (var siteEto in upsertSupplierEto.SiteEtos) { - var siteDto = GetSiteDtoFromSiteEto(siteEto, supplierId); + var siteDto = supplierAppService.GetSiteDtoFromSiteEto(siteEto, supplierId); if (existingSitesDictionary.TryGetValue(siteDto.Number, out var existingSite)) { @@ -81,29 +81,6 @@ private async Task GetSupplierFromEvent(UpsertSupplierEto eventData return supplierDto; } - private static SiteDto GetSiteDtoFromSiteEto(SiteEto siteEto, Guid supplierId) - { - return new() - { - Number = siteEto.SupplierSiteCode, - PaymentGroup = Enums.PaymentGroup.EFT, // Defaulting to EFT based on conversations with CGG/CAS - AddressLine1 = siteEto.AddressLine1, - AddressLine2 = siteEto.AddressLine2, - AddressLine3 = siteEto.AddressLine3, - City = siteEto.City, - Province = siteEto.Province, - PostalCode = siteEto.PostalCode, - SupplierId = supplierId, - Country = siteEto.Country, - EmailAddress = siteEto.EmailAddress, - EFTAdvicePref = siteEto.EFTAdvicePref, - BankAccount = siteEto.BankAccount, - ProviderId = siteEto.ProviderId, - Status = siteEto.Status, - SiteProtected = siteEto.SiteProtected, - LastUpdatedInCas = siteEto.LastUpdated - }; - } private static UpdateSupplierDto GetUpdateSupplierDtoFromEvent(UpsertSupplierEto eventData) { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/ISupplierService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/ISupplierService.cs index 1b7be705d..60796754d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/ISupplierService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/ISupplierService.cs @@ -11,5 +11,6 @@ public interface ISupplierService : IApplicationService Task GetCasSupplierInformationAsync(string? supplierNumber); Task UpdateApplicantSupplierInfo(string? supplierNumber, Guid applicantId); Task UpdateApplicantSupplierInfoByBn9(string? bn9, Guid applicantId); + Task UpdateSupplierInfo(dynamic casSupplierResponse, Guid applicantId); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs index e4f798664..429dbe579 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using System.Text.Json; using Volo.Abp.Application.Services; -using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using System.Net.Http; using Unity.Modules.Shared.Http; @@ -93,7 +92,7 @@ public async Task UpdateApplicantSupplierInfoByBn9(string? bn9, Guid ap return casSupplierResponse; } - private async Task UpdateSupplierInfo(dynamic casSupplierResponse, Guid applicantId) + public async Task UpdateSupplierInfo(dynamic casSupplierResponse, Guid applicantId) { try { @@ -159,7 +158,7 @@ string GetProp(string name) => } - protected static SiteEto GetSiteEto(JsonElement site) + public static SiteEto GetSiteEto(JsonElement site) { string accountNumber = GetJsonProperty("accountnumber", site); string maskedAccountNumber = accountNumber.Length > 4 diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/ISupplierAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/ISupplierAppService.cs index 63a476c62..3cb77ca60 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/ISupplierAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/ISupplierAppService.cs @@ -14,7 +14,8 @@ public interface ISupplierAppService : IApplicationService Task UpdateAsync(Guid id, UpdateSupplierDto updateSupplierDto); Task CreateSiteAsync(Guid id, CreateSiteDto createSiteDto); Task UpdateSiteAsync(Guid id, Guid siteId, UpdateSiteDto updateSiteDto); - Task> GetSitesBySupplierNumberAsync(string? supplierNumber); - Task DeleteAsync(Guid id); + Task GetSitesBySupplierNumberAsync(string? supplierNumber, Guid applicantId); + Task DeleteAsync(Guid id); + SiteDto GetSiteDtoFromSiteEto(SiteEto siteEto, Guid supplierId); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/SupplierAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/SupplierAppService.cs index 88b0dfdd5..6612a32ba 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/SupplierAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Suppliers/SupplierAppService.cs @@ -3,15 +3,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Unity.Payments.Domain.Suppliers; using Unity.Payments.Domain.Suppliers.ValueObjects; +using Unity.Payments.Integrations.Cas; using Volo.Abp.Features; namespace Unity.Payments.Suppliers { [RequiresFeature("Unity.Payments")] public class SupplierAppService(ISupplierRepository supplierRepository, + ISupplierService supplierService, ISiteAppService siteAppService) : PaymentsAppService, ISupplierAppService { protected ILogger logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); @@ -99,12 +102,81 @@ public virtual async Task UpdateAsync(Guid id, UpdateSupplierDto up return ObjectMapper.Map(result); } - public async Task> GetSitesBySupplierNumberAsync(string? supplierNumber) + public async Task GetSitesBySupplierNumberAsync(string? supplierNumber, Guid applicantId) { + // Change this code to get the supplier list of sites - from the datatabase which it is currently doing + // Then go to CAS and get the list of sites again + // Compare and update the database if there are any new sites + dynamic casSupplierResponse = await supplierService.GetCasSupplierInformationAsync(supplierNumber); + var casSiteDtos = new List(); + if (casSupplierResponse.TryGetProperty("supplieraddress", out JsonElement sitesJson) && + sitesJson.ValueKind == JsonValueKind.Array) + { + foreach (var site in sitesJson.EnumerateArray()) + { + SiteEto siteEto = SupplierService.GetSiteEto(site); + SiteDto siteDto = GetSiteDtoFromSiteEto(siteEto, Guid.Empty); + casSiteDtos.Add(siteDto); + } + } + var supplier = await GetBySupplierNumberAsync(supplierNumber); if (supplier == null) return new List(); List sites = await siteAppService.GetSitesBySupplierIdAsync(supplier.Id); - return sites.Select(ObjectMapper.Map).ToList(); + List existingSiteDtos = sites.Select(ObjectMapper.Map).ToList(); + + bool hasChanges = false; + // If the list of CAS sites is different from the existing sites + if (existingSiteDtos.Count != casSiteDtos.Count) + { + // Update the supplier and sites + hasChanges = true; + } + else if (existingSiteDtos.Count == casSiteDtos.Count) + { + // Go through each site and compare + hasChanges = false; + + // based on the matching number, check if any other fields are different + foreach (var casSite in casSiteDtos) + { + var existingSite = existingSiteDtos.FirstOrDefault(s => s.Number == casSite.Number); + if (existingSite != null) + { + // Compare fields and update if necessary + if (existingSite.Country != casSite.Country || + existingSite.EFTAdvicePref != casSite.EFTAdvicePref || + existingSite.EmailAddress != casSite.EmailAddress || + existingSite.PostalCode != casSite.PostalCode || + existingSite.ProviderId != casSite.ProviderId || + existingSite.Province != casSite.Province || + existingSite.SiteProtected != casSite.SiteProtected || + existingSite.City != casSite.City || + existingSite.AddressLine1 != casSite.AddressLine1 || + existingSite.AddressLine2 != casSite.AddressLine2 || + existingSite.BankAccount != casSite.BankAccount || + existingSite.Status != casSite.Status) + { + hasChanges = true; + break; + } + } + else + { + hasChanges = true; + break; + } + } + } + + if (hasChanges) + { + await supplierService.UpdateSupplierInfo(casSupplierResponse, applicantId); + existingSiteDtos = casSiteDtos; + } + + + return new { sites = existingSiteDtos, hasChanges }; } public virtual async Task CreateSiteAsync(Guid id, CreateSiteDto createSiteDto) @@ -149,5 +221,29 @@ public virtual async Task DeleteAsync(Guid id) { await supplierRepository.DeleteAsync(id); } + + public SiteDto GetSiteDtoFromSiteEto(SiteEto siteEto, Guid supplierId) + { + return new() + { + Number = siteEto.SupplierSiteCode, + PaymentGroup = Enums.PaymentGroup.EFT, // Defaulting to EFT based on conversations with CGG/CAS + AddressLine1 = siteEto.AddressLine1, + AddressLine2 = siteEto.AddressLine2, + AddressLine3 = siteEto.AddressLine3, + City = siteEto.City, + Province = siteEto.Province, + PostalCode = siteEto.PostalCode, + SupplierId = supplierId, + Country = siteEto.Country, + EmailAddress = siteEto.EmailAddress, + EFTAdvicePref = siteEto.EFTAdvicePref, + BankAccount = siteEto.BankAccount, + ProviderId = siteEto.ProviderId, + Status = siteEto.Status, + SiteProtected = siteEto.SiteProtected, + LastUpdatedInCas = siteEto.LastUpdated + }; + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js index 85eeefa85..7b796f99a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js @@ -59,9 +59,25 @@ $(function () { loadSiteInfoTable(); bindUIEvents(); validateMatchingSupplierToOrgInfo(); + enableDisableRefreshSitesButton(); }); } + function enableDisableRefreshSitesButton() { + const supplierNumber = UIElements.supplierNumber.val(); + const originalSupplierNumber = + UIElements.originalSupplierNumber.val(); + + + if (originalSupplierNumber != '' && supplierNumber && supplierNumber.trim() !== '' && supplierNumber === originalSupplierNumber) { + UIElements.refreshSitesBtn.removeAttr('disabled'); + } else { + UIElements.refreshSitesBtn.attr('disabled', 'disabled'); + } + + + } + init(); function validateMatchingSupplierToOrgInfo() { @@ -124,6 +140,8 @@ $(function () { } }); + UIElements.supplierNumber.on('change', enableDisableRefreshSitesButton); + UIElements.supplierNumber.on('keyup', enableDisableRefreshSitesButton); UIElements.supplierName.on('change', validateMatchingSupplierToOrgInfo); UIElements.orgName.on('change', validateMatchingSupplierToOrgInfo); UIElements.nonRegisteredOrgName.on( @@ -136,8 +154,9 @@ $(function () { UIElements.refreshSitesBtn.on('click', function () { let originalSupplierNumber = UIElements.originalSupplierNumber.val(); + let supplierNumber = UIElements.supplierNumber.val(); // Check if supplier Number matches the original supplier number - if (originalSupplierNumber == '') { + if (originalSupplierNumber == '' || supplierNumber !== originalSupplierNumber) { Swal.fire({ title: 'Action Complete', text: 'The Supplier # must be saved before refreshing the site list', @@ -149,31 +168,52 @@ $(function () { return; } + const applicantId = UIElements.paymentApplicantId.val(); $.ajax({ - url: `/api/app/supplier/sites-by-supplier-number?supplierNumber=${originalSupplierNumber}`, + url: `/api/app/supplier/sites-by-supplier-number/${applicantId}?supplierNumber=${originalSupplierNumber}`, method: 'GET', success: function (response) { - let dt = $('#SiteInfoTable').DataTable(); - if (dt) { - dt.clear(); - dt.rows.add(response); - dt.draw(); - dt.columns.adjust(); - let message = - 'The site list has been updated. Please re-select your default site'; - - if (response.length == 0) { - message = 'No sites were found for the supplier'; - } - Swal.fire({ - title: 'Action Complete', - text: message, - confirmButtonText: 'Ok', - customClass: { - confirmButton: 'btn btn-primary', - }, - }); + + if (!response.hasChanges) { + let message = "The site list is already up to date."; + Swal.fire({ + title: 'Action Complete', + text: message, + confirmButtonText: 'Ok', + customClass: { + confirmButton: 'btn btn-primary', + }, + }); + } else { + let dt = $('#SiteInfoTable').DataTable(); + if (dt) { + dt.clear(); + dt.rows.add(response.sites); + dt.draw(); + dt.columns.adjust(); + let message = "The site list has been updated. Please re-select your default site"; + if (response.length == 0) { + message = "No sites were found for the supplier"; + } else if (response.length > 1) { + $('input[name="default-site"]').prop('checked', false); + } else if (response.length == 1) { + // Auto select the only site as default + let onlySiteId = response[0].id; + $('input[name="default-site"][value="' + onlySiteId + '"]').prop('checked', true); + message = "The site list has been updated. Only one site was returned and has been defaulted."; + saveSiteDefault(onlySiteId); + } + + Swal.fire({ + title: 'Action Complete', + text: message, + confirmButtonText: 'Ok', + customClass: { + confirmButton: 'btn btn-primary', + }, + }); + } } }, error: function (xhr, status, error) { @@ -191,8 +231,14 @@ $(function () { const supplierOrgInfoErrorDiv = $('#supplier-error-div'); // Clear supplier fields - supplierNumber.val('').trigger('change'); + supplierNumber.value = ''; + supplierNumber.val(''); + supplierNumber.attr('value', ''); + supplierNumber.trigger('change'); + supplierNumber.trigger('keyup'); + supplierName.val(''); + supplierName.trigger('change'); $('#Status').val(''); // Clear hidden fields @@ -250,7 +296,7 @@ $(function () { text: 'Filter', className: 'custom-table-btn flex-none btn btn-secondary', id: 'btn-toggle-filter', - action: function (e, dt, node, config) {}, + action: function (e, dt, node, config) { }, attr: { id: 'btn-toggle-filter', }, @@ -395,7 +441,7 @@ $(function () { 'Unity.GrantManager.ApplicationManagement.Payment.Supplier.Update' ) ) { - return ``; + return ``; } return ``; From a0a25c998affdfa00d6fb529d5ead2b93ac5831c Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 10 Nov 2025 15:48:04 -0800 Subject: [PATCH 2/8] feature/AB#29737-ManuallyRetrieveCAS --- .../IPaymentRequestAppService.cs | 1 + .../CasPaymentRequestCoordinator.cs | 24 +++++++++++-- .../PaymentRequestAppService.cs | 34 ++++++++++++++++--- .../Pages/PaymentRequests/Index.js | 27 +++++++++++++-- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs index 9af5ab42d..87916d6e6 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs @@ -19,5 +19,6 @@ public interface IPaymentRequestAppService : IApplicationService Task GetNextBatchInfoAsync(); Task GetDefaultAccountCodingId(); Task GetUserPaymentThresholdAsync(); + Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequests); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs index 0a8a3316c..58936e24b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/CasPaymentRequestCoordinator.cs @@ -21,7 +21,7 @@ public class CasPaymentRequestCoordinator : ApplicationService private readonly ITenantRepository _tenantRepository; private readonly ICurrentTenant _currentTenant; private readonly PaymentQueueService _paymentQueueService; - private static int FiveMinutes = 5; + private static int TenMinutes = 10; public CasPaymentRequestCoordinator( PaymentQueueService paymentQueueService, @@ -63,7 +63,7 @@ public async Task AddPaymentRequestsToInvoiceQueue(PaymentRequest paymentRequest { InvoiceMessages message = new InvoiceMessages { - TimeToLive = TimeSpan.FromMinutes(FiveMinutes), + TimeToLive = TimeSpan.FromMinutes(TenMinutes), PaymentRequestId = paymentRequest.Id, InvoiceNumber = paymentRequest.InvoiceNumber, SupplierNumber = paymentRequest.SupplierNumber, @@ -81,6 +81,24 @@ public async Task AddPaymentRequestsToInvoiceQueue(PaymentRequest paymentRequest } } + public async Task ManuallyAddPaymentRequestsToReconciliationQueue(ListpaymentRequests) + { + foreach (PaymentRequestDto paymentRequest in paymentRequests) + { + ReconcilePaymentMessages reconcilePaymentMessage = new ReconcilePaymentMessages + { + TimeToLive = TimeSpan.FromMinutes(TenMinutes), + PaymentRequestId = paymentRequest.Id, + InvoiceNumber = paymentRequest.InvoiceNumber, + SupplierNumber = paymentRequest.SupplierNumber, + SiteNumber = paymentRequest.Site?.Number ?? string.Empty, + TenantId = _currentTenant.Id!.Value + }; + + await _paymentQueueService.SendPaymentToReconciliationQueueAsync(reconcilePaymentMessage); + } + } + public async Task AddPaymentRequestsToReconciliationQueue() { var tenants = await _tenantRepository.GetListAsync(); @@ -93,7 +111,7 @@ public async Task AddPaymentRequestsToReconciliationQueue() { ReconcilePaymentMessages reconcilePaymentMessage = new ReconcilePaymentMessages { - TimeToLive = TimeSpan.FromMinutes(FiveMinutes), + TimeToLive = TimeSpan.FromMinutes(TenMinutes), PaymentRequestId = paymentRequest.Id, InvoiceNumber = paymentRequest.InvoiceNumber, SupplierNumber = paymentRequest.SupplierNumber, diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs index 39115ece0..8d01497dc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/PaymentRequests/PaymentRequestAppService.cs @@ -21,6 +21,8 @@ using Unity.Payments.Domain.PaymentThresholds; using Volo.Abp.Domain.Repositories; using Unity.GrantManager.Applications; +using Unity.Payments.Domain.Suppliers; +using Unity.Payments.Suppliers; namespace Unity.Payments.PaymentRequests { @@ -37,7 +39,9 @@ public class PaymentRequestAppService( IPaymentsManager paymentsManager, IPaymentRequestRepository paymentRequestsRepository, IPaymentThresholdRepository paymentThresholdRepository, - IPermissionChecker permissionChecker) : PaymentsAppService, IPaymentRequestAppService + IPermissionChecker permissionChecker, + ISiteRepository siteRepository, + CasPaymentRequestCoordinator casPaymentRequestCoordinator) : PaymentsAppService, IPaymentRequestAppService #pragma warning restore S107 { @@ -427,12 +431,15 @@ protected internal async Task> MapToDtoAndLoadDetailsAsy paymentRequestDto.AccountCodingDisplay = await GetAccountDistributionCode(paymentRequestDto.AccountCoding); } - foreach (var expenseApproval in paymentRequestDto.ExpenseApprovals) + if (paymentRequestDto != null && paymentRequestDto.ExpenseApprovals != null) { - if (expenseApproval.DecisionUserId.HasValue - && userDictionary.TryGetValue(expenseApproval.DecisionUserId.Value, out var expenseApprovalUserDto)) + foreach (var expenseApproval in paymentRequestDto.ExpenseApprovals) { - expenseApproval.DecisionUser = expenseApprovalUserDto; + if (expenseApproval.DecisionUserId.HasValue + && userDictionary.TryGetValue(expenseApproval.DecisionUserId.Value, out var expenseApprovalUserDto)) + { + expenseApproval.DecisionUser = expenseApprovalUserDto; + } } } } @@ -524,6 +531,23 @@ protected virtual string GetCurrentRequesterName() return null; } + public async Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequestIds) + { + List paymentRequestDtos = []; + foreach (var paymentRequestId in paymentRequestIds) + { + var paymentRequest = await paymentRequestsRepository.GetAsync(paymentRequestId); + if (paymentRequest != null) + { + var paymentRequestDto = ObjectMapper.Map(paymentRequest); + Site site = await siteRepository.GetAsync(paymentRequest.SiteId); + paymentRequestDto.Site = ObjectMapper.Map(site); + paymentRequestDtos.Add(paymentRequestDto); + } + } + await casPaymentRequestCoordinator.ManuallyAddPaymentRequestsToReconciliationQueue(paymentRequestDtos); + } + private async Task GetNextSequenceNumberAsync(int currentYear) { // Retrieve all payment requests diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js index 274e29c5a..3433d107c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js @@ -40,6 +40,20 @@ $(function () { let selectedPaymentIds = []; let actionButtons = [ + { + text: 'Check Status', + className: 'custom-table-btn flex-none btn btn-secondary payment-check-status', + action: function (e, dt, node, config) { + $.ajax({ + url: '/api/app/payment-request/manually-add-payment-requests-to-reconciliation-queue', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(selectedPaymentIds) + }) + .done(() => abp.notify.info(l('Successfully Added To Reconciliation Queue'))) + .fail(() => abp.notify.error(l('Failed To Add To Reconciliation Queue'))); + } + }, { text: 'Approve', className: 'custom-table-btn flex-none btn btn-secondary payment-status', @@ -146,9 +160,11 @@ $(function () { }); let payment_approve_buttons = dataTable.buttons(['.payment-status']); + let payment_check_status_buttons = dataTable.buttons(['.payment-check-status']); let history_button = dataTable.buttons(['.history']); payment_approve_buttons.disable(); + payment_check_status_buttons.disable(); dataTable.on('search.dt', () => handleSearch()); function checkAllRowsHaveState(state) { @@ -220,6 +236,11 @@ $(function () { function checkActionButtons() { let isOnlySubmittedToCas = checkAllRowsHaveState('Submitted'); + if (isOnlySubmittedToCas) { + payment_check_status_buttons.enable(); + } else { + payment_check_status_buttons.disable(); + } if (dataTable.rows({ selected: true }).indexes().length > 0 && !isOnlySubmittedToCas) { if (abp.auth.isGranted('PaymentsPermissions.Payments.L1ApproveOrDecline') || abp.auth.isGranted('PaymentsPermissions.Payments.L2ApproveOrDecline') @@ -466,7 +487,7 @@ $(function () { data: 'paymentDate', className: 'data-table-header', index: columnIndex, - render: function(data) { + render: function (data) { if (!data) return null; // Check if date is in DD-MMM-YYYY format if (/^\d{2}-[A-Z]{3}-\d{4}$/.test(data)) { @@ -476,7 +497,7 @@ $(function () { } // Use default render for other formats return DataTable.render.date('YYYY-MM-DD', abp.localization.currentCulture.name)(data); - } + } }; } @@ -613,7 +634,7 @@ $(function () { index: columnIndex, render: function (data) { let tagNames = data - .filter(x =>x?.tag?.name) + .filter(x => x?.tag?.name) .map(x => x.tag.name); return tagNames.join(', ') ?? ''; } From 24d5493691bd71c97e527c5d2adfad90f14e69bd Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 10 Nov 2025 16:23:23 -0800 Subject: [PATCH 3/8] feature/AB#27735-RefreshSiteList --- .../Suppliers/SupplierAppService_Tests.cs | 243 +++++++++++------- 1 file changed, 157 insertions(+), 86 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs index 5e954d8dd..ded81bffe 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs @@ -1,103 +1,174 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Unity.Payments.Domain.Suppliers; +using Unity.GrantManager.Integrations; // for IEndpointManagementAppService +using Volo.Abp.Application.Dtos; using Volo.Abp.Uow; using Xunit; -namespace Unity.Payments.Suppliers; - -public class SupplierAppService_Tests : PaymentsApplicationTestBase +namespace Unity.Payments.Suppliers { - private readonly ISupplierAppService _supplierAppService; - private readonly ISupplierRepository _supplierRepository; - private readonly Volo.Abp.Uow.IUnitOfWorkManager _unitOfWorkManager; - - public SupplierAppService_Tests() + public class SupplierAppService_Tests : PaymentsApplicationTestBase { - _supplierAppService = GetRequiredService(); - _supplierRepository = GetRequiredService(); - _unitOfWorkManager = GetRequiredService(); - } + private readonly ISupplierAppService _supplierAppService; + private readonly ISupplierRepository _supplierRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; - [Fact] - [Trait("Category", "Integration")] - public async Task CreateAsync_CreatesSupplier() - { - // Arrange - CreateSupplierDto createSupplierDto = new() + public SupplierAppService_Tests() { - Name = "Supplier123", - Number = "12345", - CorrelationId = Guid.NewGuid(), - CorrelationProvider = "Applicant", - MailingAddress = "123 Goldstream Ave.", - City = "Langford", - Province = "BC", - PostalCode = "12345", - }; - - - // Act - SupplierDto supplier = await _supplierAppService.CreateAsync(createSupplierDto); - - // Assert - var dbSupplier = await _supplierRepository.GetAsync(supplier.Id); - - Assert.Equal(dbSupplier.Name, createSupplierDto.Name); - Assert.Equal(dbSupplier.Number, createSupplierDto.Number); - Assert.Equal(dbSupplier.MailingAddress, createSupplierDto.MailingAddress); - Assert.Equal(dbSupplier.City, createSupplierDto.City); - Assert.Equal(dbSupplier.Province, createSupplierDto.Province); - Assert.Equal(dbSupplier.PostalCode, createSupplierDto.PostalCode); - } + // Before resolving anything, ensure the fake is registered. + ConfigureTestServices(); + + _supplierAppService = GetRequiredService(); + _supplierRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + + /// + /// Registers the stub implementation so SupplierService can resolve it. + /// + private void ConfigureTestServices() + { + var services = ServiceProvider.GetRequiredService(); + services.AddSingleton(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task CreateAsync_CreatesSupplier() + { + // Arrange + var createSupplierDto = new CreateSupplierDto + { + Name = "Supplier123", + Number = "12345", + CorrelationId = Guid.NewGuid(), + CorrelationProvider = "Applicant", + MailingAddress = "123 Goldstream Ave.", + City = "Langford", + Province = "BC", + PostalCode = "12345", + }; + + // Act + var supplier = await _supplierAppService.CreateAsync(createSupplierDto); + + // Assert + var dbSupplier = await _supplierRepository.GetAsync(supplier.Id); + Assert.Equal(dbSupplier.Name, createSupplierDto.Name); + Assert.Equal(dbSupplier.Number, createSupplierDto.Number); + Assert.Equal(dbSupplier.MailingAddress, createSupplierDto.MailingAddress); + Assert.Equal(dbSupplier.City, createSupplierDto.City); + Assert.Equal(dbSupplier.Province, createSupplierDto.Province); + Assert.Equal(dbSupplier.PostalCode, createSupplierDto.PostalCode); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task UpdateAsync_UpdatesSupplier() + { + // Arrange + var updateSupplierDto = new UpdateSupplierDto + { + Name = "Supplier456", + Number = "67890", + Subcategory = "", + ProviderId = "", + BusinessNumber = "", + Status = "", + SupplierProtected = "", + StandardIndustryClassification = "", + LastUpdatedInCAS = DateTime.Now, + MailingAddress = "890 Peatt Road", + City = "Langford", + Province = "BC", + PostalCode = "67890", + }; - [Fact] - [Trait("Category", "Integration")] - public async Task UpdateAsync_UpdatesSupplier() + var createSupplierDto = new CreateSupplierDto + { + Name = "Supplier123", + Number = "12345", + CorrelationId = Guid.NewGuid(), + CorrelationProvider = "Applicant", + MailingAddress = "123 Goldstream Ave.", + City = "Langford", + Province = "BC", + PostalCode = "12345", + }; + + using var uow = _unitOfWorkManager.Begin(); + + // Act + var supplier = await _supplierAppService.CreateAsync(createSupplierDto); + var updatedSupplier = await _supplierAppService.UpdateAsync(supplier.Id, updateSupplierDto); + + // Assert + var dbSupplier = await _supplierRepository.GetAsync(supplier.Id); + Assert.Equal(dbSupplier.Name, updateSupplierDto.Name); + Assert.Equal(dbSupplier.Number, updateSupplierDto.Number); + Assert.Equal(dbSupplier.MailingAddress, updateSupplierDto.MailingAddress); + Assert.Equal(dbSupplier.City, updateSupplierDto.City); + Assert.Equal(dbSupplier.Province, updateSupplierDto.Province); + Assert.Equal(dbSupplier.PostalCode, updateSupplierDto.PostalCode); + } + } + /// + /// Minimal stub for IEndpointManagementAppService so Autofac can resolve dependencies. + /// + public class FakeEndpointManagementAppService : IEndpointManagementAppService { - // Arrange - var updateSupplierDto = new UpdateSupplierDto() + public Task GetEndpointAsync(string key) { - Name = "Supplier456", - Number = "67890", - Subcategory = "", - ProviderId = "", - BusinessNumber = "", - Status = "", - SupplierProtected = "", - StandardIndustryClassification = "", - LastUpdatedInCAS = System.DateTime.Now, - MailingAddress = "890 Peatt Road", - City = "Langford", - Province = "BC", - PostalCode = "67890", - }; - - CreateSupplierDto createSupplierDto = new() + // Return a dummy endpoint for test purposes + return Task.FromResult("https://fake-endpoint.local"); + } + + public Task GetChefsApiBaseUrlAsync() + { + return Task.FromResult("https://fake-chefs-api.local"); + } + + public Task GetUrlByKeyNameAsync(string keyName) + { + return Task.FromResult("https://fake-url.local"); + } + + public Task GetUgmUrlByKeyNameAsync(string keyName) + { + return Task.FromResult("https://fake-ugm-url.local"); + } + + public Task ClearCacheAsync(Guid? id) + { + return Task.CompletedTask; + } + + public Task GetAsync(Guid id) { - Name = "Supplier123", - Number = "12345", - CorrelationId = Guid.NewGuid(), - CorrelationProvider = "Applicant", - MailingAddress = "123 Goldstream Ave.", - City = "Langford", - Province = "BC", - PostalCode = "12345", - }; - - using var uow = _unitOfWorkManager.Begin(); - // Act - SupplierDto supplier = await _supplierAppService.CreateAsync(createSupplierDto); - SupplierDto updatedSupDto = await _supplierAppService.UpdateAsync(supplier.Id, updateSupplierDto); - - // Assert - var dbSupplier = await _supplierRepository.GetAsync(supplier.Id); - Assert.Equal(dbSupplier.Name, updateSupplierDto.Name); - Assert.Equal(dbSupplier.Number, updateSupplierDto.Number); - Assert.Equal(dbSupplier.MailingAddress, updateSupplierDto.MailingAddress); - Assert.Equal(dbSupplier.City, updateSupplierDto.City); - Assert.Equal(dbSupplier.Province, updateSupplierDto.Province); - Assert.Equal(dbSupplier.PostalCode, updateSupplierDto.PostalCode); + return Task.FromResult(new DynamicUrlDto()); + } + public Task> GetListAsync(PagedAndSortedResultRequestDto input) + { + return Task.FromResult(new PagedResultDto()); + } + + public Task CreateAsync(CreateUpdateDynamicUrlDto input) + { + return Task.FromResult(new DynamicUrlDto()); + } + + public Task UpdateAsync(Guid id, CreateUpdateDynamicUrlDto input) + { + return Task.FromResult(new DynamicUrlDto()); + } + + public Task DeleteAsync(Guid id) + { + return Task.CompletedTask; + } } + } From 8f6664c2900a36c8c3dccfd43bab7b86d55de57a Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 10 Nov 2025 17:14:19 -0800 Subject: [PATCH 4/8] AB#9155: Applicant Profile - Contacts and Addresses - Initial Draft --- .../UpdateApplicantContactAddressesDto.cs | 31 ++ .../Applicants/ApplicantAppService.cs | 93 ++++++ .../Applicants/IApplicantAppService.cs | 1 + .../Applications/IApplicantAgentRepository.cs | 16 +- .../Repositories/ApplicantAgentRepository.cs | 13 +- .../Pages/Applicants/Details.cshtml | 5 +- .../Pages/Applicants/Details.css | 5 +- .../Pages/Applicants/Details.js | 63 +++- .../ApplicantAddressesController.cs | 19 ++ .../ApplicantAddressesViewComponent.cs | 174 +++++++++++ .../ApplicantAddressesViewModel.cs | 79 +++++ .../ApplicantAddresses/Default.cshtml | 201 +++++++++++++ .../Components/ApplicantAddresses/Default.css | 113 ++++++++ .../Components/ApplicantAddresses/Default.js | 269 ++++++++++++++++++ .../ApplicantOrganizationInfo/Default.cshtml | 10 +- .../ApplicantOrganizationInfo/Default.css | 15 +- 16 files changed, 1082 insertions(+), 25 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesController.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewComponent.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs new file mode 100644 index 000000000..523fbce1a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs @@ -0,0 +1,31 @@ +using System; + +namespace Unity.GrantManager.Applicants; + +public class UpdateApplicantContactAddressesDto +{ + public UpdatePrimaryContactDto? PrimaryContact { get; set; } + public UpdatePrimaryApplicantAddressDto? PrimaryPhysicalAddress { get; set; } + public UpdatePrimaryApplicantAddressDto? PrimaryMailingAddress { get; set; } +} + +public class UpdatePrimaryContactDto +{ + public Guid Id { get; set; } + public string? FullName { get; set; } + public string? Title { get; set; } + public string? Email { get; set; } + public string? BusinessPhone { get; set; } + public string? CellPhone { get; set; } +} + +public class UpdatePrimaryApplicantAddressDto +{ + public Guid Id { get; set; } + public string? Street { get; set; } + public string? Street2 { get; set; } + public string? Unit { get; set; } + public string? City { get; set; } + public string? Province { get; set; } + public string? PostalCode { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index 14bd1e02c..ca7abab24 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -214,6 +214,99 @@ public async Task PartialUpdateApplicantSummaryAsync(Guid applicantId return await applicantRepository.UpdateAsync(applicant); } + public async Task UpdateApplicantContactAddressesAsync(Guid applicantId, UpdateApplicantContactAddressesDto input) + { + if (applicantId == Guid.Empty) + { + throw new ArgumentException("ApplicantId cannot be empty.", nameof(applicantId)); + } + + ArgumentNullException.ThrowIfNull(input); + + if (input.PrimaryContact == null && + input.PrimaryPhysicalAddress == null && + input.PrimaryMailingAddress == null) + { + return; + } + + if (input.PrimaryContact != null && + await AuthorizationService.IsGrantedAsync(UnitySelector.Applicant.Contact.Update)) + { + await UpdatePrimaryContactAsync(applicantId, input.PrimaryContact); + } + + if (input.PrimaryPhysicalAddress != null && + await AuthorizationService.IsGrantedAsync(UnitySelector.Applicant.Location.Update)) + { + await UpdatePrimaryAddressAsync(applicantId, input.PrimaryPhysicalAddress, GrantApplications.AddressType.PhysicalAddress); + } + + if (input.PrimaryMailingAddress != null && + await AuthorizationService.IsGrantedAsync(UnitySelector.Applicant.Location.Update)) + { + await UpdatePrimaryAddressAsync(applicantId, input.PrimaryMailingAddress, GrantApplications.AddressType.MailingAddress); + } + } + + private async Task UpdatePrimaryContactAsync(Guid applicantId, UpdatePrimaryContactDto input) + { + if (input.Id == Guid.Empty) + { + throw new ArgumentException("Contact identifier is required.", nameof(input)); + } + + var applicantAgent = await applicantAgentRepository.GetAsync(input.Id); + if (applicantAgent.ApplicantId != applicantId) + { + throw new BusinessException("Unity:Applicant:ContactNotFound") + .WithData("ApplicantId", applicantId) + .WithData("ContactId", input.Id); + } + + applicantAgent.Name = input.FullName?.Trim() ?? string.Empty; + applicantAgent.Title = input.Title?.Trim() ?? string.Empty; + applicantAgent.Email = input.Email?.Trim() ?? string.Empty; + applicantAgent.Phone = input.BusinessPhone?.Trim() ?? string.Empty; + applicantAgent.Phone2 = input.CellPhone?.Trim() ?? string.Empty; + + await applicantAgentRepository.UpdateAsync(applicantAgent); + } + + private async Task UpdatePrimaryAddressAsync(Guid applicantId, UpdatePrimaryApplicantAddressDto input, GrantApplications.AddressType expectedType) + { + if (input.Id == Guid.Empty) + { + throw new ArgumentException("Address identifier is required.", nameof(input)); + } + + var applicantAddress = await addressRepository.GetAsync(input.Id); + + if (applicantAddress.ApplicantId != applicantId) + { + throw new BusinessException("Unity:Applicant:AddressNotFound") + .WithData("ApplicantId", applicantId) + .WithData("AddressId", input.Id); + } + + if (applicantAddress.AddressType != expectedType) + { + throw new BusinessException("Unity:Applicant:AddressTypeMismatch") + .WithData("ApplicantId", applicantId) + .WithData("AddressId", input.Id) + .WithData("ExpectedType", expectedType.ToString()); + } + + applicantAddress.Street = input.Street?.Trim() ?? string.Empty; + applicantAddress.Street2 = input.Street2?.Trim() ?? string.Empty; + applicantAddress.Unit = input.Unit?.Trim() ?? string.Empty; + applicantAddress.City = input.City?.Trim() ?? string.Empty; + applicantAddress.Province = input.Province?.Trim() ?? string.Empty; + applicantAddress.Postal = input.PostalCode?.Trim() ?? string.Empty; + + await addressRepository.UpdateAsync(applicantAddress); + } + [RemoteService(true)] public async Task MatchApplicantOrgNamesAsync() { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs index dcaab117b..0cedccdf1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantAppService.cs @@ -24,4 +24,5 @@ public interface IApplicantAppService : IApplicationService Task GetApplicantLookUpAutocompleteQueryAsync(string? applicantLookUpQuery); Task> GetListAsync(ApplicantListRequestDto input); Task PartialUpdateApplicantSummaryAsync(Guid applicantId, PartialUpdateDto input); + Task UpdateApplicantContactAddressesAsync(Guid applicantId, UpdateApplicantContactAddressesDto input); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicantAgentRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicantAgentRepository.cs index df344e0dc..ac464473f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicantAgentRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IApplicantAgentRepository.cs @@ -1,10 +1,12 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; namespace Unity.GrantManager.Applications; -public interface IApplicantAgentRepository : IRepository -{ - Task GetByApplicantIdAsync(Guid applicantId); -} +public interface IApplicantAgentRepository : IRepository +{ + Task GetByApplicantIdAsync(Guid applicantId); + Task> GetListByApplicantIdAsync(Guid applicantId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantAgentRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantAgentRepository.cs index 6a3c582f8..494ce4c8c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantAgentRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantAgentRepository.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Unity.GrantManager.Applications; @@ -26,5 +28,14 @@ public ApplicantAgentRepository(IDbContextProvider dbConte return await dbContext.ApplicantAgents.FirstOrDefaultAsync(x => x.ApplicantId == applicantId); } + public async Task> GetListByApplicantIdAsync(Guid applicantId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.ApplicantAgents + .Where(agent => agent.ApplicantId == applicantId) + .OrderByDescending(agent => agent.LastModificationTime ?? agent.CreationTime) + .ToListAsync(); + } + } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml index 1897322d7..b376444a5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml @@ -52,10 +52,9 @@ @*-------- Organization Info Section END ---------*@ @*-------- Addresses Section ---------*@ - +
-
Under Construction
- @*-------- await Component.InvokeAsync("ApplicantAddresses", new { applicantId = Model.ApplicantId }) ---------*@ + @await Component.InvokeAsync("ApplicantAddresses", new { applicantId = Model.ApplicantId })
@*-------- Addresses Section END ---------*@ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css index 8a6c915a8..0fb63a205 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.css @@ -23,7 +23,7 @@ #detailsTab .tab-content { overflow-y: scroll; overflow-x: hidden; - height: calc(100vh - 250px); + height: calc(100vh - 220px); } #detailsTab .nav-item .nav-link { @@ -36,7 +36,7 @@ .details-scrollable { overflow-y: scroll; overflow-x: hidden; - height: calc(100vh - 250px); + height: calc(100vh - 220px); margin-right: -6px; } @@ -64,6 +64,7 @@ #main-divider { width: 10px; + flex: 0 0 10px; background-color: #ccc; cursor: col-resize; border-radius: 10px; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js index d6fb2633f..d8c9ebef3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js @@ -9,8 +9,17 @@ $(document).ready(function () { // Handle tab switching animations $('button[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) { - let targetTab = $(e.target).attr('data-bs-target'); - $(document).find(targetTab).addClass('fade-in-load visible'); + var targetTab = $(e.target).attr('data-bs-target'); + $(targetTab).addClass('fade-in-load visible'); + }); + + // Add event listeners for tab clicks to adjust DataTables + $('#detailsTab li').on('click', function () { + debouncedAdjustTables('detailsTab'); + }); + + $('#myTabContent li').on('click', function () { + debouncedAdjustTables('myTabContent'); }); // Handle resizable divider @@ -24,6 +33,53 @@ function initializeApplicantDetailsPage() { $('.fade-in-load').addClass('visible'); }); }, 500); + + // Initialize tooltips if any + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); +} + +// Debounce utility function +function debounce(func, wait) { + let timeout; + return function (...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} + +// Debounced DataTable resizing function (called during panel resize) +const debouncedResizeAwareDataTables = debounce(() => { + $('table[data-resize-aware="true"]:visible').each(function () { + try { + const table = $(this).DataTable(); + table.columns.adjust().draw(); + } + catch (error) { + console.error('Failed to adjust DataTable columns:', error); + } + }); +}, 15); + +// Debounced function for adjusting tables in specific container when tab is clicked +const debouncedAdjustTables = debounce(adjustVisibleTablesInContainer, 15); + +function adjustVisibleTablesInContainer(containerId) { + const activeTab = $(`#${containerId} div.active`); + const tables = activeTab.find('table[data-resize-aware="true"]:visible'); + + tables.each(function () { + try { + const table = $(this).DataTable(); + table.columns.adjust().draw(); + } + catch (error) { + console.error('Failed to adjust DataTable in tab:', error); + } + }); } function initializeResizableDivider() { @@ -61,6 +117,9 @@ function initializeResizableDivider() { if (leftPercentage >= 20 && leftPercentage <= 80) { leftPanel.style.width = leftPercentage + '%'; rightPanel.style.width = rightPercentage + '%'; + + // Resize DataTables during panel resize + debouncedResizeAwareDataTables(); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesController.cs new file mode 100644 index 000000000..f63ef6c2b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc; + +namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantAddresses +{ + [ApiController] + [Route("Widget/ApplicantAddresses")] + public class ApplicantAddressesController : AbpController + { + [HttpGet] + [Route("Refresh")] + public async Task Refresh(Guid applicantId) + { + return ViewComponent("ApplicantAddresses", new { applicantId }); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewComponent.cs new file mode 100644 index 000000000..ae468b3c7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewComponent.cs @@ -0,0 +1,174 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.Modules.Shared; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; +using Volo.Abp.Authorization.Permissions; + + +namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantAddresses +{ + [Widget( + RefreshUrl = "Widget/ApplicantAddresses/Refresh", + ScriptTypes = new[] { typeof(ApplicantAddressesScriptBundleContributor) }, + StyleTypes = new[] { typeof(ApplicantAddressesStyleBundleContributor) }, + AutoInitialize = true)] + public class ApplicantAddressesViewComponent : AbpViewComponent + { + private readonly IApplicantAddressRepository _applicantAddressRepository; + private readonly IApplicantAgentRepository _applicantAgentRepository; + private readonly IPermissionChecker _permissionChecker; + + public ApplicantAddressesViewComponent( + IApplicantAddressRepository applicantAddressRepository, + IApplicantAgentRepository applicantAgentRepository, + IPermissionChecker permissionChecker) + { + _applicantAddressRepository = applicantAddressRepository; + _applicantAgentRepository = applicantAgentRepository; + _permissionChecker = permissionChecker; + } + + public async Task InvokeAsync(Guid applicantId) + { + if (applicantId == Guid.Empty) + { + return View(new ApplicantAddressesViewModel { ApplicantId = applicantId }); + } + + + // Load addresses using repository method + // Note: The repository method returns addresses without Application navigation property loaded + // We'll handle null Application gracefully in the mapping + var addresses = await _applicantAddressRepository.FindByApplicantIdAsync(applicantId); + var agents = await _applicantAgentRepository.GetListByApplicantIdAsync(applicantId); + + var orderedAddresses = addresses + .OrderByDescending(a => a.LastModificationTime ?? a.CreationTime) + .ToList(); + + var orderedAgents = agents + .OrderByDescending(a => a.LastModificationTime ?? a.CreationTime) + .ToList(); + + var viewModel = new ApplicantAddressesViewModel + { + ApplicantId = applicantId, + CanEditContact = await _permissionChecker.IsGrantedAsync(UnitySelector.Applicant.Contact.Update), + CanEditAddress = await _permissionChecker.IsGrantedAsync(UnitySelector.Applicant.Location.Update), + Addresses = orderedAddresses + .Select(a => new ApplicantAddressItemDto + { + Id = a.Id, + AddressType = GetAddressTypeName(a.AddressType), + Street = a.Street ?? string.Empty, + Street2 = a.Street2 ?? string.Empty, + Unit = a.Unit ?? string.Empty, + City = a.City ?? string.Empty, + Province = a.Province ?? string.Empty, + Postal = a.Postal ?? string.Empty, + Country = a.Country ?? string.Empty + }).ToList(), + Contacts = orderedAgents + .Select((agent, index) => new ApplicantContactItemDto + { + Id = agent.Id, + Name = agent.Name ?? string.Empty, + Email = agent.Email ?? string.Empty, + Phone = !string.IsNullOrWhiteSpace(agent.Phone) + ? agent.Phone! + : agent.Phone2 ?? string.Empty, + Title = agent.Title ?? string.Empty, + Type = index == 0 ? "Primary" : "", + CreationTime = agent.CreationTime + }) + .ToList() + }; + + var primaryContact = orderedAgents.FirstOrDefault(); + if (primaryContact != null) + { + viewModel.PrimaryContact = new ApplicantPrimaryContactViewModel + { + Id = primaryContact.Id, + FullName = primaryContact.Name ?? string.Empty, + Title = primaryContact.Title ?? string.Empty, + Email = primaryContact.Email ?? string.Empty, + BusinessPhone = primaryContact.Phone ?? string.Empty, + CellPhone = primaryContact.Phone2 ?? string.Empty + }; + } + + var primaryPhysicalAddress = FindMostRecentAddress(orderedAddresses, GrantApplications.AddressType.PhysicalAddress); + if (primaryPhysicalAddress != null) + { + viewModel.PrimaryPhysicalAddress = MapPrimaryAddress(primaryPhysicalAddress); + } + + var primaryMailingAddress = FindMostRecentAddress(orderedAddresses, GrantApplications.AddressType.MailingAddress); + if (primaryMailingAddress != null) + { + viewModel.PrimaryMailingAddress = MapPrimaryAddress(primaryMailingAddress); + } + + return View(viewModel); + + } + + private static ApplicantAddress? FindMostRecentAddress(IEnumerable addresses, GrantApplications.AddressType addressType) + { + return addresses + .Where(address => address.AddressType == addressType) + .OrderByDescending(address => address.LastModificationTime ?? address.CreationTime) + .FirstOrDefault(); + } + + private static ApplicantPrimaryAddressViewModel MapPrimaryAddress(ApplicantAddress address) + { + return new ApplicantPrimaryAddressViewModel + { + Id = address.Id, + Street = address.Street ?? string.Empty, + Street2 = address.Street2 ?? string.Empty, + Unit = address.Unit ?? string.Empty, + City = address.City ?? string.Empty, + Province = address.Province ?? string.Empty, + PostalCode = address.Postal ?? string.Empty + }; + } + + private string GetAddressTypeName(GrantApplications.AddressType addressType) + { + return addressType switch + { + GrantApplications.AddressType.PhysicalAddress => "Physical", + GrantApplications.AddressType.MailingAddress => "Mailing", + GrantApplications.AddressType.BusinessAddress => "Business", + _ => "Unknown" + }; + } + } + + public class ApplicantAddressesStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Views/Shared/Components/ApplicantAddresses/Default.css"); + } + } + + public class ApplicantAddressesScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Views/Shared/Components/ApplicantAddresses/Default.js"); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewModel.cs new file mode 100644 index 000000000..42ad849d5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/ApplicantAddressesViewModel.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantAddresses +{ + public class ApplicantAddressesViewModel + { + public Guid ApplicantId { get; set; } + public bool CanEditContact { get; set; } + public bool CanEditAddress { get; set; } + public bool CanSave => (CanEditContact && PrimaryContact.IsEditable) + || (CanEditAddress && (PrimaryPhysicalAddress.IsEditable || PrimaryMailingAddress.IsEditable)); + public ApplicantPrimaryContactViewModel PrimaryContact { get; set; } = new(); + public ApplicantPrimaryAddressViewModel PrimaryPhysicalAddress { get; set; } = new(); + public ApplicantPrimaryAddressViewModel PrimaryMailingAddress { get; set; } = new(); + public List Contacts { get; set; } = new(); + public List Addresses { get; set; } = new List(); + } + + public class ApplicantPrimaryContactViewModel + { + public Guid Id { get; set; } + [Display(Name = "Full Name")] + public string FullName { get; set; } = string.Empty; + [Display(Name = "Title")] + public string Title { get; set; } = string.Empty; + [Display(Name = "Business Phone")] + public string BusinessPhone { get; set; } = string.Empty; + [Display(Name = "Cell Phone")] + public string CellPhone { get; set; } = string.Empty; + [Display(Name = "Email")] + public string Email { get; set; } = string.Empty; + public bool IsEditable => Id != Guid.Empty; + } + + public class ApplicantPrimaryAddressViewModel + { + public Guid Id { get; set; } + [Display(Name = "Street")] + public string Street { get; set; } = string.Empty; + [Display(Name = "Street 2")] + public string Street2 { get; set; } = string.Empty; + [Display(Name = "Unit")] + public string Unit { get; set; } = string.Empty; + [Display(Name = "City")] + public string City { get; set; } = string.Empty; + [Display(Name = "Province")] + public string Province { get; set; } = string.Empty; + [Display(Name = "Postal Code")] + public string PostalCode { get; set; } = string.Empty; + public bool IsEditable => Id != Guid.Empty; + } + + public class ApplicantContactItemDto + { + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Phone { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public DateTime CreationTime { get; set; } + } + + public class ApplicantAddressItemDto + { + public Guid Id { get; set; } + public string AddressType { get; set; } = string.Empty; + public string ReferenceNo { get; set; } = string.Empty; + public string Street { get; set; } = string.Empty; + public string Street2 { get; set; } = string.Empty; + public string Unit { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string Province { get; set; } = string.Empty; + public string Postal { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.cshtml new file mode 100644 index 000000000..5a816d24a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.cshtml @@ -0,0 +1,201 @@ +@using Unity.GrantManager.Web.Views.Shared.Components.ApplicantAddresses +@using System.Text.Json + +@model ApplicantAddressesViewModel + +@{ + Layout = null; + var canEditPrimaryContact = Model.PrimaryContact.IsEditable && Model.CanEditContact; + var canEditPrimaryPhysical = Model.PrimaryPhysicalAddress.IsEditable && Model.CanEditAddress; + var canEditPrimaryMailing = Model.PrimaryMailingAddress.IsEditable && Model.CanEditAddress; +} + +
+ @if (Model.ApplicantId == Guid.Empty) + { +
+ Applicant information not found. +
+ } + else + { + @if (Model.CanSave) + { +
+ +
+ } + +
+ + + + + +
+
+
Primary Contact
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ @if (!Model.PrimaryContact.IsEditable) + { +
No primary contact on record.
+ } +
+
+ +
+
Primary Physical Address
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ @if (!Model.PrimaryPhysicalAddress.IsEditable) + { +
No primary physical address on record.
+ } +
+
+ +
+
Primary Mailing Address
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ @if (!Model.PrimaryMailingAddress.IsEditable) + { +
No primary mailing address on record.
+ } +
+
+
+
+ +
+
+
+
Contacts
+
+
+ + +
+
+
+
+
Addresses
+
+
+ + +
+
+
+ } +
+ + + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css new file mode 100644 index 000000000..35501c08d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.css @@ -0,0 +1,113 @@ +/* ApplicantAddresses Component Styles */ + +.applicant-addresses-widget { + position: relative; +} + +.applicant-info-container { + margin-bottom: 1.5rem; + position: relative; +} + +.applicant-info-container .save-button-container { + display: flex; + justify-content: flex-end; + position: sticky; + top: 50px; + padding-right: 24px; + z-index: 999; +} + +.applicant-info-container .floating-save-btn { + min-width: 120px; + position: static; +} + +.applicant-organization-info { + background-color: #f8f9fa; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +.primary-sections { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.primary-section { + padding-bottom: 1rem; + border-bottom: 1px solid #dee2e6; +} + +.primary-section:last-child { + border-bottom: none; +} + +.primary-section__title { + font-weight: 600; + margin-bottom: 0.75rem; + color: #1f2933; +} + +.contacts-addresses-tables { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.table-card { + background-color: #fff; + border: 1px solid #e0e6ed; + border-radius: 8px; + box-shadow: 0 6px 12px rgba(1, 51, 102, 0.05); +} + +.table-card__header { + border-bottom: 1px solid #e0e6ed; + padding: 0.75rem 1.25rem; +} + +.table-card__body { + padding: 1.25rem; +} + +.addresses-table, +.contacts-table { + width: 100%; +} + +#ApplicantAddressesTable_wrapper, +#ApplicantContactsTable_wrapper { + width: 100%; +} + +#ApplicantAddressesTable_wrapper .dataTables_scroll, +#ApplicantContactsTable_wrapper .dataTables_scroll { + overflow: visible; +} + +#ApplicantAddressesTable_wrapper .dataTables_scrollHead .dataTables_scrollHeadInner, +#ApplicantContactsTable_wrapper .dataTables_scrollHead .dataTables_scrollHeadInner, +#ApplicantAddressesTable_wrapper .dataTables_scrollHead .dataTables_scrollHeadInner table, +#ApplicantContactsTable_wrapper .dataTables_scrollHead .dataTables_scrollHeadInner table { + width: 100% !important; +} + +#ApplicantAddressesTable_wrapper .dataTables_scrollBody, +#ApplicantContactsTable_wrapper .dataTables_scrollBody { + overflow-x: auto; + overflow-y: visible; + max-height: none !important; +} + +#ApplicantAddressesTable_wrapper .dataTables_scrollBody table, +#ApplicantContactsTable_wrapper .dataTables_scrollBody table { + width: 100% !important; +} + +#ApplicantAddressesTable, +#ApplicantContactsTable { + width: 100% !important; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.js new file mode 100644 index 000000000..ece8dc55e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAddresses/Default.js @@ -0,0 +1,269 @@ +$(function () { + const contactsRaw = $('#ApplicantContacts_Data').val(); + const addressesRaw = $('#ApplicantAddresses_Data').val(); + const contactsData = safeParse(contactsRaw); + const addressesData = safeParse(addressesRaw); + + const nullPlaceholder = '—'; + let contactsTable = null; + let addressesTable = null; + let zoneForm = null; + + if ($.fn.DataTable && $('#ApplicantContactsTable').length) { + contactsTable = $('#ApplicantContactsTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + data: contactsData, + serverSide: false, + order: [[0, 'asc']], + searching: true, + paging: true, + pageLength: 10, + lengthMenu: [[10, 25, 50], [10, 25, 50]], + select: false, + info: true, + scrollX: true, + drawCallback: function () { + this.api().columns.adjust(); + }, + columnDefs: [ + { + title: 'Name', + data: 'name', + width: '20%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Email', + data: 'email', + width: '25%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Phone', + data: 'phone', + width: '15%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Title', + data: 'title', + width: '20%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Type', + data: 'type', + width: '10%', + render: (data) => data || nullPlaceholder + } + ] + }) + ); + } + + if ($.fn.DataTable && $('#ApplicantAddressesTable').length) { + addressesTable = $('#ApplicantAddressesTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + data: addressesData, + serverSide: false, + order: [[0, 'asc']], + searching: true, + paging: true, + pageLength: 10, + select: false, + info: true, + scrollX: true, + drawCallback: function () { + this.api().columns.adjust(); + }, + columnDefs: [ + { + title: 'Address Type', + data: 'addressType', + width: '15%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Address', + data: 'street', + width: '25%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Unit', + data: 'unit', + width: '10%', + render: (data) => data || nullPlaceholder + }, + { + title: 'City', + data: 'city', + width: '15%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Province', + data: 'province', + width: '15%', + render: (data) => data || nullPlaceholder + }, + { + title: 'Postal Code', + data: 'postal', + width: '10%', + render: (data) => data || nullPlaceholder + } + ] + }) + ); + } + + const form = $('#ApplicantAddressesForm'); + const saveButton = $('#saveApplicantAddressesBtn'); + + if (form.length && saveButton.length && typeof UnityZoneForm === 'function') { + zoneForm = new UnityZoneForm(form, { + saveButtonSelector: '#saveApplicantAddressesBtn' + }); + + zoneForm.init(); + + saveButton.on('click', function (event) { + event.preventDefault(); + + if (!zoneForm || zoneForm.modifiedFields.size === 0) { + return; + } + + const payload = buildSavePayload(zoneForm, form); + if (!payload) { + return; + } + + const applicantId = $('#ApplicantAddresses_ApplicantId').val(); + if (!applicantId) { + abp.notify.warn('Applicant identifier is missing.'); + return; + } + + unity.grantManager.applicants.applicant + .updateApplicantContactAddresses(applicantId, payload) + .done(function () { + abp.notify.success('Contacts and addresses updated.'); + zoneForm.resetTracking(); + updateTablesAfterSave(payload, contactsTable, addressesTable); + }) + .fail(function () { + abp.notify.error('Failed to update contacts and addresses.'); + }); + }); + } + + function safeParse(value) { + try { + return JSON.parse(value || '[]'); + } catch (error) { + console.warn('Unable to parse ApplicantAddresses data.', error); + return []; + } + } + + function buildSavePayload(zoneFormInstance, $form) { + const modifiedFields = Array.from(zoneFormInstance.modifiedFields ?? []); + + const contactDirty = modifiedFields.some((field) => field.startsWith('PrimaryContact.')); + const physicalDirty = modifiedFields.some((field) => field.startsWith('PrimaryPhysicalAddress.')); + const mailingDirty = modifiedFields.some((field) => field.startsWith('PrimaryMailingAddress.')); + + const payload = {}; + + if (contactDirty) { + const contactId = $('#ApplicantAddresses_PrimaryContactId').val(); + if (!isGuidEmpty(contactId)) { + payload.primaryContact = { + id: contactId, + fullName: $form.find('[name="PrimaryContact.FullName"]').val(), + title: $form.find('[name="PrimaryContact.Title"]').val(), + email: $form.find('[name="PrimaryContact.Email"]').val(), + businessPhone: $form.find('[name="PrimaryContact.BusinessPhone"]').val(), + cellPhone: $form.find('[name="PrimaryContact.CellPhone"]').val() + }; + } + } + + if (physicalDirty) { + const addressId = $('#ApplicantAddresses_PrimaryPhysicalAddressId').val(); + if (!isGuidEmpty(addressId)) { + payload.primaryPhysicalAddress = buildAddressPayload(addressId, 'PrimaryPhysicalAddress', $form); + } + } + + if (mailingDirty) { + const addressId = $('#ApplicantAddresses_PrimaryMailingAddressId').val(); + if (!isGuidEmpty(addressId)) { + payload.primaryMailingAddress = buildAddressPayload(addressId, 'PrimaryMailingAddress', $form); + } + } + + if (!payload.primaryContact && !payload.primaryPhysicalAddress && !payload.primaryMailingAddress) { + return null; + } + + return payload; + } + + function isGuidEmpty(value) { + return !value || value === '00000000-0000-0000-0000-000000000000'; + } + + function buildAddressPayload(addressId, prefix, $form) { + return { + id: addressId, + street: $form.find(`[name="${prefix}.Street"]`).val(), + street2: $form.find(`[name="${prefix}.Street2"]`).val(), + unit: $form.find(`[name="${prefix}.Unit"]`).val(), + city: $form.find(`[name="${prefix}.City"]`).val(), + province: $form.find(`[name="${prefix}.Province"]`).val(), + postalCode: $form.find(`[name="${prefix}.PostalCode"]`).val() + }; + } + + function updateTablesAfterSave(payload, contactsDt, addressesDt) { + if (contactsDt && payload.primaryContact) { + contactsDt.rows().every(function () { + const rowData = this.data(); + if (rowData.id === payload.primaryContact.id) { + rowData.name = payload.primaryContact.fullName || ''; + rowData.email = payload.primaryContact.email || ''; + rowData.phone = payload.primaryContact.businessPhone || payload.primaryContact.cellPhone || ''; + rowData.title = payload.primaryContact.title || ''; + this.data(rowData); + } + }); + contactsDt.rows().invalidate().draw(false); + } + + if (addressesDt) { + ['primaryPhysicalAddress', 'primaryMailingAddress'].forEach((key) => { + const addressPayload = payload[key]; + if (!addressPayload) { + return; + } + addressesDt.rows().every(function () { + const rowData = this.data(); + if (rowData.id === addressPayload.id) { + rowData.street = addressPayload.street || ''; + rowData.street2 = addressPayload.street2 || ''; + rowData.unit = addressPayload.unit || ''; + rowData.city = addressPayload.city || ''; + rowData.province = addressPayload.province || ''; + rowData.postal = addressPayload.postalCode || ''; + this.data(rowData); + } + }); + }); + + addressesDt.rows().invalidate().draw(false); + } + } +}); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml index e804a1999..d17ccb78e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantOrganizationInfo/Default.cshtml @@ -12,7 +12,7 @@ @if (Model.ApplicantId != Guid.Empty) { - +
-
+ @* Applicant Info *@ - + @@ -37,7 +37,7 @@ - + @@ -63,7 +63,7 @@ - +
Organization Information
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 70fbe9a5b..68c757ac7 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 @@ -12,12 +12,12 @@ } /* Payment info container styles */ -.payment-info-container { +.applicant-info-container { margin-bottom: 1.5rem; position: relative; } -.payment-info-container .save-button-container { +.applicant-info-container .save-button-container { display: flex; justify-content: flex-end; position: sticky; @@ -26,11 +26,11 @@ z-index: 999; } -.payment-info-container .save-button-container .floating-save-btn { +.applicant-info-container .save-button-container .floating-save-btn { position: static; } -.payment-info-supplier { +.applicant-organization-info { background-color: #f8f9fa; border-radius: 8px; padding: 1rem; @@ -42,7 +42,6 @@ } .project-location { - border-bottom: 1px solid #dee2e6; margin-bottom: 1rem; } @@ -51,6 +50,12 @@ padding-bottom: 0.5rem; } +.applicant-organization-info__divider_line { + border-bottom: 1px solid #dee2e6; + margin-bottom: 1.25rem; + padding-bottom: 1.25rem; +} + /* Form field spacing */ .px-1 { padding-left: 0.25rem !important; From d62ecdee554ed8120f4167eec19bc0b1b763eabc Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 12 Nov 2025 11:46:52 -0800 Subject: [PATCH 5/8] feature/AB#27735-RefreshSiteList-removebrokentest --- .../Suppliers/SupplierAppService_Tests.cs | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs b/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs deleted file mode 100644 index ded81bffe..000000000 --- a/applications/Unity.GrantManager/modules/Unity.Payments/test/Unity.Payments.Application.Tests/Suppliers/SupplierAppService_Tests.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Unity.Payments.Domain.Suppliers; -using Unity.GrantManager.Integrations; // for IEndpointManagementAppService -using Volo.Abp.Application.Dtos; -using Volo.Abp.Uow; -using Xunit; - -namespace Unity.Payments.Suppliers -{ - public class SupplierAppService_Tests : PaymentsApplicationTestBase - { - private readonly ISupplierAppService _supplierAppService; - private readonly ISupplierRepository _supplierRepository; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public SupplierAppService_Tests() - { - // Before resolving anything, ensure the fake is registered. - ConfigureTestServices(); - - _supplierAppService = GetRequiredService(); - _supplierRepository = GetRequiredService(); - _unitOfWorkManager = GetRequiredService(); - } - - /// - /// Registers the stub implementation so SupplierService can resolve it. - /// - private void ConfigureTestServices() - { - var services = ServiceProvider.GetRequiredService(); - services.AddSingleton(); - } - - [Fact] - [Trait("Category", "Integration")] - public async Task CreateAsync_CreatesSupplier() - { - // Arrange - var createSupplierDto = new CreateSupplierDto - { - Name = "Supplier123", - Number = "12345", - CorrelationId = Guid.NewGuid(), - CorrelationProvider = "Applicant", - MailingAddress = "123 Goldstream Ave.", - City = "Langford", - Province = "BC", - PostalCode = "12345", - }; - - // Act - var supplier = await _supplierAppService.CreateAsync(createSupplierDto); - - // Assert - var dbSupplier = await _supplierRepository.GetAsync(supplier.Id); - Assert.Equal(dbSupplier.Name, createSupplierDto.Name); - Assert.Equal(dbSupplier.Number, createSupplierDto.Number); - Assert.Equal(dbSupplier.MailingAddress, createSupplierDto.MailingAddress); - Assert.Equal(dbSupplier.City, createSupplierDto.City); - Assert.Equal(dbSupplier.Province, createSupplierDto.Province); - Assert.Equal(dbSupplier.PostalCode, createSupplierDto.PostalCode); - } - - [Fact] - [Trait("Category", "Integration")] - public async Task UpdateAsync_UpdatesSupplier() - { - // Arrange - var updateSupplierDto = new UpdateSupplierDto - { - Name = "Supplier456", - Number = "67890", - Subcategory = "", - ProviderId = "", - BusinessNumber = "", - Status = "", - SupplierProtected = "", - StandardIndustryClassification = "", - LastUpdatedInCAS = DateTime.Now, - MailingAddress = "890 Peatt Road", - City = "Langford", - Province = "BC", - PostalCode = "67890", - }; - - var createSupplierDto = new CreateSupplierDto - { - Name = "Supplier123", - Number = "12345", - CorrelationId = Guid.NewGuid(), - CorrelationProvider = "Applicant", - MailingAddress = "123 Goldstream Ave.", - City = "Langford", - Province = "BC", - PostalCode = "12345", - }; - - using var uow = _unitOfWorkManager.Begin(); - - // Act - var supplier = await _supplierAppService.CreateAsync(createSupplierDto); - var updatedSupplier = await _supplierAppService.UpdateAsync(supplier.Id, updateSupplierDto); - - // Assert - var dbSupplier = await _supplierRepository.GetAsync(supplier.Id); - Assert.Equal(dbSupplier.Name, updateSupplierDto.Name); - Assert.Equal(dbSupplier.Number, updateSupplierDto.Number); - Assert.Equal(dbSupplier.MailingAddress, updateSupplierDto.MailingAddress); - Assert.Equal(dbSupplier.City, updateSupplierDto.City); - Assert.Equal(dbSupplier.Province, updateSupplierDto.Province); - Assert.Equal(dbSupplier.PostalCode, updateSupplierDto.PostalCode); - } - } - /// - /// Minimal stub for IEndpointManagementAppService so Autofac can resolve dependencies. - /// - public class FakeEndpointManagementAppService : IEndpointManagementAppService - { - public Task GetEndpointAsync(string key) - { - // Return a dummy endpoint for test purposes - return Task.FromResult("https://fake-endpoint.local"); - } - - public Task GetChefsApiBaseUrlAsync() - { - return Task.FromResult("https://fake-chefs-api.local"); - } - - public Task GetUrlByKeyNameAsync(string keyName) - { - return Task.FromResult("https://fake-url.local"); - } - - public Task GetUgmUrlByKeyNameAsync(string keyName) - { - return Task.FromResult("https://fake-ugm-url.local"); - } - - public Task ClearCacheAsync(Guid? id) - { - return Task.CompletedTask; - } - - public Task GetAsync(Guid id) - { - return Task.FromResult(new DynamicUrlDto()); - } - - public Task> GetListAsync(PagedAndSortedResultRequestDto input) - { - return Task.FromResult(new PagedResultDto()); - } - - public Task CreateAsync(CreateUpdateDynamicUrlDto input) - { - return Task.FromResult(new DynamicUrlDto()); - } - - public Task UpdateAsync(Guid id, CreateUpdateDynamicUrlDto input) - { - return Task.FromResult(new DynamicUrlDto()); - } - - public Task DeleteAsync(Guid id) - { - return Task.CompletedTask; - } - } - -} From 6131b46d564a0ce79ecd1bc3b8e863a85b31899c Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 12 Nov 2025 14:02:40 -0800 Subject: [PATCH 6/8] feature/AB#29737-ManualRetrieveFromCAS --- .../PaymentRequests/IPaymentRequestAppService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs index 87916d6e6..c3e72c776 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application.Contracts/PaymentRequests/IPaymentRequestAppService.cs @@ -19,6 +19,6 @@ public interface IPaymentRequestAppService : IApplicationService Task GetNextBatchInfoAsync(); Task GetDefaultAccountCodingId(); Task GetUserPaymentThresholdAsync(); - Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequests); + Task ManuallyAddPaymentRequestsToReconciliationQueue(List paymentRequestIds); } } From 994e393c0de9695f885717c8950a1d977514f69b Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 12 Nov 2025 16:56:26 -0800 Subject: [PATCH 7/8] bugfix/AB#30834-FixDataSeeder --- .../NotificationsDataSeedContributor.cs | 3 ++- .../GrantManagerDataSeederContributor.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs index 7c9d8ef03..b7dcd4d7f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/NotificationsDataSeedContributor.cs @@ -49,7 +49,8 @@ public async Task SeedAsync(DataSeedContext context) { foreach (var template in emailTemplateVariableDtos) { - var existingVariable = await templateVariablesRepository.FindAsync(tv => tv.Token == template.Token); + var allVariables = await templateVariablesRepository.GetListAsync(); + var existingVariable = allVariables.FirstOrDefault(tv => tv.Token == template.Token); if (existingVariable == null) { await templateVariablesRepository.InsertAsync( diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs index 9fe38d74e..a3ee4b5a9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/GrantManagerDataSeederContributor.cs @@ -9,7 +9,7 @@ namespace Unity.GrantManager; public class GrantManagerDataSeederContributor( - IApplicationStatusRepository applicationStatusRepository) : IDataSeeder, ITransientDependency + IApplicationStatusRepository applicationStatusRepository) : IDataSeedContributor, ITransientDependency { public static class GrantApplicationStates { From 63dd725f392f886abbf03a91df7105baee14bafd Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Thu, 13 Nov 2025 11:22:23 -0800 Subject: [PATCH 8/8] AB#30436: Fix ApplicationLinksTable Misalignment --- .../Unity.GrantManager.Web/Pages/GrantApplications/Details.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index bc129b5e0..d3444d2e7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -340,7 +340,7 @@ $(function () { setDetailsContext('application'); }); - $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + $('button[data-bs-toggle="tab"]').on('shown.bs.tab', function () { $($.fn.dataTable.tables(true)).DataTable().columns.adjust(); });