From 43de04d9cdb48ae047246c8f0529b796699abb3b Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 16:51:18 -0700 Subject: [PATCH 01/47] AB#28858: Multiple worksheets for AssessmentInfo Make Assessment Info drop zone support multiple worksheets --- .../LinkWorksheetsModal.cshtml | 19 ++++++---- .../LinkWorksheetsModal.cshtml.cs | 29 +++++++++----- .../ApplicationForms/LinkWorksheetsModal.js | 38 ++++++++++++++++--- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml index a18486337..32c716e01 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml @@ -12,7 +12,7 @@
- + @@ -37,14 +37,17 @@
Assessment Info:
-
- @if (Model.AssessmentInfoLink != null) +
+ @if (Model.AssessmentInfoLinks != null) { -
- - @Model.AssessmentInfoLink.Worksheet.Title (@Model.AssessmentInfoLink.Worksheet.Name) - -
+ @foreach (var assessmentInfo in Model.AssessmentInfoLinks.OrderBy(s => s.Order)) + { +
+ + @assessmentInfo.Worksheet.Title (@assessmentInfo.Worksheet.Name) + +
+ } }
Project Info:
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs index e219e13ed..6de8e8f2d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs @@ -22,7 +22,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public string? FormName { get; set; } [BindProperty] - public string? AssessmentInfoSlotId { get; set; } + public string? AssessmentInfoSlotIds { get; set; } [BindProperty] public string? ProjectInfoSlotId { get; set; } @@ -46,7 +46,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public List? PublishedWorksheets { get; set; } [BindProperty] - public WorksheetLinkDto? AssessmentInfoLink { get; set; } + public List? AssessmentInfoLinks { get; set; } [BindProperty] public WorksheetLinkDto? ApplicantInfoLink { get; set; } @@ -81,13 +81,15 @@ public async Task OnGetAsync(Guid formVersionId, string formName) CustomTabsSlotIds = string.Join(";", CustomTabLinks .OrderBy(s => s.Order) .Select(s => s.WorksheetId)); + + AssessmentInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.AssessmentInfoUiAnchor).ToList(); + AssessmentInfoSlotIds = string.Join(";", AssessmentInfoLinks + .OrderBy(s => s.Order) + .Select(s => s.WorksheetId)); } private void GetSlotIdAnchors(List worksheetLinks) { - AssessmentInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.AssessmentInfoUiAnchor); - AssessmentInfoSlotId = AssessmentInfoLink?.WorksheetId.ToString(); - ProjectInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.ProjectInfoUiAnchor); ProjectInfoSlotId = ProjectInfoLink?.WorksheetId.ToString(); @@ -119,6 +121,18 @@ public async Task OnPostAsync() } } + if (AssessmentInfoSlotIds != null && AssessmentInfoSlotIds != Guid.Empty.ToString()) + { + var assessmentInfoTabs = AssessmentInfoSlotIds.Split(';'); + uint order = 1; + + foreach (var assessmentInfoTabId in assessmentInfoTabs) //this comes in sequenced as we want for assessment info tabs + { + tabLinks.Add(new(Guid.Parse(assessmentInfoTabId), FlexConsts.AssessmentInfoUiAnchor, order)); + order++; + } + } + var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(ChefsFormVersionId); _ = await worksheetLinkAppService .UpdateWorksheetLinksAsync(new UpdateWorksheetLinksDto() @@ -134,11 +148,6 @@ public async Task OnPostAsync() private void AddSlotIdAnchors(List<(Guid worksheetId, string anchor, uint order)> tabLinks) { // We leave the order for the predefined tabs as 0 as they slot into a fixed position - if (AssessmentInfoSlotId != null && AssessmentInfoSlotId != Guid.Empty.ToString()) - { - tabLinks.Add(new(Guid.Parse(AssessmentInfoSlotId), FlexConsts.AssessmentInfoUiAnchor, 0)); - } - if (ProjectInfoSlotId != null && ProjectInfoSlotId != Guid.Empty.ToString()) { tabLinks.Add(new(Guid.Parse(ProjectInfoSlotId), FlexConsts.ProjectInfoUiAnchor, 0)); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js index 2eb2b395d..671bd8d1f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js @@ -2,12 +2,14 @@ let lastDroppedLocation = {}; let lastDragFromLocation = {}; let customTabIds = []; + let assessmentInfoIds = []; PubSub.subscribe( 'refresh_configure_worksheets', () => { lastDroppedLocation = {}; customTabIds = []; + assessmentInfoIds = []; } ); @@ -54,6 +56,12 @@ if (dragOver.classList.contains('multi-target') && event.target.classList.contains('custom-tabs-list')) { dropToCustomTabs(event, null, 'published-form'); + return; + } + + if (dragOver.classList.contains('multi-target') + && event.target.classList.contains('assessment-info-list')) { + dropToAssessmentInfo(event, null, 'published-form'); } }); @@ -73,14 +81,12 @@ clearSlotId(); updateSlotId(draggedEl); storeCustomTabsIdChange(); + storeAssessmentInfoIdChange(); } } function clearSlotId() { switch (lastDragFromLocation?.dataset?.target) { - case 'assessmentInfo': - $('#AssessmentInfoSlotId').val(null); - break; case 'projectInfo': $('#ProjectInfoSlotId').val(null); break; @@ -98,9 +104,6 @@ function updateSlotId(draggedEl) { switch (lastDroppedLocation?.dataset?.target) { - case 'assessmentInfo': - $('#AssessmentInfoSlotId').val(draggedEl.dataset.worksheetId); - break; case 'projectInfo': $('#ProjectInfoSlotId').val(draggedEl.dataset.worksheetId); break; @@ -131,6 +134,20 @@ storeCustomTabsIdChange(); } + function dropToAssessmentInfo(event, addClass, removeClass) { + event.preventDefault(); + + let dragOver = event.target; + let beingDragged = document.querySelector('.dragging'); + + // handle reordering in the ui + + updateDraggedClasses(beingDragged, addClass, removeClass); + dragOver.appendChild(beingDragged); + lastDroppedLocation = dragOver; + storeAssessmentInfoIdChange(); + } + function storeCustomTabsIdChange() { customTabIds= []; let items = Array.from($('.custom-tabs-list').children()); @@ -140,6 +157,15 @@ $('#CustomTabsSlotIds').val(customTabIds.join(';')); } + function storeAssessmentInfoIdChange() { + assessmentInfoIds = []; + let items = Array.from($('.assessment-info-list').children()); + items.forEach((item) => { + assessmentInfoIds.push(item.dataset.worksheetId); + }); + $('#AssessmentInfoSlotIds').val(assessmentInfoIds.join(';')); + } + function dropToAvailableWorksheets(event, addClass, removeClass) { dropToSingleTarget(event, addClass, removeClass); sortAvailableWorksheets(); From 3bb3241f0721d441afd9bd97d8eb7cb12f6b44d3 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 17:26:34 -0700 Subject: [PATCH 02/47] AB#28858: Multiple worksheets for Project Info Allow multiple worksheets for Project Info --- .../LinkWorksheetsModal.cshtml | 19 ++++++---- .../LinkWorksheetsModal.cshtml.cs | 29 +++++++++----- .../ApplicationForms/LinkWorksheetsModal.js | 38 ++++++++++++++++--- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml index 32c716e01..9768a6016 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml @@ -14,7 +14,7 @@ - + @@ -51,14 +51,17 @@ }
Project Info:
-
- @if (Model.ProjectInfoLink != null) +
+ @if (Model.ProjectInfoLinks != null) { -
- - @Model.ProjectInfoLink.Worksheet.Title (@Model.ProjectInfoLink.Worksheet.Name) - -
+ @foreach (var projectInfo in Model.ProjectInfoLinks.OrderBy(s => s.Order)) + { +
+ + @projectInfo.Worksheet.Title (@projectInfo.Worksheet.Name) + +
+ } }
Applicant Info:
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs index 6de8e8f2d..37e0c4ff1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs @@ -25,7 +25,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public string? AssessmentInfoSlotIds { get; set; } [BindProperty] - public string? ProjectInfoSlotId { get; set; } + public string? ProjectInfoSlotIds { get; set; } [BindProperty] public string? ApplicantInfoSlotId { get; set; } @@ -52,7 +52,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public WorksheetLinkDto? ApplicantInfoLink { get; set; } [BindProperty] - public WorksheetLinkDto? ProjectInfoLink { get; set; } + public List? ProjectInfoLinks { get; set; } [BindProperty] public WorksheetLinkDto? PaymentInfoLink { get; set; } @@ -86,13 +86,15 @@ public async Task OnGetAsync(Guid formVersionId, string formName) AssessmentInfoSlotIds = string.Join(";", AssessmentInfoLinks .OrderBy(s => s.Order) .Select(s => s.WorksheetId)); + + ProjectInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.ProjectInfoUiAnchor).ToList(); + ProjectInfoSlotIds = string.Join(";", ProjectInfoLinks + .OrderBy(s => s.Order) + .Select(s => s.WorksheetId)); } private void GetSlotIdAnchors(List worksheetLinks) { - ProjectInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.ProjectInfoUiAnchor); - ProjectInfoSlotId = ProjectInfoLink?.WorksheetId.ToString(); - ApplicantInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.ApplicantInfoUiAnchor); ApplicantInfoSlotId = ApplicantInfoLink?.WorksheetId.ToString(); @@ -133,6 +135,18 @@ public async Task OnPostAsync() } } + if (ProjectInfoSlotIds != null && ProjectInfoSlotIds != Guid.Empty.ToString()) + { + var projectInfoTabs = ProjectInfoSlotIds.Split(';'); + uint order = 1; + + foreach (var projectInfoTabId in projectInfoTabs) //this comes in sequenced as we want for project info tabs + { + tabLinks.Add(new(Guid.Parse(projectInfoTabId), FlexConsts.ProjectInfoUiAnchor, order)); + order++; + } + } + var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(ChefsFormVersionId); _ = await worksheetLinkAppService .UpdateWorksheetLinksAsync(new UpdateWorksheetLinksDto() @@ -148,11 +162,6 @@ public async Task OnPostAsync() private void AddSlotIdAnchors(List<(Guid worksheetId, string anchor, uint order)> tabLinks) { // We leave the order for the predefined tabs as 0 as they slot into a fixed position - if (ProjectInfoSlotId != null && ProjectInfoSlotId != Guid.Empty.ToString()) - { - tabLinks.Add(new(Guid.Parse(ProjectInfoSlotId), FlexConsts.ProjectInfoUiAnchor, 0)); - } - if (ApplicantInfoSlotId != null && ApplicantInfoSlotId != Guid.Empty.ToString()) { tabLinks.Add(new(Guid.Parse(ApplicantInfoSlotId), FlexConsts.ApplicantInfoUiAnchor, 0)); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js index 671bd8d1f..c3f7b5bec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js @@ -3,6 +3,7 @@ let lastDragFromLocation = {}; let customTabIds = []; let assessmentInfoIds = []; + let projectInfoIds = []; PubSub.subscribe( 'refresh_configure_worksheets', @@ -10,6 +11,7 @@ lastDroppedLocation = {}; customTabIds = []; assessmentInfoIds = []; + projectInfoIds = []; } ); @@ -62,6 +64,12 @@ if (dragOver.classList.contains('multi-target') && event.target.classList.contains('assessment-info-list')) { dropToAssessmentInfo(event, null, 'published-form'); + return; + } + + if (dragOver.classList.contains('multi-target') + && event.target.classList.contains('project-info-list')) { + dropToProjectInfo(event, null, 'published-form'); } }); @@ -82,14 +90,12 @@ updateSlotId(draggedEl); storeCustomTabsIdChange(); storeAssessmentInfoIdChange(); + storeProjectInfoIdChange(); } } function clearSlotId() { switch (lastDragFromLocation?.dataset?.target) { - case 'projectInfo': - $('#ProjectInfoSlotId').val(null); - break; case 'applicantInfo': $('#ApplicantInfoSlotId').val(null); break; @@ -104,9 +110,6 @@ function updateSlotId(draggedEl) { switch (lastDroppedLocation?.dataset?.target) { - case 'projectInfo': - $('#ProjectInfoSlotId').val(draggedEl.dataset.worksheetId); - break; case 'applicantInfo': $('#ApplicantInfoSlotId').val(draggedEl.dataset.worksheetId); break; @@ -157,6 +160,20 @@ $('#CustomTabsSlotIds').val(customTabIds.join(';')); } + function dropToProjectInfo(event, addClass, removeClass) { + event.preventDefault(); + + let dragOver = event.target; + let beingDragged = document.querySelector('.dragging'); + + // handle reordering in the ui + + updateDraggedClasses(beingDragged, addClass, removeClass); + dragOver.appendChild(beingDragged); + lastDroppedLocation = dragOver; + storeProjectInfoIdChange(); + } + function storeAssessmentInfoIdChange() { assessmentInfoIds = []; let items = Array.from($('.assessment-info-list').children()); @@ -166,6 +183,15 @@ $('#AssessmentInfoSlotIds').val(assessmentInfoIds.join(';')); } + function storeProjectInfoIdChange() { + projectInfoIds = []; + let items = Array.from($('.project-info-list').children()); + items.forEach((item) => { + projectInfoIds.push(item.dataset.worksheetId); + }); + $('#ProjectInfoSlotIds').val(projectInfoIds.join(';')); + } + function dropToAvailableWorksheets(event, addClass, removeClass) { dropToSingleTarget(event, addClass, removeClass); sortAvailableWorksheets(); From bff07ebd7c3592d07b4143574c2664d7589f14a3 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 17:37:01 -0700 Subject: [PATCH 03/47] AB#28858: Multiple worksheets for ApplicantInfo Allow multiple worksheets for ApplicantInfo --- .../LinkWorksheetsModal.cshtml | 19 ++++++---- .../LinkWorksheetsModal.cshtml.cs | 29 +++++++++----- .../ApplicationForms/LinkWorksheetsModal.js | 38 ++++++++++++++++--- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml index 9768a6016..587d3e8f8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml @@ -13,7 +13,7 @@ - + @@ -65,14 +65,17 @@ }
Applicant Info:
-
- @if (Model.ApplicantInfoLink != null) +
+ @if (Model.ApplicantInfoLinks != null) { -
- - @Model.ApplicantInfoLink.Worksheet.Title (@Model.ApplicantInfoLink.Worksheet.Name) - -
+ @foreach (var applicantInfo in Model.ApplicantInfoLinks.OrderBy(s => s.Order)) + { +
+ + @applicantInfo.Worksheet.Title (@applicantInfo.Worksheet.Name) + +
+ } }
Payment Info:
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs index 37e0c4ff1..4bcab19bb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs @@ -28,7 +28,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public string? ProjectInfoSlotIds { get; set; } [BindProperty] - public string? ApplicantInfoSlotId { get; set; } + public string? ApplicantInfoSlotIds { get; set; } [BindProperty] public string? PaymentInfoSlotId { get; set; } @@ -49,7 +49,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public List? AssessmentInfoLinks { get; set; } [BindProperty] - public WorksheetLinkDto? ApplicantInfoLink { get; set; } + public List? ApplicantInfoLinks { get; set; } [BindProperty] public List? ProjectInfoLinks { get; set; } @@ -91,13 +91,15 @@ public async Task OnGetAsync(Guid formVersionId, string formName) ProjectInfoSlotIds = string.Join(";", ProjectInfoLinks .OrderBy(s => s.Order) .Select(s => s.WorksheetId)); + + ApplicantInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.ApplicantInfoUiAnchor).ToList(); + ApplicantInfoSlotIds = string.Join(";", ApplicantInfoLinks + .OrderBy(s => s.Order) + .Select(s => s.WorksheetId)); } private void GetSlotIdAnchors(List worksheetLinks) { - ApplicantInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.ApplicantInfoUiAnchor); - ApplicantInfoSlotId = ApplicantInfoLink?.WorksheetId.ToString(); - PaymentInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.PaymentInfoUiAnchor); PaymentInfoSlotId = PaymentInfoLink?.WorksheetId.ToString(); @@ -147,6 +149,18 @@ public async Task OnPostAsync() } } + if (ApplicantInfoSlotIds != null && ApplicantInfoSlotIds != Guid.Empty.ToString()) + { + var applicantInfoTabs = ApplicantInfoSlotIds.Split(';'); + uint order = 1; + + foreach (var applicantInfoTabId in applicantInfoTabs) //this comes in sequenced as we want for applicant info tabs + { + tabLinks.Add(new(Guid.Parse(applicantInfoTabId), FlexConsts.ApplicantInfoUiAnchor, order)); + order++; + } + } + var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(ChefsFormVersionId); _ = await worksheetLinkAppService .UpdateWorksheetLinksAsync(new UpdateWorksheetLinksDto() @@ -162,11 +176,6 @@ public async Task OnPostAsync() private void AddSlotIdAnchors(List<(Guid worksheetId, string anchor, uint order)> tabLinks) { // We leave the order for the predefined tabs as 0 as they slot into a fixed position - if (ApplicantInfoSlotId != null && ApplicantInfoSlotId != Guid.Empty.ToString()) - { - tabLinks.Add(new(Guid.Parse(ApplicantInfoSlotId), FlexConsts.ApplicantInfoUiAnchor, 0)); - } - if (PaymentInfoSlotId != null && PaymentInfoSlotId != Guid.Empty.ToString()) { tabLinks.Add(new(Guid.Parse(PaymentInfoSlotId), FlexConsts.PaymentInfoUiAnchor, 0)); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js index c3f7b5bec..c2c4c39f0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js @@ -4,6 +4,7 @@ let customTabIds = []; let assessmentInfoIds = []; let projectInfoIds = []; + let applicantInfoIds = []; PubSub.subscribe( 'refresh_configure_worksheets', @@ -12,6 +13,7 @@ customTabIds = []; assessmentInfoIds = []; projectInfoIds = []; + applicantInfoIds = []; } ); @@ -70,6 +72,12 @@ if (dragOver.classList.contains('multi-target') && event.target.classList.contains('project-info-list')) { dropToProjectInfo(event, null, 'published-form'); + return; + } + + if (dragOver.classList.contains('multi-target') + && event.target.classList.contains('applicant-info-list')) { + dropToApplicantInfo(event, null, 'published-form'); } }); @@ -91,14 +99,12 @@ storeCustomTabsIdChange(); storeAssessmentInfoIdChange(); storeProjectInfoIdChange(); + storeApplicantInfoIdChange(); } } function clearSlotId() { switch (lastDragFromLocation?.dataset?.target) { - case 'applicantInfo': - $('#ApplicantInfoSlotId').val(null); - break; case 'paymentInfo': $('#PaymentInfoSlotId').val(null); break; @@ -110,9 +116,6 @@ function updateSlotId(draggedEl) { switch (lastDroppedLocation?.dataset?.target) { - case 'applicantInfo': - $('#ApplicantInfoSlotId').val(draggedEl.dataset.worksheetId); - break; case 'paymentInfo': $('#PaymentInfoSlotId').val(draggedEl.dataset.worksheetId); break; @@ -183,6 +186,20 @@ $('#AssessmentInfoSlotIds').val(assessmentInfoIds.join(';')); } + function dropToApplicantInfo(event, addClass, removeClass) { + event.preventDefault(); + + let dragOver = event.target; + let beingDragged = document.querySelector('.dragging'); + + // handle reordering in the ui + + updateDraggedClasses(beingDragged, addClass, removeClass); + dragOver.appendChild(beingDragged); + lastDroppedLocation = dragOver; + storeApplicantInfoIdChange(); + } + function storeProjectInfoIdChange() { projectInfoIds = []; let items = Array.from($('.project-info-list').children()); @@ -192,6 +209,15 @@ $('#ProjectInfoSlotIds').val(projectInfoIds.join(';')); } + function storeApplicantInfoIdChange() { + applicantInfoIds = []; + let items = Array.from($('.applicant-info-list').children()); + items.forEach((item) => { + applicantInfoIds.push(item.dataset.worksheetId); + }); + $('#ApplicantInfoSlotIds').val(applicantInfoIds.join(';')); + } + function dropToAvailableWorksheets(event, addClass, removeClass) { dropToSingleTarget(event, addClass, removeClass); sortAvailableWorksheets(); From b9058ab70a2120ab4e2ebcaae93b1913a73a3fa6 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 19:29:32 -0700 Subject: [PATCH 04/47] AB#28858: Muptiple worksheets for PaymentInfo allow multiple worksheets for PaymentInfo --- .../LinkWorksheetsModal.cshtml | 19 ++++++---- .../LinkWorksheetsModal.cshtml.cs | 29 +++++++++----- .../ApplicationForms/LinkWorksheetsModal.js | 38 ++++++++++++++++--- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml index 587d3e8f8..cace3a43f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml @@ -15,7 +15,7 @@ - + @@ -79,14 +79,17 @@ }
Payment Info:
-
- @if (Model.PaymentInfoLink != null) +
+ @if (Model.PaymentInfoLinks != null) { -
- - @Model.PaymentInfoLink.Worksheet.Title (@Model.PaymentInfoLink.Worksheet.Name) - -
+ @foreach (var paymentInfo in Model.PaymentInfoLinks.OrderBy(s => s.Order)) + { +
+ + @paymentInfo.Worksheet.Title (@paymentInfo.Worksheet.Name) + +
+ } }
Funding Agreement:
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs index 4bcab19bb..558f366e1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs @@ -31,7 +31,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public string? ApplicantInfoSlotIds { get; set; } [BindProperty] - public string? PaymentInfoSlotId { get; set; } + public string? PaymentInfoSlotIds { get; set; } [BindProperty] public string? FundingAgreementInfoSlotId { get; set; } @@ -55,7 +55,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public List? ProjectInfoLinks { get; set; } [BindProperty] - public WorksheetLinkDto? PaymentInfoLink { get; set; } + public List? PaymentInfoLinks { get; set; } [BindProperty] public WorksheetLinkDto? FundingAgreementInfoLink { get; set; } @@ -96,13 +96,15 @@ public async Task OnGetAsync(Guid formVersionId, string formName) ApplicantInfoSlotIds = string.Join(";", ApplicantInfoLinks .OrderBy(s => s.Order) .Select(s => s.WorksheetId)); + + PaymentInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.PaymentInfoUiAnchor).ToList(); + PaymentInfoSlotIds = string.Join(";", PaymentInfoLinks + .OrderBy(s => s.Order) + .Select(s => s.WorksheetId)); } private void GetSlotIdAnchors(List worksheetLinks) { - PaymentInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.PaymentInfoUiAnchor); - PaymentInfoSlotId = PaymentInfoLink?.WorksheetId.ToString(); - FundingAgreementInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.FundingAgreementInfoUiAnchor); FundingAgreementInfoSlotId = FundingAgreementInfoLink?.WorksheetId.ToString(); } @@ -161,6 +163,18 @@ public async Task OnPostAsync() } } + if (PaymentInfoSlotIds != null && PaymentInfoSlotIds != Guid.Empty.ToString()) + { + var paymentInfoTabs = PaymentInfoSlotIds.Split(';'); + uint order = 1; + + foreach (var paymentInfoTabId in paymentInfoTabs) //this comes in sequenced as we want for payment info tabs + { + tabLinks.Add(new(Guid.Parse(paymentInfoTabId), FlexConsts.PaymentInfoUiAnchor, order)); + order++; + } + } + var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(ChefsFormVersionId); _ = await worksheetLinkAppService .UpdateWorksheetLinksAsync(new UpdateWorksheetLinksDto() @@ -176,11 +190,6 @@ public async Task OnPostAsync() private void AddSlotIdAnchors(List<(Guid worksheetId, string anchor, uint order)> tabLinks) { // We leave the order for the predefined tabs as 0 as they slot into a fixed position - if (PaymentInfoSlotId != null && PaymentInfoSlotId != Guid.Empty.ToString()) - { - tabLinks.Add(new(Guid.Parse(PaymentInfoSlotId), FlexConsts.PaymentInfoUiAnchor, 0)); - } - if (FundingAgreementInfoSlotId != null && FundingAgreementInfoSlotId != Guid.Empty.ToString()) { tabLinks.Add(new(Guid.Parse(FundingAgreementInfoSlotId), FlexConsts.FundingAgreementInfoUiAnchor, 0)); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js index c2c4c39f0..5805d7972 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js @@ -5,6 +5,7 @@ let assessmentInfoIds = []; let projectInfoIds = []; let applicantInfoIds = []; + let paymentInfoIds = []; PubSub.subscribe( 'refresh_configure_worksheets', @@ -14,6 +15,7 @@ assessmentInfoIds = []; projectInfoIds = []; applicantInfoIds = []; + paymentInfoIds = []; } ); @@ -78,6 +80,12 @@ if (dragOver.classList.contains('multi-target') && event.target.classList.contains('applicant-info-list')) { dropToApplicantInfo(event, null, 'published-form'); + return; + } + + if (dragOver.classList.contains('multi-target') + && event.target.classList.contains('payment-info-list')) { + dropToPaymentInfo(event, null, 'published-form'); } }); @@ -100,14 +108,12 @@ storeAssessmentInfoIdChange(); storeProjectInfoIdChange(); storeApplicantInfoIdChange(); + storePaymentInfoIdChange(); } } function clearSlotId() { switch (lastDragFromLocation?.dataset?.target) { - case 'paymentInfo': - $('#PaymentInfoSlotId').val(null); - break; case 'fundingAgreementInfo': $('#FundingAgreementInfoSlotId').val(null); break; @@ -116,9 +122,6 @@ function updateSlotId(draggedEl) { switch (lastDroppedLocation?.dataset?.target) { - case 'paymentInfo': - $('#PaymentInfoSlotId').val(draggedEl.dataset.worksheetId); - break; case 'fundingAgreementInfo': $('#FundingAgreementInfoSlotId').val(draggedEl.dataset.worksheetId); break; @@ -209,6 +212,20 @@ $('#ProjectInfoSlotIds').val(projectInfoIds.join(';')); } + function dropToPaymentInfo(event, addClass, removeClass) { + event.preventDefault(); + + let dragOver = event.target; + let beingDragged = document.querySelector('.dragging'); + + // handle reordering in the ui + + updateDraggedClasses(beingDragged, addClass, removeClass); + dragOver.appendChild(beingDragged); + lastDroppedLocation = dragOver; + storePaymentInfoIdChange(); + } + function storeApplicantInfoIdChange() { applicantInfoIds = []; let items = Array.from($('.applicant-info-list').children()); @@ -218,6 +235,15 @@ $('#ApplicantInfoSlotIds').val(applicantInfoIds.join(';')); } + function storePaymentInfoIdChange() { + paymentInfoIds = []; + let items = Array.from($('.payment-info-list').children()); + items.forEach((item) => { + paymentInfoIds.push(item.dataset.worksheetId); + }); + $('#PaymentInfoSlotIds').val(paymentInfoIds.join(';')); + } + function dropToAvailableWorksheets(event, addClass, removeClass) { dropToSingleTarget(event, addClass, removeClass); sortAvailableWorksheets(); From 14952bf68cd0a8118764d4c785424a773ee4eba0 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 19:39:57 -0700 Subject: [PATCH 05/47] AB#28858: Multiple worksheets for Funding Agreeement Allow multiple worksheets for Funding Agreeement --- .../LinkWorksheetsModal.cshtml | 19 ++++---- .../LinkWorksheetsModal.cshtml.cs | 36 +++++++------- .../ApplicationForms/LinkWorksheetsModal.js | 48 ++++++++++++------- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml index cace3a43f..5728116f9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml @@ -16,7 +16,7 @@ - + @@ -93,14 +93,17 @@ }
Funding Agreement:
-
- @if (Model.FundingAgreementInfoLink != null) +
+ @if (Model.FundingAgreementInfoLinks != null) { -
- - @Model.FundingAgreementInfoLink.Worksheet.Title (@Model.FundingAgreementInfoLink.Worksheet.Name) - -
+ @foreach (var fundingAgreementInfo in Model.FundingAgreementInfoLinks.OrderBy(s => s.Order)) + { +
+ + @fundingAgreementInfo.Worksheet.Title (@fundingAgreementInfo.Worksheet.Name) + +
+ } }
Additional Tabs:
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs index 558f366e1..b08774295 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs @@ -34,7 +34,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public string? PaymentInfoSlotIds { get; set; } [BindProperty] - public string? FundingAgreementInfoSlotId { get; set; } + public string? FundingAgreementInfoSlotIds { get; set; } [BindProperty] public string? CustomTabsSlotIds { get; set; } @@ -58,7 +58,7 @@ public class LinkWorksheetModalModel(IWorksheetListAppService worksheetListAppSe public List? PaymentInfoLinks { get; set; } [BindProperty] - public WorksheetLinkDto? FundingAgreementInfoLink { get; set; } + public List? FundingAgreementInfoLinks { get; set; } [BindProperty] public List? CustomTabLinks { get; set; } @@ -75,7 +75,6 @@ public async Task OnGetAsync(Guid formVersionId, string formName) .Where(s => s.Published && !WorksheetLinks.Select(s => s.WorksheetId).Contains(s.Id)) .OrderBy(s => s.Title)]; - GetSlotIdAnchors(WorksheetLinks); CustomTabLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.CustomTab).ToList(); CustomTabsSlotIds = string.Join(";", CustomTabLinks @@ -101,19 +100,18 @@ public async Task OnGetAsync(Guid formVersionId, string formName) PaymentInfoSlotIds = string.Join(";", PaymentInfoLinks .OrderBy(s => s.Order) .Select(s => s.WorksheetId)); - } - private void GetSlotIdAnchors(List worksheetLinks) - { - FundingAgreementInfoLink = worksheetLinks.Find(s => s.UiAnchor == FlexConsts.FundingAgreementInfoUiAnchor); - FundingAgreementInfoSlotId = FundingAgreementInfoLink?.WorksheetId.ToString(); + FundingAgreementInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.FundingAgreementInfoUiAnchor).ToList(); + FundingAgreementInfoSlotIds = string.Join(";", FundingAgreementInfoLinks + .OrderBy(s => s.Order) + .Select(s => s.WorksheetId)); } + public async Task OnPostAsync() { var tabLinks = new List<(Guid worksheetId, string anchor, uint order)>(); - AddSlotIdAnchors(tabLinks); if (CustomTabsSlotIds != null && CustomTabsSlotIds != Guid.Empty.ToString()) { @@ -175,6 +173,18 @@ public async Task OnPostAsync() } } + if (FundingAgreementInfoSlotIds != null && FundingAgreementInfoSlotIds != Guid.Empty.ToString()) + { + var fundingAgreementInfoTabs = FundingAgreementInfoSlotIds.Split(';'); + uint order = 1; + + foreach (var fundingAgreementInfoTabId in fundingAgreementInfoTabs) //this comes in sequenced as we want for funding agreement info tabs + { + tabLinks.Add(new(Guid.Parse(fundingAgreementInfoTabId), FlexConsts.FundingAgreementInfoUiAnchor, order)); + order++; + } + } + var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(ChefsFormVersionId); _ = await worksheetLinkAppService .UpdateWorksheetLinksAsync(new UpdateWorksheetLinksDto() @@ -187,13 +197,5 @@ public async Task OnPostAsync() return new OkObjectResult(new { ChefsFormVersionId }); } - private void AddSlotIdAnchors(List<(Guid worksheetId, string anchor, uint order)> tabLinks) - { - // We leave the order for the predefined tabs as 0 as they slot into a fixed position - if (FundingAgreementInfoSlotId != null && FundingAgreementInfoSlotId != Guid.Empty.ToString()) - { - tabLinks.Add(new(Guid.Parse(FundingAgreementInfoSlotId), FlexConsts.FundingAgreementInfoUiAnchor, 0)); - } - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js index 5805d7972..77c26154f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js @@ -6,6 +6,7 @@ let projectInfoIds = []; let applicantInfoIds = []; let paymentInfoIds = []; + let fundingAgreementInfoIds = []; PubSub.subscribe( 'refresh_configure_worksheets', @@ -16,6 +17,7 @@ projectInfoIds = []; applicantInfoIds = []; paymentInfoIds = []; + fundingAgreementInfoIds = []; } ); @@ -86,6 +88,12 @@ if (dragOver.classList.contains('multi-target') && event.target.classList.contains('payment-info-list')) { dropToPaymentInfo(event, null, 'published-form'); + return; + } + + if (dragOver.classList.contains('multi-target') + && event.target.classList.contains('funding-agreement-info-list')) { + dropToFundingAgreementInfo(event, null, 'published-form'); } }); @@ -102,31 +110,16 @@ if (draggedEl.classList + "" != "undefined") { draggedEl.classList.remove('dragging'); - clearSlotId(); - updateSlotId(draggedEl); storeCustomTabsIdChange(); storeAssessmentInfoIdChange(); storeProjectInfoIdChange(); storeApplicantInfoIdChange(); storePaymentInfoIdChange(); + storeFundingAgreementInfoIdChange(); } } - function clearSlotId() { - switch (lastDragFromLocation?.dataset?.target) { - case 'fundingAgreementInfo': - $('#FundingAgreementInfoSlotId').val(null); - break; - } - } - function updateSlotId(draggedEl) { - switch (lastDroppedLocation?.dataset?.target) { - case 'fundingAgreementInfo': - $('#FundingAgreementInfoSlotId').val(draggedEl.dataset.worksheetId); - break; - } - } function dropToCustomTabs(event, addClass, removeClass) { @@ -235,6 +228,20 @@ $('#ApplicantInfoSlotIds').val(applicantInfoIds.join(';')); } + function dropToFundingAgreementInfo(event, addClass, removeClass) { + event.preventDefault(); + + let dragOver = event.target; + let beingDragged = document.querySelector('.dragging'); + + // handle reordering in the ui + + updateDraggedClasses(beingDragged, addClass, removeClass); + dragOver.appendChild(beingDragged); + lastDroppedLocation = dragOver; + storeFundingAgreementInfoIdChange(); + } + function storePaymentInfoIdChange() { paymentInfoIds = []; let items = Array.from($('.payment-info-list').children()); @@ -244,6 +251,15 @@ $('#PaymentInfoSlotIds').val(paymentInfoIds.join(';')); } + function storeFundingAgreementInfoIdChange() { + fundingAgreementInfoIds = []; + let items = Array.from($('.funding-agreement-info-list').children()); + items.forEach((item) => { + fundingAgreementInfoIds.push(item.dataset.worksheetId); + }); + $('#FundingAgreementInfoSlotIds').val(fundingAgreementInfoIds.join(';')); + } + function dropToAvailableWorksheets(event, addClass, removeClass) { dropToSingleTarget(event, addClass, removeClass); sortAvailableWorksheets(); From f661aff7262eb0a08c58dac7275be857a204d180 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 20:34:53 -0700 Subject: [PATCH 06/47] AB#28858: Refactor codes. Remove duplications. --- .../LinkWorksheetsModal.cshtml.cs | 132 +++++------------- .../ApplicationForms/LinkWorksheetsModal.js | 118 ++++------------ 2 files changed, 57 insertions(+), 193 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs index b08774295..b4c8dd250 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.cshtml.cs @@ -76,35 +76,12 @@ public async Task OnGetAsync(Guid formVersionId, string formName) .OrderBy(s => s.Title)]; - CustomTabLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.CustomTab).ToList(); - CustomTabsSlotIds = string.Join(";", CustomTabLinks - .OrderBy(s => s.Order) - .Select(s => s.WorksheetId)); - - AssessmentInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.AssessmentInfoUiAnchor).ToList(); - AssessmentInfoSlotIds = string.Join(";", AssessmentInfoLinks - .OrderBy(s => s.Order) - .Select(s => s.WorksheetId)); - - ProjectInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.ProjectInfoUiAnchor).ToList(); - ProjectInfoSlotIds = string.Join(";", ProjectInfoLinks - .OrderBy(s => s.Order) - .Select(s => s.WorksheetId)); - - ApplicantInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.ApplicantInfoUiAnchor).ToList(); - ApplicantInfoSlotIds = string.Join(";", ApplicantInfoLinks - .OrderBy(s => s.Order) - .Select(s => s.WorksheetId)); - - PaymentInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.PaymentInfoUiAnchor).ToList(); - PaymentInfoSlotIds = string.Join(";", PaymentInfoLinks - .OrderBy(s => s.Order) - .Select(s => s.WorksheetId)); - - FundingAgreementInfoLinks = WorksheetLinks.Where(s => s.UiAnchor == FlexConsts.FundingAgreementInfoUiAnchor).ToList(); - FundingAgreementInfoSlotIds = string.Join(";", FundingAgreementInfoLinks - .OrderBy(s => s.Order) - .Select(s => s.WorksheetId)); + (CustomTabLinks, CustomTabsSlotIds) = ProcessWorksheetLinks(WorksheetLinks, FlexConsts.CustomTab); + (AssessmentInfoLinks, AssessmentInfoSlotIds) = ProcessWorksheetLinks(WorksheetLinks, FlexConsts.AssessmentInfoUiAnchor); + (ProjectInfoLinks, ProjectInfoSlotIds) = ProcessWorksheetLinks(WorksheetLinks, FlexConsts.ProjectInfoUiAnchor); + (ApplicantInfoLinks, ApplicantInfoSlotIds) = ProcessWorksheetLinks(WorksheetLinks, FlexConsts.ApplicantInfoUiAnchor); + (PaymentInfoLinks, PaymentInfoSlotIds) = ProcessWorksheetLinks(WorksheetLinks, FlexConsts.PaymentInfoUiAnchor); + (FundingAgreementInfoLinks, FundingAgreementInfoSlotIds) = ProcessWorksheetLinks(WorksheetLinks, FlexConsts.FundingAgreementInfoUiAnchor); } @@ -113,77 +90,12 @@ public async Task OnPostAsync() var tabLinks = new List<(Guid worksheetId, string anchor, uint order)>(); - if (CustomTabsSlotIds != null && CustomTabsSlotIds != Guid.Empty.ToString()) - { - var customTabs = CustomTabsSlotIds.Split(';'); - uint order = 1; - - foreach (var customTabId in customTabs) //this comes in sequenced as we want for custom tabs - { - tabLinks.Add(new(Guid.Parse(customTabId), FlexConsts.CustomTab, order)); - order++; - } - } - - if (AssessmentInfoSlotIds != null && AssessmentInfoSlotIds != Guid.Empty.ToString()) - { - var assessmentInfoTabs = AssessmentInfoSlotIds.Split(';'); - uint order = 1; - - foreach (var assessmentInfoTabId in assessmentInfoTabs) //this comes in sequenced as we want for assessment info tabs - { - tabLinks.Add(new(Guid.Parse(assessmentInfoTabId), FlexConsts.AssessmentInfoUiAnchor, order)); - order++; - } - } - - if (ProjectInfoSlotIds != null && ProjectInfoSlotIds != Guid.Empty.ToString()) - { - var projectInfoTabs = ProjectInfoSlotIds.Split(';'); - uint order = 1; - - foreach (var projectInfoTabId in projectInfoTabs) //this comes in sequenced as we want for project info tabs - { - tabLinks.Add(new(Guid.Parse(projectInfoTabId), FlexConsts.ProjectInfoUiAnchor, order)); - order++; - } - } - - if (ApplicantInfoSlotIds != null && ApplicantInfoSlotIds != Guid.Empty.ToString()) - { - var applicantInfoTabs = ApplicantInfoSlotIds.Split(';'); - uint order = 1; - - foreach (var applicantInfoTabId in applicantInfoTabs) //this comes in sequenced as we want for applicant info tabs - { - tabLinks.Add(new(Guid.Parse(applicantInfoTabId), FlexConsts.ApplicantInfoUiAnchor, order)); - order++; - } - } - - if (PaymentInfoSlotIds != null && PaymentInfoSlotIds != Guid.Empty.ToString()) - { - var paymentInfoTabs = PaymentInfoSlotIds.Split(';'); - uint order = 1; - - foreach (var paymentInfoTabId in paymentInfoTabs) //this comes in sequenced as we want for payment info tabs - { - tabLinks.Add(new(Guid.Parse(paymentInfoTabId), FlexConsts.PaymentInfoUiAnchor, order)); - order++; - } - } - - if (FundingAgreementInfoSlotIds != null && FundingAgreementInfoSlotIds != Guid.Empty.ToString()) - { - var fundingAgreementInfoTabs = FundingAgreementInfoSlotIds.Split(';'); - uint order = 1; - - foreach (var fundingAgreementInfoTabId in fundingAgreementInfoTabs) //this comes in sequenced as we want for funding agreement info tabs - { - tabLinks.Add(new(Guid.Parse(fundingAgreementInfoTabId), FlexConsts.FundingAgreementInfoUiAnchor, order)); - order++; - } - } + ProcessSlotIds(CustomTabsSlotIds, FlexConsts.CustomTab, tabLinks); + ProcessSlotIds(AssessmentInfoSlotIds, FlexConsts.AssessmentInfoUiAnchor, tabLinks); + ProcessSlotIds(ProjectInfoSlotIds, FlexConsts.ProjectInfoUiAnchor, tabLinks); + ProcessSlotIds(ApplicantInfoSlotIds, FlexConsts.ApplicantInfoUiAnchor, tabLinks); + ProcessSlotIds(PaymentInfoSlotIds, FlexConsts.PaymentInfoUiAnchor, tabLinks); + ProcessSlotIds(FundingAgreementInfoSlotIds, FlexConsts.FundingAgreementInfoUiAnchor, tabLinks); var formVersion = await applicationFormVersionAppService.GetByChefsFormVersionId(ChefsFormVersionId); _ = await worksheetLinkAppService @@ -197,5 +109,25 @@ public async Task OnPostAsync() return new OkObjectResult(new { ChefsFormVersionId }); } + private (List links, string slotIds) ProcessWorksheetLinks(List worksheetLinks, string uiAnchor) + { + var links = worksheetLinks.Where(s => s.UiAnchor == uiAnchor).ToList(); + var slotIds = string.Join(";", links.OrderBy(s => s.Order).Select(s => s.WorksheetId)); + return (links, slotIds); + } + + private void ProcessSlotIds(string? slotIds, string uiAnchor, List<(Guid worksheetId, string anchor, uint order)> tabLinks) + { + if (!string.IsNullOrWhiteSpace(slotIds) && slotIds != Guid.Empty.ToString()) + { + var tabs = slotIds.Split(';'); + uint order = 1; + foreach (var tabId in tabs) + { + tabLinks.Add(new(Guid.Parse(tabId), uiAnchor, order)); + order++; + } + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js index 77c26154f..ccbb2d841 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.js @@ -119,144 +119,76 @@ } } + function getMultiTargetIds(cssSelector) { + let ids = []; + let items = Array.from($(cssSelector).children()); + items.forEach((item) => { + ids.push(item.dataset.worksheetId); + }); + return ids; + } - - - function dropToCustomTabs(event, addClass, removeClass) { + function dropToMultiTarget(event, addClass, removeClass, storeFunction) { event.preventDefault(); - let dragOver = event.target; let beingDragged = document.querySelector('.dragging'); - - // handle reordering in the ui - updateDraggedClasses(beingDragged, addClass, removeClass); dragOver.appendChild(beingDragged); lastDroppedLocation = dragOver; - storeCustomTabsIdChange(); + storeFunction(); } - function dropToAssessmentInfo(event, addClass, removeClass) { - event.preventDefault(); - - let dragOver = event.target; - let beingDragged = document.querySelector('.dragging'); - - // handle reordering in the ui + function dropToCustomTabs(event, addClass, removeClass) { + dropToMultiTarget(event, addClass, removeClass, storeCustomTabsIdChange); + } - updateDraggedClasses(beingDragged, addClass, removeClass); - dragOver.appendChild(beingDragged); - lastDroppedLocation = dragOver; - storeAssessmentInfoIdChange(); + function dropToAssessmentInfo(event, addClass, removeClass) { + dropToMultiTarget(event, addClass, removeClass, storeAssessmentInfoIdChange); } function storeCustomTabsIdChange() { - customTabIds= []; - let items = Array.from($('.custom-tabs-list').children()); - items.forEach((item) => { - customTabIds.push(item.dataset.worksheetId); - }); + customTabIds = getMultiTargetIds('.custom-tabs-list'); $('#CustomTabsSlotIds').val(customTabIds.join(';')); } function dropToProjectInfo(event, addClass, removeClass) { - event.preventDefault(); - - let dragOver = event.target; - let beingDragged = document.querySelector('.dragging'); - - // handle reordering in the ui - - updateDraggedClasses(beingDragged, addClass, removeClass); - dragOver.appendChild(beingDragged); - lastDroppedLocation = dragOver; - storeProjectInfoIdChange(); + dropToMultiTarget(event, addClass, removeClass, storeProjectInfoIdChange); } function storeAssessmentInfoIdChange() { - assessmentInfoIds = []; - let items = Array.from($('.assessment-info-list').children()); - items.forEach((item) => { - assessmentInfoIds.push(item.dataset.worksheetId); - }); + assessmentInfoIds = getMultiTargetIds('.assessment-info-list'); $('#AssessmentInfoSlotIds').val(assessmentInfoIds.join(';')); } function dropToApplicantInfo(event, addClass, removeClass) { - event.preventDefault(); - - let dragOver = event.target; - let beingDragged = document.querySelector('.dragging'); - - // handle reordering in the ui - - updateDraggedClasses(beingDragged, addClass, removeClass); - dragOver.appendChild(beingDragged); - lastDroppedLocation = dragOver; - storeApplicantInfoIdChange(); + dropToMultiTarget(event, addClass, removeClass, storeApplicantInfoIdChange); } function storeProjectInfoIdChange() { - projectInfoIds = []; - let items = Array.from($('.project-info-list').children()); - items.forEach((item) => { - projectInfoIds.push(item.dataset.worksheetId); - }); + projectInfoIds = getMultiTargetIds('.project-info-list'); $('#ProjectInfoSlotIds').val(projectInfoIds.join(';')); } function dropToPaymentInfo(event, addClass, removeClass) { - event.preventDefault(); - - let dragOver = event.target; - let beingDragged = document.querySelector('.dragging'); - - // handle reordering in the ui - - updateDraggedClasses(beingDragged, addClass, removeClass); - dragOver.appendChild(beingDragged); - lastDroppedLocation = dragOver; - storePaymentInfoIdChange(); + dropToMultiTarget(event, addClass, removeClass, storePaymentInfoIdChange); } function storeApplicantInfoIdChange() { - applicantInfoIds = []; - let items = Array.from($('.applicant-info-list').children()); - items.forEach((item) => { - applicantInfoIds.push(item.dataset.worksheetId); - }); + applicantInfoIds = getMultiTargetIds('.applicant-info-list'); $('#ApplicantInfoSlotIds').val(applicantInfoIds.join(';')); } function dropToFundingAgreementInfo(event, addClass, removeClass) { - event.preventDefault(); - - let dragOver = event.target; - let beingDragged = document.querySelector('.dragging'); - - // handle reordering in the ui - - updateDraggedClasses(beingDragged, addClass, removeClass); - dragOver.appendChild(beingDragged); - lastDroppedLocation = dragOver; - storeFundingAgreementInfoIdChange(); + dropToMultiTarget(event, addClass, removeClass, storeFundingAgreementInfoIdChange); } function storePaymentInfoIdChange() { - paymentInfoIds = []; - let items = Array.from($('.payment-info-list').children()); - items.forEach((item) => { - paymentInfoIds.push(item.dataset.worksheetId); - }); + paymentInfoIds = getMultiTargetIds('.payment-info-list'); $('#PaymentInfoSlotIds').val(paymentInfoIds.join(';')); } function storeFundingAgreementInfoIdChange() { - fundingAgreementInfoIds = []; - let items = Array.from($('.funding-agreement-info-list').children()); - items.forEach((item) => { - fundingAgreementInfoIds.push(item.dataset.worksheetId); - }); + fundingAgreementInfoIds = getMultiTargetIds('.funding-agreement-info-list'); $('#FundingAgreementInfoSlotIds').val(fundingAgreementInfoIds.join(';')); } From 9de552fdb94ca33b25485b74913fa02f94a9007c Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 Aug 2025 20:59:25 -0700 Subject: [PATCH 07/47] AB#28858: Fit components into the height --- .../Pages/ApplicationForms/LinkWorksheetsModal.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.css index d4a3e9b5d..c0b2d3ccd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/LinkWorksheetsModal.css @@ -26,6 +26,8 @@ flex: 0.5; padding: 5px; border-radius: 5px; + height: 100%; + overflow-y: auto; } .single-slot { From 08809a8d71cbf9a48b85bd2caa62676356f02d6c Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Wed, 6 Aug 2025 12:52:54 -0700 Subject: [PATCH 08/47] AB#29719 initial commit for electoral district backfill endpoint --- .../Locality/IElectoralDistrictService.cs | 4 +- .../Intakes/Events/ApplicationProcessEvent.cs | 3 + .../DetermineElectoralDistrictHandler.cs | 11 +- .../Handlers/GenerateReportDataHandler.cs | 8 +- ...etrofillElectoralDistrictsBackgroundJob.cs | 103 ++++++++++++++++++ ...fillElectoralDistrictsBackgroundJobArgs.cs | 9 ++ .../Locality/ElectoralDistrictAppService.cs | 18 ++- 7 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJobArgs.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Locality/IElectoralDistrictService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Locality/IElectoralDistrictService.cs index 57977121c..4ce380569 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Locality/IElectoralDistrictService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Locality/IElectoralDistrictService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Services; @@ -7,5 +8,6 @@ namespace Unity.GrantManager.Locality; public interface IElectoralDistrictService : IApplicationService { Task> GetListAsync(); + Task RetroFillElectoralDistricts(Guid tenantId); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs index 4eff7732c..dee5886fa 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs @@ -8,5 +8,8 @@ public class ApplicationProcessEvent public ApplicationFormVersion? FormVersion { get; internal set; } public ApplicationFormSubmission? ApplicationFormSubmission { get; internal set; } public dynamic? RawSubmission { get; internal set; } + + // As this expands, turn this into a flags enum for control over which event handlers to run + public bool? OnlyLocationRetrofill { get; set; } = false; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs index 487a6bac1..e4192d987 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs @@ -42,6 +42,13 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } + if (!string.IsNullOrEmpty(eventData.Application.Applicant.ElectoralDistrict)) + { + logger.LogInformation("Electoral district already set to '{ExistingElectoralDistrict}' for application {ApplicationId}.", + eventData.Application.Applicant.ElectoralDistrict, eventData.Application.Id); + return; + } + // Use local variable to avoid modifying the entity property var addressType = eventData.Application.ApplicationForm.ElectoralDistrictAddressType ?? GrantApplications.AddressType.PhysicalAddress; logger.LogInformation("Using electoral district address type: {AddressType} for electoral determination", addressType); @@ -50,7 +57,7 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) if (applicantAddresses == null || applicantAddresses.Count == 0) { - logger.LogWarning("Applicant addresses are null or empty in DetermineElectoralDistrictHandler for application {ApplicationId}.", + logger.LogWarning("Applicant addresses are null or empty in DetermineElectoralDistrictHandler for application {ApplicationId}.", eventData.Application.Id); return; } @@ -81,6 +88,8 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) if (electoralDistrict.Name != null) { eventData.Application.Applicant.SetElectoralDistrict(electoralDistrict.Name); + logger.LogInformation("Electoral district '{ElectoralDistrict}' determined for address: {Address}", + electoralDistrict.Name, address); } else { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs index 189441218..02e6e303f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs @@ -10,7 +10,7 @@ namespace Unity.GrantManager.Intakes.Handlers { public class GenerateReportDataHandler(IReportingDataGenerator reportingDataGenerator, - ILogger logger, + ILogger logger, IFeatureChecker featureChecker) : ILocalEventHandler, ITransientDependency { /// @@ -26,6 +26,12 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } + if (eventData.OnlyLocationRetrofill == true) + { + logger.LogInformation("Skip report data generator handler."); + return; + } + if (await featureChecker.IsEnabledAsync(FeatureConsts.Reporting)) { logger.LogInformation("Generating report data for application {ApplicationId}.", eventData.Application?.Id); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs new file mode 100644 index 000000000..577831656 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs @@ -0,0 +1,103 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Intakes.Events; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; + +namespace Unity.GrantManager.Locality.BackgroundJobs +{ + public class RetrofillElectoralDistrictsBackgroundJob( + IApplicationRepository applicationRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationFormVersionRepository applicationFormVersionRepository, + ILocalEventBus localEventBus, + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + ILogger logger) : AsyncBackgroundJob, ITransientDependency + { + private const string LogPrefix = "[ElectoralRetroFill]"; + + public override async Task ExecuteAsync(RetrofillElectoralDistrictsBackgroundJobArgs args) + { + LogPrefixedInfo($"Executing electoral district retrofill for {args.TenantId}"); + + using (currentTenant.Change(args.TenantId)) + { + try + { + // Read all the applications for the tenant, get a list of their Id's + var applicationIds = (await applicationRepository + .GetListAsync()) + .Select(s => s.Id); + + // Foreach one we read again individually, and commit indivudally and log per record + foreach (var applicationId in applicationIds) + { + try + { + using var unitOfWork = unitOfWorkManager.Begin(true); + + LogPrefixedInfo($"Processing applicationId {applicationId}"); + + var application = await (await applicationRepository.GetQueryableAsync()) + .Include(s => s.Applicant) + .ThenInclude(s => s.ApplicantAddresses) + .Include(s => s.ApplicationForm) + .FirstOrDefaultAsync(s => s.Id == applicationId); + + var submission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); + var formVersionId = submission?.ApplicationFormVersionId; + + if (formVersionId == null) + { + LogPrefixedInfo($"No form version found for applicationId {applicationId}, skipping retrofill."); + continue; + } + + var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); + + await localEventBus.PublishAsync(new ApplicationProcessEvent + { + Application = application, + FormVersion = formVersion, + ApplicationFormSubmission = null, + RawSubmission = null, + OnlyLocationRetrofill = true + }); + + await unitOfWork.CompleteAsync(); + + // To avoid and rate limiting issues with any external services, we add a small delay + await Task.Delay(500); + } + catch (Exception ex) + { + LogPrefixedError(ex, $"Error executing electoral district retrofill for applicationId: {applicationId}"); + } + } + } + catch (Exception ex) + { + LogPrefixedError(ex, "Error executing electoral district retrofill for tenantId: {args.TenantId}"); + } + } + } + + private void LogPrefixedInfo(string message) + { + logger.LogInformation("{Prefix} {message}", LogPrefix, message); + } + + private void LogPrefixedError(Exception ex, string message) + { + logger.LogError(ex, "{Prefix} {message}", LogPrefix, message); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJobArgs.cs new file mode 100644 index 000000000..290af8743 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJobArgs.cs @@ -0,0 +1,9 @@ +using System; + +namespace Unity.GrantManager.Locality.BackgroundJobs +{ + public class RetrofillElectoralDistrictsBackgroundJobArgs + { + public Guid? TenantId { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/ElectoralDistrictAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/ElectoralDistrictAppService.cs index 8da6f0802..b4ca4b29b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/ElectoralDistrictAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/ElectoralDistrictAppService.cs @@ -1,11 +1,14 @@ -using System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Caching.Distributed; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Caching.Distributed; +using Unity.GrantManager.Locality.BackgroundJobs; using Unity.GrantManager.Settings; +using Unity.Modules.Shared.Permissions; using Volo.Abp.Application.Services; +using Volo.Abp.BackgroundJobs; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; @@ -17,7 +20,8 @@ namespace Unity.GrantManager.Locality [Dependency(ReplaceServices = true)] [ExposeServices(typeof(ElectoralDistrictAppService), typeof(IElectoralDistrictService))] public class ElectoralDistrictAppService(IElectoralDistrictRepository electoralDistrictRepository, - IDistributedCache cache) : ApplicationService, IElectoralDistrictService + IDistributedCache cache, + IBackgroundJobManager backgroundJobManager) : ApplicationService, IElectoralDistrictService { public virtual async Task> GetListAsync() { @@ -32,6 +36,12 @@ public virtual async Task> GetListAsync() return electoralDistrictsCache?.ElectoralDistricts ?? []; } + + [Authorize(IdentityConsts.ITAdminPolicyName)] + public virtual async Task RetroFillElectoralDistricts(Guid tenantId) + { + await backgroundJobManager.EnqueueAsync(new RetrofillElectoralDistrictsBackgroundJobArgs() { TenantId = tenantId }); + } protected virtual async Task GetElectoralDistrictsAsync() { From 9049fcf0ae7360ad61f08961419d438d9b30f144 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 6 Aug 2025 21:42:25 -0700 Subject: [PATCH 09/47] AB#28858: Allow multiple worksheets per tab AB#28858: Allow multiple worksheets per tab --- .../IWorksheetInstanceAppService.cs | 2 +- .../Worksheets/CustomDataFieldDto.cs | 4 +- .../Worksheets/IWorksheetAppService.cs | 2 +- .../Domain/Services/WorksheetsManager.cs | 2 +- .../IWorksheetInstanceRepository.cs | 1 - .../Domain/Worksheets/IWorksheetRepository.cs | 2 +- .../WorksheetInstanceRepository.cs | 13 - .../Repositories/WorksheetRepository.cs | 5 +- .../Worksheets/WorksheetAppService.cs | 8 +- .../Worksheets/WorksheetInstanceAppService.cs | 7 +- .../BCAddressWidget/BCAddressWidget.cs | 6 +- .../CheckboxGroupWidget.cs | 5 +- .../CheckboxGroupWidget/Default.cshtml | 19 +- .../CheckboxWidget/CheckboxWidget.cs | 5 +- .../Components/CheckboxWidget/Default.cshtml | 15 +- .../CurrencyWidget/CurrencyWidget.cs | 5 +- .../Components/CurrencyWidget/Default.cshtml | 11 +- .../Components/DateWidget/DateWidget.cs | 5 +- .../Components/DateWidget/Default.cshtml | 9 +- .../Components/RadioWidget/Default.cshtml | 18 +- .../Components/RadioWidget/RadioWidget.cs | 5 +- .../SelectListWidget/Default.cshtml | 9 +- .../SelectListWidget/SelectListWidget.cs | 5 +- .../Components/TextAreaWidget/Default.cshtml | 9 +- .../TextAreaWidget/TextAreaWidget.cs | 5 +- .../WorksheetInstanceWidget/Default.cshtml | 275 +++++++++++------ .../ViewModels/WorksheetViewModel.cs | 3 + .../WorksheetInstanceWidget.cs | 97 ++++-- .../Components/WorksheetViewModelBase.cs | 4 +- .../Components/YesNoWidget/Default.cshtml | 9 +- .../Components/YesNoWidget/YesNoWidget.cs | 5 +- .../Worksheets/WorksheetAppServiceTests.cs | 12 +- .../WorksheetInstanceAppServiceTests.cs | 28 -- .../PaymentInfo/PaymentInfoAppService.cs | 51 +++- .../Shared/Components/PaymentInfo/Default.js | 14 +- .../ApplicationApplicantAppService.cs | 280 ++++++++++-------- .../GrantApplicationAppService.cs | 131 +++++++- .../Components/ApplicantInfo/Default.js | 173 ++++++----- .../Components/AssessmentResults/Default.js | 14 +- .../FundingAgreementInfo/Default.js | 14 +- .../Shared/Components/ProjectInfo/Default.js | 15 +- 41 files changed, 873 insertions(+), 429 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/WorksheetInstances/IWorksheetInstanceAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/WorksheetInstances/IWorksheetInstanceAppService.cs index a7c0851eb..c19a67de9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/WorksheetInstances/IWorksheetInstanceAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/WorksheetInstances/IWorksheetInstanceAppService.cs @@ -6,7 +6,7 @@ namespace Unity.Flex.WorksheetInstances { public interface IWorksheetInstanceAppService : IApplicationService { - Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, Guid? worksheetId, string uiAnchor); + Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, Guid worksheetId, string uiAnchor); Task CreateAsync(CreateWorksheetInstanceDto dto); Task UpdateAsync(PersistWorksheetIntanceValuesDto dto); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/CustomDataFieldDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/CustomDataFieldDto.cs index 9d018efe8..69b6e704f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/CustomDataFieldDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/CustomDataFieldDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Unity.Flex.Worksheets { @@ -6,6 +7,7 @@ public class CustomDataFieldDto { public dynamic? CustomFields { get; set; } public Guid CorrelationId { get; set; } - public Guid WorksheetId { get; set; } + public Guid WorksheetId { get; set; } // Keep for backward compatibility + public List WorksheetIds { get; set; } = new List(); // NEW - multiple worksheets support } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs index 35d89d837..e36f44c45 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs @@ -10,7 +10,7 @@ public interface IWorksheetAppService : IApplicationService Task GetAsync(Guid id); Task> GetListAsync(); Task> GetListByCorrelationAsync(Guid correlationId, string correlationProvider); - Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor); + Task> GetListByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor); Task CreateAsync(CreateWorksheetDto dto); Task CreateSectionAsync(Guid id, CreateSectionDto dto); Task EditAsync(Guid id, EditWorksheetDto dto); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs index a367feaa7..320d6cdd9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Services/WorksheetsManager.cs @@ -42,7 +42,7 @@ public async Task PersistWorksheetData(PersistWorksheetIntanceValuesEto eventDat Worksheet? worksheet; if (string.IsNullOrEmpty(eventData.FormDataName)) { - worksheet = await worksheetRepository.GetByCorrelationAnchorAsync(eventData.SheetCorrelationId, eventData.SheetCorrelationProvider, eventData.UiAnchor, true); + worksheet = await worksheetRepository.GetAsync(eventData.WorksheetId, true); } else { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs index 54d1767d5..b60d3f98c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs @@ -8,7 +8,6 @@ namespace Unity.Flex.Domain.WorksheetInstances public interface IWorksheetInstanceRepository : IBasicRepository { Task GetByCorrelationAnchorWorksheetAsync(Guid correlationId, string correlationProvider, Guid worksheetId, string uiAnchor, bool includeDetails); - Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor, bool includeDetails); Task> GetByWorksheetCorrelationAsync(Guid worksheetId, string uiAnchor, Guid worksheetCorrelationId, string worksheetCorrelationProvider); Task GetWithValuesAsync(Guid worksheetInstanceId); Task ExistsAsync(Guid worksheetId, Guid instanceCorrelationId, string instanceCorrelationProvider, Guid sheetCorrelationId, string sheetCorrelationProvider, string? uiAnchor); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/IWorksheetRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/IWorksheetRepository.cs index 31e39276c..992ba2942 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/IWorksheetRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/IWorksheetRepository.cs @@ -9,7 +9,7 @@ public interface IWorksheetRepository : IBasicRepository { Task GetAsync(Guid id, bool includeDetails = true); Task> GetListAsync(bool includeDetails = false); - Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor, bool includeDetails = false); + Task> GetListByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor, bool includeDetails = false); Task GetByCorrelationByNameAsync(Guid correlationId, string correlationProvider, string name, bool includeDetails = false); Task GetByNameAsync(string name, bool includeDetails = false); Task GetBySectionAsync(Guid id, bool includeDetails = false); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs index e050aa414..ee92456a1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs @@ -26,19 +26,6 @@ public class WorksheetInstanceRepository(IDbContextProvider dbCon && s.UiAnchor == uiAnchor); } - public async Task GetByCorrelationAnchorAsync(Guid correlationId, - string correlationProvider, - string uiAnchor, - bool includeDetails) - { - var dbSet = await GetDbSetAsync(); - - return await dbSet.IncludeDetails(includeDetails) - .FirstOrDefaultAsync(s => s.CorrelationId == correlationId - && s.CorrelationProvider == correlationProvider - && s.UiAnchor == uiAnchor); - } - public async Task> GetByWorksheetCorrelationAsync(Guid worksheetId, string uiAnchor, Guid worksheetCorrelationId, diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetRepository.cs index a7fd6bcd0..cb1add5b5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetRepository.cs @@ -11,13 +11,14 @@ namespace Unity.Flex.EntityFrameworkCore.Repositories { public class WorksheetRepository(IDbContextProvider dbContextProvider) : EfCoreRepository(dbContextProvider), IWorksheetRepository { - public async Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor, bool includeDetails = false) + public async Task> GetListByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor, bool includeDetails = false) { var dbSet = await GetDbSetAsync(); return await dbSet .IncludeDetails(includeDetails) - .FirstOrDefaultAsync(s => s.Links.Any(s => s.CorrelationId == correlationId && s.CorrelationProvider == correlationProvider && s.UiAnchor == uiAnchor)); + .Where(s => s.Links.Any(l => l.CorrelationId == correlationId && l.CorrelationProvider == correlationProvider && l.UiAnchor == uiAnchor)) + .ToListAsync(); } public async Task GetByCorrelationByNameAsync(Guid correlationId, string correlationProvider, string name, bool includeDetails = false) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs index 2bb0e08ca..47114d2e2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs @@ -33,13 +33,11 @@ public virtual async Task> GetListByCorrelationAsync(Guid cor return ObjectMapper.Map, List>(worksheets); } - public virtual async Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor) + public virtual async Task> GetListByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, string uiAnchor) { - var worksheet = await worksheetRepository.GetByCorrelationAnchorAsync(correlationId, correlationProvider, uiAnchor, true); + var worksheets = await worksheetRepository.GetListByCorrelationAnchorAsync(correlationId, correlationProvider, uiAnchor, true); - if (worksheet == null) return null; - - return ObjectMapper.Map(worksheet); + return ObjectMapper.Map, List>(worksheets); } public virtual async Task CreateAsync(CreateWorksheetDto dto) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetInstanceAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetInstanceAppService.cs index bdad72b10..da2c963a1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetInstanceAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetInstanceAppService.cs @@ -9,12 +9,9 @@ namespace Unity.Flex.WorksheetInstances [Authorize] public class WorksheetInstanceAppService(IWorksheetInstanceRepository worksheetInstanceRepository, WorksheetsManager worksheetsManager) : FlexAppService, IWorksheetInstanceAppService { - public virtual async Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, Guid? worksheetId, string uiAnchor) + public virtual async Task GetByCorrelationAnchorAsync(Guid correlationId, string correlationProvider, Guid worksheetId, string uiAnchor) { - if (worksheetId == null) - return ObjectMapper.Map(await worksheetInstanceRepository.GetByCorrelationAnchorAsync(correlationId, correlationProvider, uiAnchor, true)); - else - return ObjectMapper.Map(await worksheetInstanceRepository.GetByCorrelationAnchorWorksheetAsync(correlationId, correlationProvider, worksheetId.Value, uiAnchor, true)); + return ObjectMapper.Map(await worksheetInstanceRepository.GetByCorrelationAnchorWorksheetAsync(correlationId, correlationProvider, worksheetId, uiAnchor, true)); } public virtual async Task CreateAsync(CreateWorksheetInstanceDto dto) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/BCAddressWidget/BCAddressWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/BCAddressWidget/BCAddressWidget.cs index 1b699cf49..e50d6e692 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/BCAddressWidget/BCAddressWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/BCAddressWidget/BCAddressWidget.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; @@ -16,12 +17,13 @@ namespace Unity.Flex.Web.Views.Shared.Components.BCAddressWidget AutoInitialize = true)] public class BCAddressWidget : AbpViewComponent { - public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName) + public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName, Guid? worksheetId = null) { return View(await Task.FromResult(new BCAddressViewModel() { Field = fieldModel, - Name = modelName + Name = modelName, + WorksheetId = worksheetId })); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/CheckboxGroupWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/CheckboxGroupWidget.cs index 9545bad9a..7520b626e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/CheckboxGroupWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/CheckboxGroupWidget.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; @@ -16,9 +17,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.CheckboxGroupWidget AutoInitialize = true)] public class CheckboxGroupWidget : AbpViewComponent { - public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName) + public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName, Guid? worksheetId = null) { - return View(await Task.FromResult(new CheckboxGroupViewModel() { Field = fieldModel, Name = modelName })); + return View(await Task.FromResult(new CheckboxGroupViewModel() { Field = fieldModel, Name = modelName, WorksheetId = worksheetId })); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/Default.cshtml index ac8cf6b0d..17c69fea7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxGroupWidget/Default.cshtml @@ -7,29 +7,36 @@ @if (Model.Field != null) { + + var fieldBaseName = Model.WorksheetId.HasValue + ? $"{Model.Field?.Name}.{Model.Name}.{Model.Field?.Id}.{Model.WorksheetId}" + : $"{Model.Field?.Name}.{Model.Name}.{Model.Field?.Id}"; +
@{ var checkedValues = Model.Field?.CurrentValue?.GetCheckedOptions() ?? []; foreach (var option in ((CheckboxGroupDefinition?)Model.Field?.Definition?.ConvertDefinition(Model.Field.Type))?.Options ?? []) - { + { + var optionId = $"{fieldBaseName}.{option.Key}"; +
@if (checkedValues.Contains(option.Key)) { } else { } - +
} } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/CheckboxWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/CheckboxWidget.cs index 612fc4c0a..5fd729d58 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/CheckboxWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/CheckboxWidget.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; @@ -16,9 +17,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.CheckboxWidget AutoInitialize = true)] public class CheckboxWidget : AbpViewComponent { - public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName) + public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName, Guid? worksheetId = null) { - return View(await Task.FromResult(new CheckboxViewModel() { Field = fieldModel, Name = modelName })); + return View(await Task.FromResult(new CheckboxViewModel() { Field = fieldModel, Name = modelName, WorksheetId = worksheetId })); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/Default.cshtml index be8fa4388..fbc2b5f70 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CheckboxWidget/Default.cshtml @@ -6,22 +6,27 @@ @if (Model.Field != null) { + + var fieldId = Model.WorksheetId.HasValue + ? $"{Model.Field?.Name}.{Model.Name}.{Model.Field?.Id}.{Model.WorksheetId}" + : $"{Model.Field?.Name}.{Model.Name}.{Model.Field?.Id}"; +
@if ((bool)(Model.Field?.CurrentValue?.ConvertInputValueOrNull(Model.Field.Type) ?? false)) { } else { } - +
} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/CurrencyWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/CurrencyWidget.cs index 769346ac8..ae0e5d685 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/CurrencyWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/CurrencyWidget.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -16,9 +17,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.CurrencyWidget AutoInitialize = true)] public class CurrencyWidget : AbpViewComponent { - public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName) + public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName, Guid? worksheetId = null) { - return View(await Task.FromResult(new CurrencyViewModel() { Field = fieldModel, Name = modelName })); + return View(await Task.FromResult(new CurrencyViewModel() { Field = fieldModel, Name = modelName, WorksheetId = worksheetId })); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/Default.cshtml index 06c53a5b0..6b84c9ef8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/CurrencyWidget/Default.cshtml @@ -6,6 +6,11 @@ @if (Model.Field != null) { + + var fieldId = Model.WorksheetId.HasValue + ? $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}.{Model.WorksheetId}" + : $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}"; +
$ - +
} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/DateWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/DateWidget.cs index 8d1df78a3..09ab5165b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/DateWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/DateWidget.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; @@ -16,9 +17,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.DateWidget AutoInitialize = true)] public class DateWidget : AbpViewComponent { - public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName) + public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName, Guid? worksheetId = null) { - return View(await Task.FromResult(new DateViewModel() { Field = fieldModel, Name = modelName })); + return View(await Task.FromResult(new DateViewModel() { Field = fieldModel, Name = modelName, WorksheetId = worksheetId })); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/Default.cshtml index b9e5b1e66..5006e52bb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DateWidget/Default.cshtml @@ -6,14 +6,19 @@ @if (Model.Field != null) { + + var fieldId = Model.WorksheetId.HasValue + ? $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}.{Model.WorksheetId}" + : $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}"; +
} \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/Default.cshtml index e7eb992bc..59d3a1cca 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/Default.cshtml @@ -7,18 +7,26 @@ @if (Model.Field != null) { + + var fieldBaseName = Model.WorksheetId.HasValue + ? $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}.{Model.WorksheetId}" + : $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}"; +
@foreach (var option in ((RadioDefinition?)Model.Field?.Definition?.ConvertDefinition(Model.Field.Type))?.Options ?? []) { + + var optionId = $"{fieldBaseName}.{option.Value}"; +
@if (Model.Field != null) { @if ((Model.Field?.CurrentValue?.ConvertInputValueOrNull(Model.Field.Type))?.ToString() == option.Value) { @@ -26,12 +34,12 @@ else { } - + }
} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/RadioWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/RadioWidget.cs index fe0a67370..f5bdace73 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/RadioWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/RadioWidget/RadioWidget.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using System; using System.Threading.Tasks; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; @@ -16,9 +17,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.RadioWidget AutoInitialize = true)] public class RadioWidget : AbpViewComponent { - public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName) + public async Task InvokeAsync(WorksheetFieldViewModel? fieldModel, string modelName, Guid? worksheetId = null) { - return View(await Task.FromResult(new RadioViewModel() { Field = fieldModel, Name = modelName })); + return View(await Task.FromResult(new RadioViewModel() { Field = fieldModel, Name = modelName, WorksheetId = worksheetId })); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/SelectListWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/SelectListWidget/Default.cshtml index e3ae125b2..1c4a6512b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/SelectListWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/SelectListWidget/Default.cshtml @@ -7,9 +7,14 @@ @if (Model.Field != null) { + + var fieldId = Model.WorksheetId.HasValue + ? $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}.{Model.WorksheetId}" + : $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}"; + +
+ +
+ + + + + `; + + $('body').append(modalHtml); + const modal = new bootstrap.Modal(document.getElementById('createGroupModal')); + modal.show(); + + $('#saveNewGroup').on('click', function() { + const name = $('#groupName').val(); + const description = $('#groupDescription').val(); + + if (!name) { + abp.notify.error('Group name is required'); + return; + } + + const dto = { + name: name, + description: description, + type: 'Dynamic' + }; + + unity.notifications.emailGroups.emailGroups.create(dto).then(function(result) { + abp.notify.success('Email group created successfully'); + modal.hide(); + self.loadGroups(); + }).catch(function(error) { + console.error('Failed to create email group:', error); + abp.notify.error('Failed to create email group'); + }); + }); + + $('#createGroupModal').on('hidden.bs.modal', function() { + $(this).remove(); + }); + }, + + showEditGroupModal: function(group) { + const self = this; + const modalHtml = ` + + `; + + $('body').append(modalHtml); + const modal = new bootstrap.Modal(document.getElementById('editGroupModal')); + modal.show(); + + $('#updateGroup').on('click', function() { + const name = $('#editGroupName').val(); + const description = $('#editGroupDescription').val(); + + if (!name) { + abp.notify.error('Group name is required'); + return; + } + + const dto = { + id: group.id, + name: name, + description: description, + type: group.type + }; + + unity.notifications.emailGroups.emailGroups.update(dto).then(function(result) { + abp.notify.success('Email group updated successfully'); + modal.hide(); + self.loadGroups(); + }).catch(function(error) { + console.error('Failed to update email group:', error); + abp.notify.error('Failed to update email group'); + }); + }); + + $('#editGroupModal').on('hidden.bs.modal', function() { + $(this).remove(); + }); + }, + + deleteGroup: function(groupId) { + const self = this; + abp.message.confirm( + 'Are you sure you want to delete this email group? This action cannot be undone.', + 'Delete Email Group', + function(isConfirmed) { + if (isConfirmed) { + unity.notifications.emailGroups.emailGroups.delete(groupId).then(function() { + abp.notify.success('Email group deleted successfully'); + self.loadGroups(); + }).catch(function(error) { + console.error('Failed to delete email group:', error); + abp.notify.error('Failed to delete email group'); + }); + } + } + ); + }, + + showManageUsersModal: function(group) { + const self = this; + const modalHtml = ` + + `; + + $('body').append(modalHtml); + const modal = new bootstrap.Modal(document.getElementById('manageUsersModal')); + modal.show(); + + this.loadGroupUsers(group.id); + + $('#searchUsersBtn').on('click', function() { + const searchTerm = $('#userSearchInput').val(); + if (searchTerm) { + self.searchUsers(searchTerm, group.id); + } + }); + + $('#userSearchInput').on('keypress', function(e) { + if (e.which === 13) { + $('#searchUsersBtn').click(); + } + }); + + // Use modal-specific event handlers instead of document-wide + $('#manageUsersModal').on('click', '.add-user-btn', function() { + const userId = $(this).data('user-id'); + self.addUserToGroup(userId, group.id); + }); + + $('#manageUsersModal').on('click', '.remove-user-btn', function() { + const groupUserId = $(this).data('group-user-id'); + self.removeUserFromGroup(groupUserId, group.id); + }); + + $('#manageUsersModal').on('hidden.bs.modal', function() { + $(this).remove(); + }); + }, + + loadGroupUsers: function(groupId) { + const self = this; + + console.log('Loading users for group:', groupId); + + // Clear the container first to avoid duplicates + $('#currentGroupUsers').html('

Loading users...

'); + + // Always fetch fresh data from backend - no caching + unity.notifications.emailGroups.emailGroupUsers.getEmailGroupUsersByGroupId(groupId).then(function(groupUsers) { + console.log('Fresh group users loaded from backend:', groupUsers); + + if (!groupUsers || groupUsers.length === 0) { + $('#currentGroupUsers').html('

No users in this group

'); + return; + } + + // Fetch user details for each group user + const userPromises = groupUsers.map(gu => + $.ajax({ + url: `/api/identity/users/${gu.userId}`, + method: 'GET' + }).then(function(user) { + return { + id: gu.id, + userId: gu.userId, + groupId: gu.groupId, + userName: user.userName || user.name, + email: user.email + }; + }).catch(function() { + return { + id: gu.id, + userId: gu.userId, + groupId: gu.groupId, + userName: 'Unknown User', + email: '' + }; + }) + ); + + Promise.all(userPromises).then(function(usersWithDetails) { + self.renderGroupUsers(usersWithDetails, groupId); + }); + }).catch(function(error) { + console.error('Failed to load group users:', error); + $('#currentGroupUsers').html('

Failed to load group users.

'); + }); + }, + + renderGroupUsers: function(users, groupId) { + const container = $('#currentGroupUsers'); + + // Always clear the container first + container.empty(); + + if (!users || users.length === 0) { + container.html('

No users in this group

'); + return; + } + + console.log('Rendering group users:', users); + + // Create unique list based on userId to prevent duplicates + const uniqueUsers = []; + const seenUserIds = new Set(); + + users.forEach(user => { + if (!seenUserIds.has(user.userId)) { + seenUserIds.add(user.userId); + uniqueUsers.push(user); + } + }); + + const usersHtml = uniqueUsers.map(user => ` +
+
+ ${user.userName || 'Unknown User'} + ${user.email || ''} +
+ +
+ `).join(''); + + container.html(usersHtml); + }, + + renderSearchResults: function(users, groupId, resultsContainer) { + const self = this; + + if (!Array.isArray(users)) { + users = []; + } + + console.log('Rendering search results for users:', users); + + // Get current group users to filter them out + unity.notifications.emailGroups.emailGroupUsers.getEmailGroupUsersByGroupId(groupId).then(function(groupUsers) { + const groupUserIds = (groupUsers || []).map(gu => gu.userId); + const availableUsers = users.filter(user => !groupUserIds.includes(user.id)); + + if (availableUsers.length === 0) { + resultsContainer.html('

No users found or all matching users are already in the group

'); + return; + } + + const usersHtml = availableUsers.map(user => ` +
+
+ ${user.userName || user.name || user.displayName || 'Unknown'} + ${user.email || user.emailAddress || ''} +
+ +
+ `).join(''); + + resultsContainer.html(usersHtml); + }).catch(function() { + // If we can't get group users, show all search results + if (users.length === 0) { + resultsContainer.html('

No users found

'); + return; + } + + const usersHtml = users.map(user => ` +
+
+ ${user.userName || user.name || user.displayName || 'Unknown'} + ${user.email || user.emailAddress || ''} +
+ +
+ `).join(''); + + resultsContainer.html(usersHtml); + }); + }, + + searchUsers: function(searchTerm, groupId) { + const self = this; + const resultsContainer = $('#userSearchResults'); + + console.log('SearchUsers called with:', { searchTerm, groupId }); + resultsContainer.html('

Searching...

'); + + // Try direct AJAX call with debugging + const requestData = { + filter: searchTerm, + maxResultCount: 10, + skipCount: 0 + }; + + console.log('Making request to /api/identity/users with data:', requestData); + + $.ajax({ + url: '/api/identity/users', + method: 'GET', + data: requestData, + headers: { + 'RequestVerificationToken': abp.security.antiForgery.getToken() + }, + success: function(response) { + console.log('User search response:', response); + const users = response.items || response || []; + self.renderSearchResults(users, groupId, resultsContainer); + }, + error: function(xhr, status, error) { + console.error('User search failed:', { + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText, + error: error + }); + } + }); + }, + + addUserToGroup: function(userId, groupId) { + const self = this; + const dto = { + userId: userId, + groupId: groupId + }; + + unity.notifications.emailGroups.emailGroupUsers.insert(dto).then(function() { + abp.notify.success('User added to group'); + self.loadGroupUsers(groupId); + $('#userSearchResults').empty(); + $('#userSearchInput').val(''); + }).catch(function(error) { + console.error('Failed to add user to group:', error); + abp.notify.error('Failed to add user to group'); + }); + }, + + removeUserFromGroup: function(groupUserId, groupId) { + const self = this; + + unity.notifications.emailGroups.emailGroupUsers.deleteUser(groupUserId).then(function() { + abp.notify.success('User removed from group'); + self.loadGroupUsers(groupId); + }).catch(function(error) { + console.error('Failed to remove user from group:', error); + abp.notify.error('Failed to remove user from group'); + }); + } + }; + + // Initialize only when the Internal Email Group tab is shown + $('#nav-internal-email-group-tab').on('shown.bs.tab', function() { + emailGroupsManager.init(); + }); + }); +})(jQuery); \ No newline at end of file From ebc1185378ba0bdf3dac0fa86149eb8c2942006e Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:21:51 -0700 Subject: [PATCH 20/47] AB#29617 - Add Authorization to Dashboard Razor Page --- .../Pages/Dashboard/Index.cshtml.cs | 283 +++++++++--------- 1 file changed, 142 insertions(+), 141 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.cshtml.cs index 9155568cd..d3a725aca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.cshtml.cs @@ -1,5 +1,5 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using System; @@ -12,166 +12,167 @@ using Unity.GrantManager.GrantApplications; using Unity.GrantManager.Identity; using Unity.GrantManager.Intakes; +using Unity.GrantManager.Permissions; using Volo.Abp.Domain.Repositories; -namespace Unity.GrantManager.Web.Pages.Dashboard +namespace Unity.GrantManager.Web.Pages.Dashboard; + +[Authorize(GrantApplicationPermissions.Dashboard.Default)] +public class IndexModel : GrantManagerPageModel { - public class IndexModel : PageModel + private readonly IIntakeRepository _intakeRepository; + private readonly IApplicationFormRepository _applicationFormRepository; + private readonly IApplicationStatusRepository _applicationStatusRepository; + private readonly IApplicationTagsRepository _applicationTagsRepository; + private readonly IApplicationAssignmentRepository _applicationAssignmentRepository; + private readonly IPersonRepository _personRepository; + + public List IntakeOptionsList { get; set; } = []; + public List CategoryOptionsList { get; set; } = []; + public List StatusOptionsList { get; set; } = []; + public List SubStatusActionList { get; set; } = []; + public List TagsOptionsList { get; set; } = []; + public List AssigneesOptionList { get; set; } = []; + + [BindProperty] + [Display(Name = "")] + public Guid[] IntakeIds { get; set; } = []; + [BindProperty] + [Display(Name = "")] + public string[]? CategoryNames { get; set; } + [BindProperty] + [Display(Name = "")] + public string[]? Statuses { get; set; } + [BindProperty] + [Display(Name = "")] + public string[]? SubStatuses { get; set; } + + [BindProperty] + [Display(Name = "")] + public DateTime? SubmissionDateFrom { get; set; } + [BindProperty] + [Display(Name = "")] + public DateTime? SubmissionDateTo { get; set; } + [BindProperty] + [Display(Name = "")] + public string[]? Tags { get; set; } + [BindProperty] + [Display(Name = "")] + public string[]? Assignees { get; set; } + + public List DashboardIntakes { get; set; } = []; + + public IndexModel(IIntakeRepository intakeRepository, + IApplicationFormRepository applicationFormRepository, + IApplicationStatusRepository applicationStatusRepository, + IApplicationTagsRepository applicationTagsRepository, + IApplicationAssignmentRepository applicationAssignmentRepository, + IPersonRepository personRepository) { - private readonly IIntakeRepository _intakeRepository; - private readonly IApplicationFormRepository _applicationFormRepository; - private readonly IApplicationStatusRepository _applicationStatusRepository; - private readonly IApplicationTagsRepository _applicationTagsRepository; - private readonly IApplicationAssignmentRepository _applicationAssignmentRepository; - private readonly IPersonRepository _personRepository; - - public List IntakeOptionsList { get; set; } = []; - public List CategoryOptionsList { get; set; } = []; - public List StatusOptionsList { get; set; } = []; - public List SubStatusActionList { get; set; } = []; - public List TagsOptionsList { get; set; } = []; - public List AssigneesOptionList { get; set; } = []; - - [BindProperty] - [Display(Name = "")] - public Guid[] IntakeIds { get; set; } = []; - [BindProperty] - [Display(Name = "")] - public string[]? CategoryNames { get; set; } - [BindProperty] - [Display(Name = "")] - public string[]? Statuses { get; set; } - [BindProperty] - [Display(Name = "")] - public string[]? SubStatuses { get; set; } - - [BindProperty] - [Display(Name = "")] - public DateTime? SubmissionDateFrom { get; set; } - [BindProperty] - [Display(Name = "")] - public DateTime? SubmissionDateTo { get; set; } - [BindProperty] - [Display(Name = "")] - public string[]? Tags { get; set; } - [BindProperty] - [Display(Name = "")] - public string[]? Assignees { get; set; } - - public List DashboardIntakes { get; set; } = []; - - public IndexModel(IIntakeRepository intakeRepository, - IApplicationFormRepository applicationFormRepository, - IApplicationStatusRepository applicationStatusRepository, - IApplicationTagsRepository applicationTagsRepository, - IApplicationAssignmentRepository applicationAssignmentRepository, - IPersonRepository personRepository) - { - _intakeRepository = intakeRepository; - _applicationFormRepository = applicationFormRepository; - _applicationStatusRepository = applicationStatusRepository; - _applicationTagsRepository = applicationTagsRepository; - _applicationAssignmentRepository = applicationAssignmentRepository; - _personRepository = personRepository; - } + _intakeRepository = intakeRepository; + _applicationFormRepository = applicationFormRepository; + _applicationStatusRepository = applicationStatusRepository; + _applicationTagsRepository = applicationTagsRepository; + _applicationAssignmentRepository = applicationAssignmentRepository; + _personRepository = personRepository; + } - public async Task OnGetAsync() + public async Task OnGetAsync() + { + using (_intakeRepository.DisableTracking()) + using (_applicationFormRepository.DisableTracking()) { - using (_intakeRepository.DisableTracking()) - using (_applicationFormRepository.DisableTracking()) + var intakesQ = from intakesq in await _intakeRepository.GetQueryableAsync() + join formsq in await _applicationFormRepository.GetQueryableAsync() on intakesq.Id equals formsq.IntakeId + select new IntakeQ + { + IntakeId = intakesq.Id, + IntakeCreationTime = intakesq.CreationTime, + IntakeName = intakesq.IntakeName, + Category = formsq.Category ?? DashboardConsts.EmptyValue + }; + + var intakeR = await intakesQ.ToListAsync(); + if (intakeR.Count == 0) return; + + IntakeOptionsList = intakeR.DistinctBy(s => s.IntakeId).Select(intake => new SelectListItem { Value = intake.IntakeId.ToString(), Text = intake.IntakeName }).ToList(); + var latestIntakeId = intakeR.OrderByDescending(intake => intake.IntakeCreationTime).FirstOrDefault()?.IntakeId; + IntakeIds = [latestIntakeId ?? Guid.Empty]; + + foreach (var intake in intakeR) { - var intakesQ = from intakesq in await _intakeRepository.GetQueryableAsync() - join formsq in await _applicationFormRepository.GetQueryableAsync() on intakesq.Id equals formsq.IntakeId - select new IntakeQ - { - IntakeId = intakesq.Id, - IntakeCreationTime = intakesq.CreationTime, - IntakeName = intakesq.IntakeName, - Category = formsq.Category ?? DashboardConsts.EmptyValue - }; - - var intakeR = await intakesQ.ToListAsync(); - if (intakeR.Count == 0) return; - - IntakeOptionsList = intakeR.DistinctBy(s => s.IntakeId).Select(intake => new SelectListItem { Value = intake.IntakeId.ToString(), Text = intake.IntakeName }).ToList(); - var latestIntakeId = intakeR.OrderByDescending(intake => intake.IntakeCreationTime).FirstOrDefault()?.IntakeId; - IntakeIds = [latestIntakeId ?? Guid.Empty]; - - foreach (var intake in intakeR) - { - List categoryList = [.. intakeR.Where(s => !string.IsNullOrWhiteSpace(s.Category) && s.IntakeId == intake.IntakeId) - .Select(s => s.Category) - .Distinct() - .OrderBy(c => c)]; - - DashboardIntakes.Add(new() - { - IntakeId = intake.IntakeId, - IntakeName = intake.IntakeName, - Categories = categoryList - }); - - if (intake.IntakeId == latestIntakeId) - { - CategoryOptionsList = categoryList.Select(category => new SelectListItem { Value = category, Text = category }).ToList(); - CategoryNames = [.. categoryList]; - } - } + List categoryList = [.. intakeR.Where(s => !string.IsNullOrWhiteSpace(s.Category) && s.IntakeId == intake.IntakeId) + .Select(s => s.Category) + .Distinct() + .OrderBy(c => c)]; - var statuses = await _applicationStatusRepository.GetListAsync(); - StatusOptionsList = statuses.Select(s => new SelectListItem { Value = s.StatusCode.ToString(), Text = s.InternalStatus }).ToList(); - Statuses = statuses.Select(s => s.StatusCode.ToString()).ToArray(); - SubStatusActionList = AssessmentResultsOptionsList.SubStatusActionList.Select(s => new SelectListItem { Value = s.Key, Text = s.Value }).ToList(); - SubStatusActionList.Add(new SelectListItem { Value = DashboardConsts.EmptyValue, Text = DashboardConsts.EmptyValue }); //for applications with no Sub-Status - SubStatuses = SubStatusActionList.Select(s => s.Value).ToArray(); + DashboardIntakes.Add(new() + { + IntakeId = intake.IntakeId, + IntakeName = intake.IntakeName, + Categories = categoryList + }); - await GetTagsFilter(); - await GetUsersFilter(); + if (intake.IntakeId == latestIntakeId) + { + CategoryOptionsList = categoryList.Select(category => new SelectListItem { Value = category, Text = category }).ToList(); + CategoryNames = [.. categoryList]; + } } - } - private async Task GetUsersFilter() - { - var userAssignments = await _applicationAssignmentRepository.GetQueryableAsync(); - var users = await _personRepository.GetQueryableAsync(); + var statuses = await _applicationStatusRepository.GetListAsync(); + StatusOptionsList = statuses.Select(s => new SelectListItem { Value = s.StatusCode.ToString(), Text = s.InternalStatus }).ToList(); + Statuses = statuses.Select(s => s.StatusCode.ToString()).ToArray(); + SubStatusActionList = AssessmentResultsOptionsList.SubStatusActionList.Select(s => new SelectListItem { Value = s.Key, Text = s.Value }).ToList(); + SubStatusActionList.Add(new SelectListItem { Value = DashboardConsts.EmptyValue, Text = DashboardConsts.EmptyValue }); //for applications with no Sub-Status + SubStatuses = SubStatusActionList.Select(s => s.Value).ToArray(); - var assignees = from userAssignment in userAssignments - join user in users on userAssignment.AssigneeId equals user.Id - select new { user.Id, user.FullName }; + await GetTagsFilter(); + await GetUsersFilter(); + } + } - var distinctOrderedUsers = assignees.Distinct().OrderBy(s => s.FullName); + private async Task GetUsersFilter() + { + var userAssignments = await _applicationAssignmentRepository.GetQueryableAsync(); + var users = await _personRepository.GetQueryableAsync(); - AssigneesOptionList = await distinctOrderedUsers.Select(user => new SelectListItem - { - Value = user.Id.ToString(), - Text = !string.IsNullOrEmpty(user.FullName) ? user.FullName : DashboardConsts.EmptyValue, - }).ToListAsync(); + var assignees = from userAssignment in userAssignments + join user in users on userAssignment.AssigneeId equals user.Id + select new { user.Id, user.FullName }; - AssigneesOptionList.Add(new SelectListItem { Value = string.Empty, Text = DashboardConsts.EmptyValue }); - Assignees = AssigneesOptionList.Select(s => s.Value).Distinct().ToArray(); - } + var distinctOrderedUsers = assignees.Distinct().OrderBy(s => s.FullName); - private async Task GetTagsFilter() + AssigneesOptionList = await distinctOrderedUsers.Select(user => new SelectListItem { - var tagResult = await _applicationTagsRepository.GetListAsync(true); - var tags = tagResult.SelectMany(tag => tag.Tag.Name.Split(',').Select(t => t.Trim())).Distinct(); + Value = user.Id.ToString(), + Text = !string.IsNullOrEmpty(user.FullName) ? user.FullName : DashboardConsts.EmptyValue, + }).ToListAsync(); - TagsOptionsList = tags.Select(tag => new SelectListItem - { - Value = tag, - Text = !string.IsNullOrEmpty(tag) ? tag : DashboardConsts.EmptyValue - }).ToList(); + AssigneesOptionList.Add(new SelectListItem { Value = string.Empty, Text = DashboardConsts.EmptyValue }); + Assignees = AssigneesOptionList.Select(s => s.Value).Distinct().ToArray(); + } - TagsOptionsList.Add(new SelectListItem { Value = string.Empty, Text = DashboardConsts.EmptyValue }); - Tags = TagsOptionsList.Select(tag => tag.Value).Distinct().ToArray(); - } + private async Task GetTagsFilter() + { + var tagResult = await _applicationTagsRepository.GetListAsync(true); + var tags = tagResult.SelectMany(tag => tag.Tag.Name.Split(',').Select(t => t.Trim())).Distinct(); - private sealed class IntakeQ + TagsOptionsList = tags.Select(tag => new SelectListItem { - public Guid IntakeId { get; internal set; } - public string? Category { get; internal set; } = null; - public DateTime IntakeCreationTime { get; internal set; } - public string IntakeName { get; internal set; } = string.Empty; - } + Value = tag, + Text = !string.IsNullOrEmpty(tag) ? tag : DashboardConsts.EmptyValue + }).ToList(); + + TagsOptionsList.Add(new SelectListItem { Value = string.Empty, Text = DashboardConsts.EmptyValue }); + Tags = TagsOptionsList.Select(tag => tag.Value).Distinct().ToArray(); + } + + private sealed class IntakeQ + { + public Guid IntakeId { get; internal set; } + public string? Category { get; internal set; } = null; + public DateTime IntakeCreationTime { get; internal set; } + public string IntakeName { get; internal set; } = string.Empty; } } From f90f89cbd3cd426ed7aa2f576460fe51b7ce97f0 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:22:43 -0700 Subject: [PATCH 21/47] AB#29617 - Switch Dashboard query parameters from GET URI to POST request body --- .../Dashboard/DashboardAppService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Dashboard/DashboardAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Dashboard/DashboardAppService.cs index ed12f9ca4..9ae027115 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Dashboard/DashboardAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Dashboard/DashboardAppService.cs @@ -10,6 +10,7 @@ using Unity.GrantManager.Intakes; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; +using Microsoft.AspNetCore.Mvc; namespace Unity.GrantManager.Dashboard; @@ -44,6 +45,7 @@ IIntakeRepository intakeRepository } [Authorize(GrantApplicationPermissions.Dashboard.EconomicRegionCount)] + [HttpPost] public virtual async Task> GetEconomicRegionCountAsync(DashboardParametersDto dashboardParams) { var parameters = PrepareParameters(dashboardParams); @@ -70,6 +72,7 @@ public virtual async Task> GetEconomicRegionCountAsyn } [Authorize(GrantApplicationPermissions.Dashboard.ApplicationStatusCount)] + [HttpPost] public virtual async Task> GetApplicationStatusCountAsync(DashboardParametersDto dashboardParams) { var parameters = PrepareParameters(dashboardParams); @@ -96,6 +99,7 @@ public virtual async Task> GetApplicationStatusCou } [Authorize(GrantApplicationPermissions.Dashboard.ApplicationTagsCount)] + [HttpPost] public virtual async Task> GetApplicationTagsCountAsync(DashboardParametersDto dashboardParams) { var parameters = PrepareParameters(dashboardParams); @@ -124,6 +128,7 @@ join tag in tagQueryable on baseQuery.Application.Id equals tag.ApplicationId } [Authorize(GrantApplicationPermissions.Dashboard.RequestedAmountPerSubsector)] + [HttpPost] public virtual async Task> GetRequestedAmountPerSubsectorAsync(DashboardParametersDto dashboardParams) { var parameters = PrepareParameters(dashboardParams); @@ -154,6 +159,7 @@ join applicant in await _applicantRepository.GetQueryableAsync() on baseQuery.Ap } [Authorize(GrantApplicationPermissions.Dashboard.ApplicationAssigneeCount)] + [HttpPost] public virtual async Task> GetApplicationAssigneeCountAsync(DashboardParametersDto dashboardParams) { var parameters = PrepareParameters(dashboardParams); @@ -186,6 +192,7 @@ where parameters.Assignees.Contains(baseQuery.AppAssignee.AssigneeId.ToString()) } [Authorize(GrantApplicationPermissions.Dashboard.RequestApprovedCount)] + [HttpPost] public virtual async Task> GetRequestApprovedCountAsync(DashboardParametersDto dashboardParams) { var parameters = PrepareParameters(dashboardParams); From 1f8539b6c5dcc2006b216b628bc6244e852f5802 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 12 Aug 2025 10:31:34 -0700 Subject: [PATCH 22/47] AB#29719 SQ and codeQL cleanup --- .../Intakes/Events/ApplicationProcessEvent.cs | 2 +- .../Handlers/DetermineElectoralDistrictHandler.cs | 14 +++++++------- .../RetrofillElectoralDistrictsBackgroundJob.cs | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs index dee5886fa..c5e218694 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Events/ApplicationProcessEvent.cs @@ -10,6 +10,6 @@ public class ApplicationProcessEvent public dynamic? RawSubmission { get; internal set; } // As this expands, turn this into a flags enum for control over which event handlers to run - public bool? OnlyLocationRetrofill { get; set; } = false; + public bool OnlyLocationRetrofill { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs index e4192d987..392216c7a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/DetermineElectoralDistrictHandler.cs @@ -28,6 +28,13 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } + if (!string.IsNullOrEmpty(eventData.Application.Applicant.ElectoralDistrict)) + { + logger.LogInformation("Electoral district already set to '{ExistingElectoralDistrict}' for application {ApplicationId}.", + eventData.Application.Applicant.ElectoralDistrict, eventData.Application.Id); + return; + } + if (eventData.FormVersion == null) { logger.LogWarning("Form version data is null in DetermineElectoralDistrictHandler."); @@ -42,13 +49,6 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } - if (!string.IsNullOrEmpty(eventData.Application.Applicant.ElectoralDistrict)) - { - logger.LogInformation("Electoral district already set to '{ExistingElectoralDistrict}' for application {ApplicationId}.", - eventData.Application.Applicant.ElectoralDistrict, eventData.Application.Id); - return; - } - // Use local variable to avoid modifying the entity property var addressType = eventData.Application.ApplicationForm.ElectoralDistrictAddressType ?? GrantApplications.AddressType.PhysicalAddress; logger.LogInformation("Using electoral district address type: {AddressType} for electoral determination", addressType); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs index 577831656..fb7c6dedf 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Locality/BackgroundJobs/RetrofillElectoralDistrictsBackgroundJob.cs @@ -74,7 +74,7 @@ await localEventBus.PublishAsync(new ApplicationProcessEvent await unitOfWork.CompleteAsync(); - // To avoid and rate limiting issues with any external services, we add a small delay + // To avoid any rate limiting issues with any external services, we add a small delay await Task.Delay(500); } catch (Exception ex) @@ -85,19 +85,19 @@ await localEventBus.PublishAsync(new ApplicationProcessEvent } catch (Exception ex) { - LogPrefixedError(ex, "Error executing electoral district retrofill for tenantId: {args.TenantId}"); + LogPrefixedError(ex, $"Error executing electoral district retrofill for tenantId: {args.TenantId}"); } } } private void LogPrefixedInfo(string message) { - logger.LogInformation("{Prefix} {message}", LogPrefix, message); + logger.LogInformation("{Prefix} {Message}", LogPrefix, message); } private void LogPrefixedError(Exception ex, string message) { - logger.LogError(ex, "{Prefix} {message}", LogPrefix, message); + logger.LogError(ex, "{Prefix} {Message}", LogPrefix, message); } } } From c5150aff28cfd47859957b773e89ecad394f9673 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 12 Aug 2025 11:01:21 -0700 Subject: [PATCH 23/47] AB#29719 last SQ cleanup --- .../Intakes/Handlers/GenerateReportDataHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs index 02e6e303f..406fac8f0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/Handlers/GenerateReportDataHandler.cs @@ -26,7 +26,7 @@ public async Task HandleEventAsync(ApplicationProcessEvent eventData) return; } - if (eventData.OnlyLocationRetrofill == true) + if (eventData.OnlyLocationRetrofill) { logger.LogInformation("Skip report data generator handler."); return; From 5e5c9f1d9500a9de5f1662fb1c3d2239c970f661 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:21:07 -0700 Subject: [PATCH 24/47] AB#29512 - History Service - Support rendering of deleted users --- .../History/HistoryAppService.cs | 136 +++++++++--------- 1 file changed, 71 insertions(+), 65 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs index bde5340f9..514d82b85 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/History/HistoryAppService.cs @@ -2,92 +2,98 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Volo.Abp; using Volo.Abp.Auditing; using Volo.Abp.AuditLogging; +using Volo.Abp.Data; using Volo.Abp.Domain.ChangeTracking; using Volo.Abp.Identity; -namespace Unity.GrantManager.History +namespace Unity.GrantManager.History; + +public class HistoryAppService( + IAuditLogRepository auditLogRepository, + IIdentityUserRepository identityUserRepository, + IDataFilter softDataFilter) : GrantManagerAppService, IHistoryAppService { - public class HistoryAppService(IAuditLogRepository auditLogRepository, IIdentityUserRepository identityUserRepository) : GrantManagerAppService, IHistoryAppService + [DisableEntityChangeTracking] + public async Task> GetHistoryList(string? entityId, + string filterPropertyName, + Dictionary? lookupDictionary) { - [DisableEntityChangeTracking] - public async Task> GetHistoryList(string? entityId, - string filterPropertyName, - Dictionary? lookupDictionary) - { - List historyList = []; - string? sorting = null; - int maxResultCount = 50; - int skipCount = 0; - DateTime? startTime = null; - DateTime? endTime = null; - bool includeDetails = true; - Guid? auditLogId = null; - EntityChangeType? changeType = null; - string? entityTypeFullName = null; - CancellationToken cancellationToken = default; + List historyList = []; + string? sorting = null; + int maxResultCount = 50; + int skipCount = 0; + DateTime? startTime = null; + DateTime? endTime = null; + bool includeDetails = true; + Guid? auditLogId = null; + EntityChangeType? changeType = null; + string? entityTypeFullName = null; + CancellationToken cancellationToken = default; - var entityChanges = await auditLogRepository.GetEntityChangeListAsync( - sorting, - maxResultCount, - skipCount, - auditLogId, - startTime, endTime, - changeType, - entityId, - entityTypeFullName, - includeDetails, - cancellationToken); + var entityChanges = await auditLogRepository.GetEntityChangeListAsync( + sorting, + maxResultCount, + skipCount, + auditLogId, + startTime, endTime, + changeType, + entityId, + entityTypeFullName, + includeDetails, + cancellationToken); - foreach (var entityChange in entityChanges) + foreach (var entityChange in entityChanges) + { + foreach (var propertyChange in entityChange.PropertyChanges) { - foreach (var propertyChange in entityChange.PropertyChanges) + if (propertyChange.PropertyName == filterPropertyName) { - if (propertyChange.PropertyName == filterPropertyName) + string origninalValue = CleanValue(propertyChange.OriginalValue); + string newValue = CleanValue(propertyChange.NewValue); + // Signal the kind of time so that tolocal knows how to convert it on the page + DateTime utcDateTime = DateTime.SpecifyKind(entityChange.ChangeTime, DateTimeKind.Utc); + HistoryDto historyDto = new() { - string origninalValue = CleanValue(propertyChange.OriginalValue); - string newValue = CleanValue(propertyChange.NewValue); - // Signal the kind of time so that tolocal knows how to convert it on the page - DateTime utcDateTime = DateTime.SpecifyKind(entityChange.ChangeTime, DateTimeKind.Utc); - HistoryDto historyDto = new() - { - OriginalValue = GetLookupValue(origninalValue, lookupDictionary), - NewValue = GetLookupValue(newValue, lookupDictionary), - ChangeTime = utcDateTime.ToLocalTime(), - UserName = await LookupUserName(entityChange.AuditLogId) - }; - historyList.Add(historyDto); - } + OriginalValue = GetLookupValue(origninalValue, lookupDictionary), + NewValue = GetLookupValue(newValue, lookupDictionary), + ChangeTime = utcDateTime.ToLocalTime(), + UserName = await LookupUserName(entityChange.AuditLogId) + }; + historyList.Add(historyDto); } } - return historyList; } + return historyList; + } - private static string CleanValue(string? value) - { - return value?.Replace("\"", "") ?? ""; - } + private static string CleanValue(string? value) + { + return value?.Replace("\"", "") ?? ""; + } - private static string GetLookupValue(string value, Dictionary? lookupDictionary) + private static string GetLookupValue(string value, Dictionary? lookupDictionary) + { + return lookupDictionary != null && lookupDictionary.TryGetValue(value, out var lookupValue) + ? lookupValue + : value; + } + + public async Task LookupUserName(Guid auditLogId) + { + var auditLog = await auditLogRepository.GetAsync(auditLogId); + if (auditLog?.UserId == null || auditLog.UserId == Guid.Empty) { - return lookupDictionary != null && lookupDictionary.TryGetValue(value, out var lookupValue) - ? lookupValue - : value; + return string.Empty; } - public async Task LookupUserName(Guid auditLogId) + var userId = auditLog.UserId.Value; + using (softDataFilter.Disable()) { - var auditLog = await auditLogRepository.GetAsync(auditLogId); - if (auditLog?.UserId == null || auditLog.UserId == Guid.Empty) - { - return string.Empty; - } - - var userId = auditLog.UserId.Value; - var user = await identityUserRepository.GetAsync(userId); - - return user != null ? $"{user.Name} {user.Surname}" : string.Empty; + var user = await identityUserRepository.FindAsync(userId); + return user != null ? $"{user.Name} {user.Surname}" : "(Full-Deleted User)"; } } } From e71841529c26c0733e4e7a7a9de97821a2070bfb Mon Sep 17 00:00:00 2001 From: Cyrus Parsons Date: Tue, 12 Aug 2025 16:55:45 -0700 Subject: [PATCH 25/47] feature/AB#25072 Notification group frontend almost done --- .../NotificationsSettingGroup/Default.cshtml | 20 +- .../NotificationsSettingGroup/Default.css | 60 +- .../InternalEmailGroups.js | 924 +++++++++++++----- 3 files changed, 746 insertions(+), 258 deletions(-) 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 5d0c97b09..8aac22c84 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 @@ -66,14 +66,22 @@