From 797a9f38b0669807cc0e31046ab4422902b8d9a2 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Tue, 14 Apr 2026 16:32:54 -0700 Subject: [PATCH 001/134] feature/AB#32632-Worksheets --- .../Worksheets/EditSectionDto.cs | 1 + .../Worksheets/IWorksheetAppService.cs | 1 + .../Worksheets/WorksheetDto.cs | 1 + .../Worksheets/WorksheetSectionDto.cs | 2 + .../Domain/Worksheets/Worksheet.cs | 10 +- .../Domain/Worksheets/WorksheetSection.cs | 10 + .../FlexApplicationAutoMapperProfile.cs | 18 +- .../Worksheets/WorksheetAppService.cs | 7 + .../Worksheets/WorksheetSectionAppService.cs | 7 +- .../Localization/Flex/en.json | 14 +- .../Definitions/CustomFieldDefinition.cs | 27 + .../Pages/WorksheetConfiguration/Index.cshtml | 19 +- .../Pages/WorksheetConfiguration/Index.css | 61 +- .../Pages/WorksheetConfiguration/Index.js | 81 + .../UpsertCustomFieldModal.cshtml | 165 +- .../UpsertCustomFieldModal.cshtml.cs | 75 +- .../UpsertSectionModal.cshtml | 77 + .../UpsertSectionModal.cshtml.cs | 8 +- .../DefaultFieldWidget/Default.cshtml | 25 + .../DefaultFieldViewModel.cs | 9 + .../DefaultFieldWidget/DefaultFieldWidget.cs | 17 + .../WorksheetInstanceWidget/Default.cshtml | 218 +- .../WorksheetInstanceWidget/Default.css | 55 + .../ViewModels/WorksheetSectionRenderModel.cs | 14 + .../ViewModels/WorksheetSectionViewModel.cs | 1 + .../WorksheetInstanceWidget.cs | 3 +- .../_WorksheetSections.cshtml | 133 + .../WorksheetListWidget/Default.cshtml | 8 +- .../WorksheetListWidget/WorksheetList.css | 15 + .../WorksheetListWidget.cs | 2 + .../Components/WorksheetWidget/Default.cshtml | 64 +- .../Components/WorksheetWidget/Worksheet.css | 18 + .../Components/WorksheetWidget/Worksheet.js | 44 + .../wwwroot/themes/ux2/fluentui-icons.css | 5 + ...dDefinitionToWorksheetSections.Designer.cs | 4944 ++++++++++++++++ ...153147_AddDefinitionToWorksheetSections.cs | 30 + ...0000_AddIsArchivedToWorksheets.Designer.cs | 4949 +++++++++++++++++ ...0260414100000_AddIsArchivedToWorksheets.cs | 31 + .../GrantTenantDbContextModelSnapshot.cs | 8 + 39 files changed, 10918 insertions(+), 259 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/Default.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/DefaultFieldViewModel.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/DefaultFieldWidget.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/ViewModels/WorksheetSectionRenderModel.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/_WorksheetSections.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260413153147_AddDefinitionToWorksheetSections.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260413153147_AddDefinitionToWorksheetSections.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260414100000_AddIsArchivedToWorksheets.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260414100000_AddIsArchivedToWorksheets.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs index 4da2682097..104c59d70b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs @@ -6,5 +6,6 @@ namespace Unity.Flex.Worksheets public class EditSectionDto { public string Name { get; set; } = string.Empty; + public int? FieldWidth { get; set; } } } 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 13f1168010..6b0c1dc8c3 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 @@ -16,6 +16,7 @@ public interface IWorksheetAppService : IApplicationService Task EditAsync(Guid id, EditWorksheetDto dto); Task CloneAsync(Guid id); Task PublishAsync(Guid id); + Task ArchiveAsync(Guid id, bool archive); Task DeleteAsync(Guid id); Task GetLinkedFormsAsync(Guid worksheetId); Task ResequenceSectionsAsync(Guid id, uint oldIndex, uint newIndex); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs index b4503bbd42..5dc6ffdccb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs @@ -15,5 +15,6 @@ public class WorksheetDto : ExtensibleFullAuditedEntityDto public uint TotalSections { get; set; } = 0; public uint Version { get; set; } = 0; public bool Published { get; set; } = false; + public bool IsArchived { get; set; } = false; } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs index 59f19bf6a5..87d5431352 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs @@ -9,6 +9,8 @@ public class WorksheetSectionDto : EntityDto { public string Name { get; set; } = string.Empty; public uint Order { get; set; } + public int? FieldWidth { get; set; } + public string? Definition { get; set; } public List Fields { get; set; } = []; } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs index 34b24f6484..5a59baf8ad 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs @@ -16,6 +16,7 @@ public class Worksheet : FullAuditedAggregateRoot, IMultiTenant, IReportab public virtual string Title { get; private set; } = string.Empty; public virtual uint Version { get; private set; } = 1; public virtual bool Published { get; private set; } = false; + public virtual bool IsArchived { get; private set; } = false; public Guid? TenantId { get; set; } @@ -61,9 +62,10 @@ internal Worksheet CloneSection(WorksheetSection clonedSection) return this; } - public Worksheet UpdateSection(WorksheetSection section, string name) + public Worksheet UpdateSection(WorksheetSection section, string name, string? definition = null) { section.SetName(name); + section.SetDefinition(definition); return this; } @@ -88,6 +90,12 @@ public Worksheet SetPublished(bool published) return this; } + public Worksheet SetArchived(bool archived) + { + IsArchived = archived; + return this; + } + public Worksheet RemoveSection(WorksheetSection section) { Sections.Remove(section); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs index be6fdef71b..b0770c0b20 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text.Json.Serialization; using Volo.Abp; @@ -26,6 +27,9 @@ public virtual Worksheet Worksheet public virtual string Name { get; private set; } = string.Empty; public virtual uint Order { get; private set; } + [Column(TypeName = "jsonb")] + public virtual string? Definition { get; private set; } + public virtual Collection Fields { get; private set; } = []; public Guid? TenantId { get; set; } @@ -75,6 +79,12 @@ public WorksheetSection SetOrder(uint order) return this; } + public WorksheetSection SetDefinition(string? definition) + { + Definition = definition; + return this; + } + public WorksheetSection RemoveField(CustomField field) { Fields.Remove(field); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs index 1d972e8aa7..fbe39d8126 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using System.Linq; +using System.Text.Json; using Unity.Flex.Domain.ScoresheetInstances; using Unity.Flex.Domain.Scoresheets; using Unity.Flex.Domain.WorksheetInstances; @@ -21,7 +22,9 @@ public FlexApplicationAutoMapperProfile() .ForMember(dest => dest.TotalFields, opt => opt.MapFrom(s => s.Sections.SelectMany(s => s.Fields).Count())); CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.FieldWidth, opt => opt.MapFrom(src => + ParseFieldWidth(src.Definition))); CreateMap(); CreateMap().ReverseMap(); CreateMap(); @@ -46,4 +49,17 @@ public FlexApplicationAutoMapperProfile() CreateMap(); CreateMap(); } + + private static int? ParseFieldWidth(string? definition) + { + if (string.IsNullOrEmpty(definition)) return null; + try + { + using var doc = JsonDocument.Parse(definition); + if (doc.RootElement.TryGetProperty("fieldWidth", out var prop) && prop.TryGetInt32(out var value)) + return value > 0 ? value : null; + } + catch { /* malformed JSON — fall through */ } + return null; + } } 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 90b41405a2..3f1a07c05a 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 @@ -124,6 +124,13 @@ public virtual async Task PublishAsync(Guid id) return await Task.FromResult(true); } + public virtual async Task ArchiveAsync(Guid id, bool archive) + { + var worksheet = await worksheetRepository.GetAsync(id); + _ = worksheet.SetArchived(archive); + return await Task.FromResult(true); + } + [Authorize(FlexPermissions.Worksheets.Delete)] public virtual async Task DeleteAsync(Guid id) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs index 7e8f4aa39f..ba7d54c466 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using System; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Unity.Flex.Domain.Worksheets; using Volo.Abp; @@ -20,7 +21,11 @@ public virtual async Task EditAsync(Guid id, EditSectionDto { (Worksheet worksheet, WorksheetSection section) = await GetWorksheetAndSectionAsync(id); - _ = worksheet.UpdateSection(section, dto.Name.Trim()); + string? definition = dto.FieldWidth is > 0 + ? JsonSerializer.Serialize(new { fieldWidth = dto.FieldWidth }) + : null; + + _ = worksheet.UpdateSection(section, dto.Name.Trim(), definition); return ObjectMapper.Map(section); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json index c615f56148..be1208b9d2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json @@ -20,16 +20,16 @@ "Scoresheet:Configuration:DiscardChangesButtonText": "DISCARD CHANGES", "Worksheet:Configuration:SaveChangesButtonText": "SAVE CHANGES", "Worksheet:Configuration:DiscardChangesButtonText": "DISCARD CHANGES", - "Worksheet:Configuration:EditWorksheetButtonText": "Edit Worksheet", + "Worksheet:Configuration:EditWorksheetButtonText": "Edit", "Worksheet:Configuration:AddCustomFieldButtonText": "Add Field", "Worksheet:Configuration:AddSectionButtonText": "ADD SECTION", - "Worksheet:Configuration:AddWorksheetButtonText": "ADD WORKSHEET", - "Worksheet:Configuration:LinkWorksheetButtonText": "Link Worksheet", - "Worksheet:Configuration:CloneWorksheetButtonText": "Clone Worksheet", - "Worksheet:Configuration:PublishWorksheetButtonText": "Publish Worksheet", - "Worksheet:Configuration:DeleteWorksheetButtonText": "Delete Worksheet", + "Worksheet:Configuration:AddWorksheetButtonText": "ADD", + "Worksheet:Configuration:LinkWorksheetButtonText": "Link", + "Worksheet:Configuration:CloneWorksheetButtonText": "Clone", + "Worksheet:Configuration:PublishWorksheetButtonText": "Publish", + "Worksheet:Configuration:DeleteWorksheetButtonText": "Delete", "Worksheet:Configuration:AddCheckboxOptionText": "Add Option", - "Worksheet:Configuration:ExportWorksheetButtonText": "Export Worksheet", + "Worksheet:Configuration:ExportWorksheetButtonText": "Export", "Worksheet:Configuration:AddSelectListOptionText": "Add Option", "Worksheet:Configuration:AddColumnOptionText": "Add Column", "DataGrids:DynamicColumnsHeader": "Dynamic Columns", diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs index 8f188b66c9..9e5a9e9fa5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs @@ -7,6 +7,33 @@ public class CustomFieldDefinition [JsonPropertyName("required")] public bool Required { get; set; } = false; + [JsonPropertyName("isHidden")] + public bool IsHidden { get; set; } = false; + + [JsonPropertyName("hideLabel")] + public bool HideLabel { get; set; } = false; + + [JsonPropertyName("isDisabled")] + public bool IsDisabled { get; set; } = false; + + [JsonPropertyName("labelPosition")] + public string LabelPosition { get; set; } = "Top"; + + [JsonPropertyName("style")] + public string? Style { get; set; } + + [JsonPropertyName("cssClass")] + public string? CssClass { get; set; } + + [JsonPropertyName("labelStyle")] + public string? LabelStyle { get; set; } + + [JsonPropertyName("labelCssClass")] + public string? LabelCssClass { get; set; } + + [JsonPropertyName("securityClassification")] + public string? SecurityClassification { get; set; } + public CustomFieldDefinition() : base() { } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml index a1eeeb7708..dc24af3ca1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml @@ -24,11 +24,11 @@
- + -

Worksheets

-
+
+

Worksheets

+
+ +
+ Published filter + + + + +
+
@await Component.InvokeAsync(typeof(WorksheetListWidget))
@@ -51,7 +61,8 @@ - +
+

Preview

diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css index 3ce40d4e41..5ccd0f5f84 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css @@ -3,10 +3,40 @@ flex-wrap: nowrap; } -.worksheet-configuration-container > div { +.worksheet-configuration-container > .col { overflow-y: scroll; } +.column-resizer { + width: 6px; + cursor: col-resize; + background-color: #dee2e6; + flex-shrink: 0; + transition: background-color 0.15s; + border-radius: 3px; +} + +.column-resizer:hover, +.column-resizer.dragging { + background-color: var(--bs-accordion-active-bg); +} + +.worksheet-filters { + flex-wrap: nowrap; +} + +.btn-label-position { + border: 2px solid #acb2b7; + background-color: transparent; + color: inherit; +} + +.btn-check:checked + .btn-label-position { + background-color: #acb2b7; + border: 2px solid var(--bc-colors-blue-primary); + color: inherit; +} + .worksheet-scrollable-content { overflow-y: scroll; height: calc(100vh - 120px); @@ -36,4 +66,33 @@ .sticky-preview { position: sticky; top: 0; +} + +/* Custom field modal section headings */ +.cf-section-heading { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--bs-secondary-color, #6c757d); + padding-bottom: 0.35rem; + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + margin-top: 1.25rem; + margin-bottom: 0.75rem; +} + +.cf-section-heading:first-child { + margin-top: 0; +} + +/* Tab error indicator badge */ +.tab-error-badge { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--bs-danger, #dc3545); + margin-left: 6px; + vertical-align: middle; + flex-shrink: 0; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js index 0500b11825..9a4c34092a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js @@ -2,6 +2,87 @@ $(function () { $('#worksheet_import_upload_btn').click(function () { $('#worksheet_import_upload').trigger('click'); }); + + // --- Column resizer --- + const resizer = document.getElementById('column-resizer'); + const leftCol = document.getElementById('worksheet-left-col'); + const rightCol = document.getElementById('worksheet-right-col'); + + if (resizer && leftCol && rightCol) { + let isResizing = false; + let startX = 0; + let startLeftWidth = 0; + + resizer.addEventListener('mousedown', function (e) { + isResizing = true; + startX = e.clientX; + startLeftWidth = leftCol.getBoundingClientRect().width; + resizer.classList.add('dragging'); + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', function (e) { + if (!isResizing) return; + const container = resizer.parentElement; + const containerWidth = container.getBoundingClientRect().width; + const resizerWidth = resizer.getBoundingClientRect().width; + const dx = e.clientX - startX; + let newLeftWidth = startLeftWidth + dx; + newLeftWidth = Math.max(200, Math.min(containerWidth - resizerWidth - 200, newLeftWidth)); + leftCol.style.flex = `0 0 ${newLeftWidth}px`; + rightCol.style.flex = '1 1 0'; + }); + + document.addEventListener('mouseup', function () { + if (!isResizing) return; + isResizing = false; + resizer.classList.remove('dragging'); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }); + } + + // --- Worksheet name filter & published/archived toggle --- + function applyWorksheetFilters() { + const searchText = $('#worksheet-name-filter').val().toLowerCase(); + const publishedFilter = $('#worksheet-published-toggle .active').data('filter'); + + $('#worksheet-accordion .accordion-item').each(function () { + const $item = $(this); + const title = $item.find('.worksheet-title').text().toLowerCase(); + const name = $item.find('.worksheet-name').text().toLowerCase(); + const isPublished = !$item.find('.worksheet-published-icon').hasClass('hidden'); + const isArchived = $item.data('is-archived') === true || $item.data('is-archived') === 'true'; + + const matchesText = !searchText || title.includes(searchText) || name.includes(searchText); + const matchesFilter = + publishedFilter === 'archived' + ? isArchived + : !isArchived && ( + publishedFilter === 'all' || + (publishedFilter === 'published' && isPublished) || + (publishedFilter === 'unpublished' && !isPublished) + ); + + $item.toggle(matchesText && matchesFilter); + }); + } + + $(document).on('input', '#worksheet-name-filter', applyWorksheetFilters); + + $(document).on('click', '#worksheet-published-toggle button', function () { + $('#worksheet-published-toggle button').removeClass('active'); + $(this).addClass('active'); + applyWorksheetFilters(); + }); + + PubSub.subscribe('worksheet_list_refreshed', function () { + applyWorksheetFilters(); + }); + + applyWorksheetFilters(); }); function importWorksheetFile(inputId) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml index e89d9c4d46..d2eae9f33b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml @@ -25,34 +25,99 @@ - - - - - - - - - - - - - - - -
- @await Component.InvokeAsync(typeof(CustomFieldDefinitionWidget), new { type = Model.FieldType, definition = Model.Definition }) + +
+
+ + + + + + + + + + + +
+ Label position + + + + +
+
+
+ + + + + + +
+ @await Component.InvokeAsync(typeof(CustomFieldDefinitionWidget), new { type = Model.FieldType, definition = Model.Definition }) +
+
+
+

Security

+ @{ + string? scNone = string.IsNullOrEmpty(Model.SecurityClassification) ? "selected" : null; + string? scA = Model.SecurityClassification == "ProtectedA" ? "selected" : null; + string? scB = Model.SecurityClassification == "ProtectedB" ? "selected" : null; + string? scC = Model.SecurityClassification == "ProtectedC" ? "selected" : null; + } +
+ + +
+
+ +

Visibility

+
+
+ +
+
+ +
+
+ +
+
+ +

Label

+
+ + +
+
+ + +
+ +
+
- - @if (Model.UpsertAction == WorksheetUpsertAction.Update && !Model.Published) { - + } + + @@ -90,4 +155,62 @@ } }); } + + const classificationHints = { + 'ProtectedA': 'If compromised, could cause limited or moderate injury to an individual or organisation — e.g. an exact salary figure or home address.', + 'ProtectedB': 'Could cause serious injury if disclosed — e.g. Social Insurance Numbers, employment equity data, or personal health records.', + 'ProtectedC': 'The most sensitive level — disclosure could cause extremely grave injury.' + }; + + function updateClassificationHint(value) { + const hint = document.getElementById('classificationHint'); + if (hint) hint.textContent = classificationHints[value] || ''; + } + + updateClassificationHint('@(Model.SecurityClassification ?? "")'); + + // Cross-tab validation: flag tabs that contain invalid fields when save is attempted + document.querySelector('[name="saveCustomFieldBtn"]').addEventListener('click', function () { + // Brief defer so browser native validation runs first + setTimeout(function () { + var form = document.getElementById('customFieldInfo'); + var panes = ['pane-display', 'pane-attributes']; + var tabButtons = { 'pane-display': 'tab-display', 'pane-attributes': 'tab-attributes' }; + var activePane = document.querySelector('#customFieldTabContent .tab-pane.show'); + var activePaneId = activePane ? activePane.id : null; + var hasOffTabErrors = false; + + panes.forEach(function (paneId) { + if (paneId === activePaneId) return; + var pane = document.getElementById(paneId); + var invalid = pane ? pane.querySelector(':invalid') : null; + var btn = document.getElementById(tabButtons[paneId]); + if (!btn) return; + var badge = btn.querySelector('.tab-error-badge'); + if (invalid) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'tab-error-badge'; + badge.setAttribute('aria-label', 'This tab has errors'); + btn.appendChild(badge); + } + hasOffTabErrors = true; + } else { + if (badge) badge.remove(); + } + }); + + if (hasOffTabErrors) { + abp.notify.warn('There are validation errors on another tab. Please review all tabs before saving.'); + } + }, 0); + }); + + // Clear error badge when a tab is activated + document.querySelectorAll('#customFieldTabs [data-bs-toggle="tab"]').forEach(function (btn) { + btn.addEventListener('shown.bs.tab', function () { + var badge = btn.querySelector('.tab-error-badge'); + if (badge) badge.remove(); + }); + }); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs index 9a1c7e55a2..9d24b91d39 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs @@ -5,8 +5,11 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using System.Text.Json; +using Unity.Flex; using Unity.Flex.Web.Views.Shared.Components.CustomFieldDefinitionWidget; using Unity.Flex.Worksheets; +using Unity.Flex.Worksheets.Definitions; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; namespace Unity.Flex.Web.Pages.WorksheetConfiguration; @@ -52,6 +55,31 @@ public class UpsertCustomFieldModalModel(ICustomFieldAppService customFieldAppSe [BindProperty] public bool IsDelete { get; set; } + [BindProperty] + [DisplayName("Is Hidden")] + public bool IsHidden { get; set; } + + [BindProperty] + [DisplayName("Hide Label")] + public bool HideLabel { get; set; } + + [BindProperty] + [DisplayName("Is Disabled")] + public bool IsDisabled { get; set; } + + [BindProperty] + public string LabelPosition { get; set; } = "Top"; + + [BindProperty] + public string? LabelStyle { get; set; } + + [BindProperty] + public string? LabelCssClass { get; set; } + + [BindProperty] + [DisplayName("Security Classification")] + public string? SecurityClassification { get; set; } + [SelectItems(nameof(FieldTypes))] [Required] [BindProperty] @@ -77,6 +105,18 @@ public async Task OnGetAsync(Guid worksheetId, Guid sectionId, Guid fieldId, str Published = worksheet.Published; FieldType = customField.Type.ToString(); Definition = customField.Definition; + + if (customField.Definition != null) + { + var existingDef = JsonSerializer.Deserialize(customField.Definition); + IsHidden = existingDef?.IsHidden ?? false; + HideLabel = existingDef?.HideLabel ?? false; + IsDisabled = existingDef?.IsDisabled ?? false; + LabelPosition = existingDef?.LabelPosition ?? "Top"; + LabelStyle = existingDef?.LabelStyle; + LabelCssClass = existingDef?.LabelCssClass; + SecurityClassification = existingDef?.SecurityClassification; + } } } @@ -135,9 +175,38 @@ private async Task UpdateCustomField() private object? ExtractDefinition() { - var fieldType = Enum.TryParse(FieldType, out CustomFieldType type); - if (!fieldType) return null; - return CustomFieldDefinitionWidget.ParseFormValues(type, Request.Form); + if (!Enum.TryParse(FieldType, out CustomFieldType type)) return null; + + var definition = CustomFieldDefinitionWidget.ParseFormValues(type, Request.Form); + + // Types with a definition editor: ParseFormValues already built the typed object. + if (definition is CustomFieldDefinition def) + { + ApplyFieldOptions(def); + return definition; + } + + // Types without a definition editor (Date, Checkbox, YesNo, Email, Phone…): + // rehydrate the existing definition JSON so we preserve Required, then apply options. + var existingDef = (Definition ?? "{}").ConvertDefinition(type); + if (existingDef != null) + { + ApplyFieldOptions(existingDef); + return existingDef; + } + + return definition; + } + + private void ApplyFieldOptions(CustomFieldDefinition def) + { + def.IsHidden = IsHidden; + def.HideLabel = HideLabel; + def.IsDisabled = IsDisabled; + def.LabelPosition = LabelPosition; + def.LabelStyle = string.IsNullOrWhiteSpace(LabelStyle) ? null : LabelStyle.Trim(); + def.LabelCssClass = string.IsNullOrWhiteSpace(LabelCssClass) ? null : LabelCssClass.Trim(); + def.SecurityClassification = string.IsNullOrEmpty(SecurityClassification) ? null : SecurityClassification; } private OkObjectResult MapModalResponse(CustomFieldDto customFieldDto) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml index df0f763371..3495d3990a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml @@ -28,6 +28,36 @@ + + + +
+ Auto + + 100% + +
+
+ + +
+
+ Set to 0 to use automatic column layout based on number of fields. Otherwise fields in this section will be a fixed percentage width. +
+
+
@@ -42,6 +72,53 @@ diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs index 9d24b91d39..7a8486b53b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs @@ -80,6 +80,9 @@ public class UpsertCustomFieldModalModel(ICustomFieldAppService customFieldAppSe [DisplayName("Security Classification")] public string? SecurityClassification { get; set; } + [BindProperty] + public string? Placeholder { get; set; } + [SelectItems(nameof(FieldTypes))] [Required] [BindProperty] @@ -116,6 +119,7 @@ public async Task OnGetAsync(Guid worksheetId, Guid sectionId, Guid fieldId, str LabelStyle = existingDef?.LabelStyle; LabelCssClass = existingDef?.LabelCssClass; SecurityClassification = existingDef?.SecurityClassification; + Placeholder = existingDef?.Placeholder; } } } @@ -207,6 +211,7 @@ private void ApplyFieldOptions(CustomFieldDefinition def) def.LabelStyle = string.IsNullOrWhiteSpace(LabelStyle) ? null : LabelStyle.Trim(); def.LabelCssClass = string.IsNullOrWhiteSpace(LabelCssClass) ? null : LabelCssClass.Trim(); def.SecurityClassification = string.IsNullOrEmpty(SecurityClassification) ? null : SecurityClassification; + def.Placeholder = string.IsNullOrWhiteSpace(Placeholder) ? null : Placeholder.Trim(); } private OkObjectResult MapModalResponse(CustomFieldDto customFieldDto) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js new file mode 100644 index 0000000000..341d446069 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js @@ -0,0 +1,104 @@ +abp.modals.UpsertCustomFieldModal = function () { + + const classificationHints = { + 'ProtectedA': 'If compromised, could cause limited or moderate injury to an individual or organisation — e.g. an exact salary figure or home address.', + 'ProtectedB': 'Could cause serious injury if disclosed — e.g. Social Insurance Numbers, employment equity data, or personal health records.', + 'ProtectedC': 'The most sensitive level — disclosure could cause extremely grave injury.' + }; + + const placeholderSupportedTypes = ['Text', 'TextArea', 'Numeric', 'Currency', 'Email', 'Phone']; + + function updateClassificationHint(value) { + const hint = document.getElementById('classificationHint'); + if (hint) hint.textContent = classificationHints[value] || ''; + } + + function updatePlaceholderVisibility(type) { + const row = document.getElementById('placeholder-row'); + if (row) row.style.display = placeholderSupportedTypes.includes(type) ? '' : 'none'; + } + + function initModal(modalManager, args) { + const initClassification = document.getElementById('InitSecurityClassification')?.value ?? ''; + const initFieldType = document.getElementById('InitFieldType')?.value ?? 'Text'; + + updateClassificationHint(initClassification); + updatePlaceholderVisibility(initFieldType); + + document.getElementById('fieldType')?.addEventListener('change', function () { + updatePlaceholderVisibility(this.value); + const customFieldWidget = new abp.WidgetManager({ + wrapper: '#definition-editor', + filterCallback: function () { + return { 'type': $('#fieldType').val() }; + } + }); + customFieldWidget.refresh(); + }); + + document.getElementById('SecurityClassification')?.addEventListener('change', function () { + updateClassificationHint(this.value); + }); + + document.querySelector('[name="deleteCustomFieldBtn"]')?.addEventListener('click', function () { + Swal.fire({ + title: "Delete Custom Field?", + text: 'Are you sure you want to delete this custom field?', + showCancelButton: true, + confirmButtonText: 'Confirm', + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then((result) => { + if (result.isConfirmed) { + $('#DeleteAction').val(true); + $('#customFieldInfo').submit(); + } + }); + }); + + document.querySelector('[name="saveCustomFieldBtn"]')?.addEventListener('click', function () { + setTimeout(function () { + var panes = ['pane-display', 'pane-attributes']; + var tabButtons = { 'pane-display': 'tab-display', 'pane-attributes': 'tab-attributes' }; + var activePane = document.querySelector('#customFieldTabContent .tab-pane.show'); + var activePaneId = activePane ? activePane.id : null; + var hasOffTabErrors = false; + + panes.forEach(function (paneId) { + if (paneId === activePaneId) return; + var pane = document.getElementById(paneId); + var invalid = pane ? pane.querySelector(':invalid') : null; + var btn = document.getElementById(tabButtons[paneId]); + if (!btn) return; + var badge = btn.querySelector('.tab-error-badge'); + if (invalid) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'tab-error-badge'; + badge.setAttribute('aria-label', 'This tab has errors'); + btn.appendChild(badge); + } + hasOffTabErrors = true; + } else { + if (badge) badge.remove(); + } + }); + + if (hasOffTabErrors) { + abp.notify.warn('There are validation errors on another tab. Please review all tabs before saving.'); + } + }, 0); + }); + + document.querySelectorAll('#customFieldTabs [data-bs-toggle="tab"]').forEach(function (btn) { + btn.addEventListener('shown.bs.tab', function () { + var badge = btn.querySelector('.tab-error-badge'); + if (badge) badge.remove(); + }); + }); + } + + return { initModal: initModal }; +}; 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 6b84c9ef84..810d8b832a 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 @@ -19,6 +19,7 @@ class="@Model.Field.Type.ApplyCssClass()" data-min="@Model.Field.Definition?.ConvertDefinition(Model.Field.Type)?.GetMinValueOrNull()" data-max="@Model.Field.Definition?.ConvertDefinition(Model.Field.Type)?.GetMaxValueOrNull()" + placeholder="@Model.Field.Definition?.ConvertDefinition(Model.Field.Type)?.Placeholder" id="@fieldId" name="@fieldId" onchange="@($"notifyFieldChange('{Model.Name}', '{Model.Field?.UiAnchor}', this)")"/> diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/Default.cshtml index c8de12149c..926c9b6a5b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DefaultFieldWidget/Default.cshtml @@ -19,6 +19,7 @@ max="@fieldDef?.GetMaxValueOrNull()" minlength="@fieldDef?.GetMinLengthValueOrNull()" maxlength="@fieldDef?.GetMaxLengthValueOrNull()" + placeholder="@fieldDef?.Placeholder" id="@fieldId" name="@fieldId" onchange="@($"notifyFieldChange('{Model.Name}', '{Model.Field.UiAnchor}', this)")" /> diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/TextAreaWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/TextAreaWidget/Default.cshtml index 3229b5dfe5..0a3eb5241e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/TextAreaWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/TextAreaWidget/Default.cshtml @@ -12,10 +12,13 @@ ? $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}.{Model.WorksheetId}" : $"{Model.Field.Name}.{Model.Name}.{Model.Field.Id}"; + var fieldDef = (TextAreaDefinition?)Model.Field?.Definition?.ConvertDefinition(Model.Field.Type); + } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 5d2ac16896..dbe753c4ff 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -11,7 +11,7 @@ $(function () { makeSectionsAndFieldsSortable(); function bindActionButtons() { - let addWorksheetButton = $("#add_worksheet_btn"); + let addWorksheetButton = $(".worksheet-add-btn"); if (addWorksheetButton) { addWorksheetButton.on("click", function (_) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js index 56075e343e..a5fe12bb47 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js @@ -4,7 +4,8 @@ $(function () { }); let customFieldModal = new abp.ModalManager({ - viewUrl: 'WorksheetConfiguration/UpsertCustomFieldModal' + viewUrl: 'WorksheetConfiguration/UpsertCustomFieldModal', + modalClass: 'UpsertCustomFieldModal' }); let editWorksheetModal = new abp.ModalManager({ diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/WorksheetWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/WorksheetWidget.cs index 19d3afbea6..889e17d819 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/WorksheetWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/WorksheetWidget.cs @@ -60,5 +60,7 @@ public override void ConfigureBundle(BundleConfigurationContext context) { context.Files .AddIfNotContains("/Views/Shared/Components/WorksheetWidget/Worksheet.js"); + context.Files + .AddIfNotContains("/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js"); } } \ No newline at end of file From afa5b8200dc95b094b2ce059e8fd9ae245cb425c Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 15 Apr 2026 12:04:54 -0700 Subject: [PATCH 006/134] feature/AB#32632-Worksheets add delete to the unarchive --- .../Views/Shared/Components/WorksheetWidget/Default.cshtml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml index ca210efe16..21cfaf4b0c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml @@ -22,6 +22,12 @@ + @if (await PermissionChecker.IsGrantedAsync(FlexPermissions.Worksheets.Delete)) + { + + } } else { From a2c3c7c1e7a3d37dc185059c2614dd01134c04ad Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 15 Apr 2026 12:11:12 -0700 Subject: [PATCH 007/134] feature/AB#32632-Worksheets hooked into sonar cloud --- .../WorksheetListWidget/WorksheetList.js | 91 ++++++------- .../Components/WorksheetWidget/Worksheet.js | 126 +++++++++--------- 2 files changed, 109 insertions(+), 108 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index dbe753c4ff..036ab4b8b2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -61,20 +61,7 @@ $(function () { PubSub.publish('refresh_worksheet_list'); }); - function refreshWorksheetListWidget() { - const url = `../Flex/Widgets/WorksheetList/Refresh`; - fetch(url) - .then(response => response.text()) - .then(data => { - document.getElementById('worksheet-info-widget-list').innerHTML = data; - setTimeout(() => { - PubSub.publish('worksheet_list_refreshed'); - }, 100); - }) - .catch(error => { - console.error('Error refreshing worksheet-info-list-widget:', error); - }); - } + function makeSectionsAndFieldsSortable() { makeCustomFieldsSortable(); @@ -146,37 +133,6 @@ $(function () { }); } - function updatePreview() { - let worksheets = $('button.accordion-button[aria-expanded=true]'); - const previewPane = $('#preview'); - - if (worksheets?.length > 0) { - let worksheetId = worksheets[0].dataset.worksheetId; - const url = `../Flex/Widgets/WorksheetInstance/Refresh?` - + `instanceCorrelationId=00000000-0000-0000-0000-000000000000&` - + `instanceCorrelationProvider=Preview&` - + `sheetCorrelationId=00000000-0000-0000-0000-000000000000&` - + `sheetCorrelationProvider=Preview&` - + `uiAnchor=Preview&` - + `worksheetId=${worksheetId}`; - fetch(url) - .then(response => response.text()) - .then(data => { - previewPane.html(data); - $("#preview :input").prop("readonly", true); - PubSub.publish('worksheet_preview_datagrid_refresh'); - }) - .catch(error => { - console.error('Error generating preview:', error); - }); - - } else { - previewPane?.html('

No sections to display.

'); - } - - $('.preview-scrollable').first().scrollTop(0); - } - PubSub.subscribe( 'refresh_worksheet_list', () => { @@ -205,3 +161,48 @@ $(function () { } ); }); + +function refreshWorksheetListWidget() { + const url = `../Flex/Widgets/WorksheetList/Refresh`; + fetch(url) + .then(response => response.text()) + .then(data => { + document.getElementById('worksheet-info-widget-list').innerHTML = data; + setTimeout(() => { + PubSub.publish('worksheet_list_refreshed'); + }, 100); + }) + .catch(error => { + console.error('Error refreshing worksheet-info-list-widget:', error); + }); +} + +function updatePreview() { + let worksheets = $('button.accordion-button[aria-expanded=true]'); + const previewPane = $('#preview'); + + if (worksheets?.length > 0) { + let worksheetId = worksheets[0].dataset.worksheetId; + const url = `../Flex/Widgets/WorksheetInstance/Refresh?` + + `instanceCorrelationId=00000000-0000-0000-0000-000000000000&` + + `instanceCorrelationProvider=Preview&` + + `sheetCorrelationId=00000000-0000-0000-0000-000000000000&` + + `sheetCorrelationProvider=Preview&` + + `uiAnchor=Preview&` + + `worksheetId=${worksheetId}`; + fetch(url) + .then(response => response.text()) + .then(data => { + previewPane.html(data); + $('#preview :input').prop('readonly', true); + PubSub.publish('worksheet_preview_datagrid_refresh'); + }) + .catch(error => { + console.error('Error generating preview:', error); + }); + } else { + previewPane?.html('

No sections to display.

'); + } + + $('.preview-scrollable').first().scrollTop(0); +} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js index a5fe12bb47..1561dfb19f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js @@ -103,36 +103,6 @@ $(function () { setupTooltips(); } - function setupTooltips() { - $('[data-toggle="tooltip"]').tooltip({ - placement: 'top', - delay: { show: 100, hide: 0 } - }); - } - - function exportWorksheet(worksheetId, worksheetName, worksheetTitle) { - fetch(`/api/app/worksheet/export/${worksheetId}`) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.blob(); - }) - .then(blob => { - let url = window.URL.createObjectURL(blob); - let a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = `worksheet_${worksheetTitle}_${worksheetName}.json`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - }) - .catch(error => { - console.error('There was a problem with the fetch operation:', error); - }); - } - function openEditWorksheetModal(worksheetId) { editWorksheetModal.open({ worksheetId: worksheetId, @@ -188,39 +158,6 @@ $(function () { PubSub.publish('refresh_worksheet', { worksheetId: response.responseText.worksheetId }); }); - function refreshWorksheetInfoWidget(worksheetId) { - const url = `../Flex/Widgets/Worksheet/Refresh?worksheetId=${worksheetId}`; - fetch(url) - .then(response => response.text()) - .then(data => { - document.getElementById('worksheet-info-widget-' + worksheetId).innerHTML = data; - PubSub.publish('worksheet_refreshed', worksheetId); - }) - .catch(error => { - console.error('Error refreshing worksheet-info-widget:', error); - }); - } - - function updateWorksheetAccordionButton(worksheetId) { - // Get basic refreshed header level details of the worksheet - unity.flex.worksheets.worksheetList.get(worksheetId) - .done(function (result) { - let titleField = $("#worksheet-title-" + worksheetId); - let nameField = $("#worksheet-name-" + worksheetId); - let sectionCountField = $("#worksheet-total-sections-" + worksheetId); - let fieldsCountField = $("#worksheet-total-fields-" + worksheetId); - let worksheetPublished = $("#worksheet-published-" + worksheetId); - - titleField?.text(result.title); - sectionCountField?.text(result.totalSections); - fieldsCountField?.text(result.totalFields); - nameField?.text(result.name); - if (result.published) { - worksheetPublished?.removeClass('hidden'); - } - }); - } - PubSub.subscribe( 'worksheet_refreshed', (_, data) => { @@ -245,6 +182,69 @@ $(function () { ); }); +function setupTooltips() { + $('[data-toggle="tooltip"]').tooltip({ + placement: 'top', + delay: { show: 100, hide: 0 } + }); +} + +function exportWorksheet(worksheetId, worksheetName, worksheetTitle) { + fetch(`/api/app/worksheet/export/${worksheetId}`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.blob(); + }) + .then(blob => { + let url = globalThis.URL.createObjectURL(blob); + let a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `worksheet_${worksheetTitle}_${worksheetName}.json`; + document.body.appendChild(a); + a.click(); + globalThis.URL.revokeObjectURL(url); + }) + .catch(error => { + console.error('There was a problem with the fetch operation:', error); + }); +} + +function refreshWorksheetInfoWidget(worksheetId) { + const url = `../Flex/Widgets/Worksheet/Refresh?worksheetId=${worksheetId}`; + fetch(url) + .then(response => response.text()) + .then(data => { + document.getElementById('worksheet-info-widget-' + worksheetId).innerHTML = data; + PubSub.publish('worksheet_refreshed', worksheetId); + }) + .catch(error => { + console.error('Error refreshing worksheet-info-widget:', error); + }); +} + +function updateWorksheetAccordionButton(worksheetId) { + // Get basic refreshed header level details of the worksheet + unity.flex.worksheets.worksheetList.get(worksheetId) + .done(function (result) { + let titleField = $("#worksheet-title-" + worksheetId); + let nameField = $("#worksheet-name-" + worksheetId); + let sectionCountField = $("#worksheet-total-sections-" + worksheetId); + let fieldsCountField = $("#worksheet-total-fields-" + worksheetId); + let worksheetPublished = $("#worksheet-published-" + worksheetId); + + titleField?.text(result.title); + sectionCountField?.text(result.totalSections); + fieldsCountField?.text(result.totalFields); + nameField?.text(result.name); + if (result.published) { + worksheetPublished?.removeClass('hidden'); + } + }); +} + function handleArchiveWorksheet(worksheetId, currentlyArchived) { const action = currentlyArchived ? 'unarchive' : 'archive'; const label = currentlyArchived ? 'Unarchive Worksheet' : 'Archive Worksheet'; From 583d2fe210c84abaa7784f0d35fb56b3fcceba3a Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 15 Apr 2026 12:32:18 -0700 Subject: [PATCH 008/134] feature/AB#32632-Worksheets hooked into sonar cloud --- .../WorksheetListWidget/WorksheetList.js | 142 +++++++++--------- 1 file changed, 70 insertions(+), 72 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 036ab4b8b2..353735b209 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -61,78 +61,6 @@ $(function () { PubSub.publish('refresh_worksheet_list'); }); - - - function makeSectionsAndFieldsSortable() { - makeCustomFieldsSortable(); - makeSectionsSortable(); - } - - function makeCustomFieldsSortable() { - document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { - const wrapper = div.closest('.sections-wrapper-outer'); - const isArchived = wrapper?.dataset.isArchived === 'true'; - _ = new Sortable(div, { - animation: 150, - disabled: isArchived, - onEnd: function (evt) { - updateCustomFieldsSequence(evt); - }, - ghostClass: 'blue-background', - onMove: function (_) { - return true; - } - }); - }); - } - - function makeSectionsSortable() { - document.querySelectorAll('.sections-wrapper-outer').forEach(function (div) { - const isArchived = div.dataset.isArchived === 'true'; - _ = new Sortable(div, { - animation: 150, - disabled: isArchived, - onEnd: function (evt) { - updateSectionSequence(evt); - }, - ghostClass: 'blue-background', - onMove: function (_) { - return true; - } - }); - }); - } - - function updateCustomFieldsSequence(evt) { - let sectionId = evt.target.dataset.sectionId; - let oldIndex = evt.oldIndex; - let newIndex = evt.newIndex; - - unity.flex.worksheets.worksheetSection - .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) - .done(function () { - updatePreview(); - abp.notify.success( - 'Custom fields order updated.' - ); - }); - } - - function updateSectionSequence(evt) { - let worksheetId = evt.target.dataset.worksheetId; - let oldIndex = evt.oldIndex; - let newIndex = evt.newIndex; - - unity.flex.worksheets.worksheet - .resequenceSections(worksheetId, oldIndex, newIndex, {}) - .done(function () { - updatePreview(); - abp.notify.success( - 'Sections fields order updated.' - ); - }); - } - PubSub.subscribe( 'refresh_worksheet_list', () => { @@ -162,6 +90,76 @@ $(function () { ); }); +function makeSectionsAndFieldsSortable() { + makeCustomFieldsSortable(); + makeSectionsSortable(); +} + +function makeCustomFieldsSortable() { + document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { + const wrapper = div.closest('.sections-wrapper-outer'); + const isArchived = wrapper?.dataset.isArchived === 'true'; + _ = new Sortable(div, { + animation: 150, + disabled: isArchived, + onEnd: function (evt) { + updateCustomFieldsSequence(evt); + }, + ghostClass: 'blue-background', + onMove: function (_) { + return true; + } + }); + }); +} + +function makeSectionsSortable() { + document.querySelectorAll('.sections-wrapper-outer').forEach(function (div) { + const isArchived = div.dataset.isArchived === 'true'; + _ = new Sortable(div, { + animation: 150, + disabled: isArchived, + onEnd: function (evt) { + updateSectionSequence(evt); + }, + ghostClass: 'blue-background', + onMove: function (_) { + return true; + } + }); + }); +} + +function updateCustomFieldsSequence(evt) { + let sectionId = evt.target.dataset.sectionId; + let oldIndex = evt.oldIndex; + let newIndex = evt.newIndex; + + unity.flex.worksheets.worksheetSection + .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) + .done(function () { + updatePreview(); + abp.notify.success( + 'Custom fields order updated.' + ); + }); +} + +function updateSectionSequence(evt) { + let worksheetId = evt.target.dataset.worksheetId; + let oldIndex = evt.oldIndex; + let newIndex = evt.newIndex; + + unity.flex.worksheets.worksheet + .resequenceSections(worksheetId, oldIndex, newIndex, {}) + .done(function () { + updatePreview(); + abp.notify.success( + 'Sections fields order updated.' + ); + }); +} + function refreshWorksheetListWidget() { const url = `../Flex/Widgets/WorksheetList/Refresh`; fetch(url) From b3d2498bb5f62dc7297e3945120fa83284f4dc49 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 15 Apr 2026 15:14:44 -0700 Subject: [PATCH 009/134] feature/AB#32632-Worksheets show message for no worksheets --- .../Pages/WorksheetConfiguration/Index.cshtml | 63 ++++++++----------- .../Pages/WorksheetConfiguration/Index.css | 26 +++++++- .../Pages/WorksheetConfiguration/Index.js | 7 ++- 3 files changed, 56 insertions(+), 40 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml index 65e72c2b9b..e765faa9d3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml @@ -24,41 +24,14 @@
- - - -
- -
-
-

Worksheets

-
- - -
-
-
- -
- Published filter - - - - -
-
-
- @await Component.InvokeAsync(typeof(WorksheetListWidget)) -
-
+ +
+
+ +
+
+

Worksheets

+
- - +
+
+ +
+ Published filter + + + + +
+
+
+
+
+ @await Component.InvokeAsync(typeof(WorksheetListWidget)) +
+ +
diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css index 5ccd0f5f84..5097e505fb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css @@ -3,10 +3,28 @@ flex-wrap: nowrap; } -.worksheet-configuration-container > .col { - overflow-y: scroll; +#worksheet-left-col { + overflow: hidden; +} + +.worksheet-left-header { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--bs-card-bg, #fff); + padding: 1rem 1rem 0; + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; } +.worksheet-left-body { + overflow-y: auto; + flex: 1 1 auto; + padding: 0 1rem; +} + + .column-resizer { width: 6px; cursor: col-resize; @@ -25,6 +43,10 @@ flex-wrap: nowrap; } +#worksheet-accordion { + padding-top: 1px; +} + .btn-label-position { border: 2px solid #acb2b7; background-color: transparent; diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js index a5fe0f5ce6..3ce1b62450 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js @@ -49,6 +49,7 @@ $(function () { const searchText = $('#worksheet-name-filter').val().toLowerCase(); const publishedFilter = $('#worksheet-published-toggle .active').data('filter'); + let visibleCount = 0; $('#worksheet-accordion .accordion-item').each(function () { const $item = $(this); const title = $item.find('.worksheet-title').text().toLowerCase(); @@ -66,8 +67,12 @@ $(function () { (publishedFilter === 'unpublished' && !isPublished) ); - $item.toggle(matchesText && matchesFilter); + const visible = matchesText && matchesFilter; + $item.toggle(visible); + if (visible) visibleCount++; }); + + $('#worksheet-no-results').toggle(visibleCount === 0); } $(document).on('input', '#worksheet-name-filter', applyWorksheetFilters); From 7bb50ce6371b107779194a4147cc8b70c8f54f31 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 16 Apr 2026 10:54:02 -0700 Subject: [PATCH 010/134] feature/AB#32632-Worksheets hooked into sonar cloud --- .../UpsertCustomFieldModal.js | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js index 341d446069..abb0f483ab 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js @@ -6,7 +6,7 @@ abp.modals.UpsertCustomFieldModal = function () { 'ProtectedC': 'The most sensitive level — disclosure could cause extremely grave injury.' }; - const placeholderSupportedTypes = ['Text', 'TextArea', 'Numeric', 'Currency', 'Email', 'Phone']; + const placeholderSupportedTypes = new Set(['Text', 'TextArea', 'Numeric', 'Currency', 'Email', 'Phone']); function updateClassificationHint(value) { const hint = document.getElementById('classificationHint'); @@ -15,7 +15,7 @@ abp.modals.UpsertCustomFieldModal = function () { function updatePlaceholderVisibility(type) { const row = document.getElementById('placeholder-row'); - if (row) row.style.display = placeholderSupportedTypes.includes(type) ? '' : 'none'; + if (row) row.style.display = placeholderSupportedTypes.has(type) ? '' : 'none'; } function initModal(modalManager, args) { @@ -58,33 +58,37 @@ abp.modals.UpsertCustomFieldModal = function () { }); }); + function checkPaneErrors(activePaneId, tabButtons, paneId) { + if (paneId === activePaneId) return false; + let pane = document.getElementById(paneId); + let invalid = pane ? pane.querySelector(':invalid') : null; + let btn = document.getElementById(tabButtons[paneId]); + if (!btn) return false; + let badge = btn.querySelector('.tab-error-badge'); + if (invalid) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'tab-error-badge'; + badge.setAttribute('aria-label', 'This tab has errors'); + btn.appendChild(badge); + } + return true; + } + if (badge) badge.remove(); + return false; + } + document.querySelector('[name="saveCustomFieldBtn"]')?.addEventListener('click', function () { setTimeout(function () { - var panes = ['pane-display', 'pane-attributes']; - var tabButtons = { 'pane-display': 'tab-display', 'pane-attributes': 'tab-attributes' }; - var activePane = document.querySelector('#customFieldTabContent .tab-pane.show'); - var activePaneId = activePane ? activePane.id : null; - var hasOffTabErrors = false; + let panes = ['pane-display', 'pane-attributes']; + let tabButtons = { 'pane-display': 'tab-display', 'pane-attributes': 'tab-attributes' }; + let activePane = document.querySelector('#customFieldTabContent .tab-pane.show'); + let activePaneId = activePane ? activePane.id : null; + let hasOffTabErrors = false; - panes.forEach(function (paneId) { - if (paneId === activePaneId) return; - var pane = document.getElementById(paneId); - var invalid = pane ? pane.querySelector(':invalid') : null; - var btn = document.getElementById(tabButtons[paneId]); - if (!btn) return; - var badge = btn.querySelector('.tab-error-badge'); - if (invalid) { - if (!badge) { - badge = document.createElement('span'); - badge.className = 'tab-error-badge'; - badge.setAttribute('aria-label', 'This tab has errors'); - btn.appendChild(badge); - } - hasOffTabErrors = true; - } else { - if (badge) badge.remove(); - } - }); + for (const paneId of panes) { + if (checkPaneErrors(activePaneId, tabButtons, paneId)) hasOffTabErrors = true; + } if (hasOffTabErrors) { abp.notify.warn('There are validation errors on another tab. Please review all tabs before saving.'); @@ -94,7 +98,7 @@ abp.modals.UpsertCustomFieldModal = function () { document.querySelectorAll('#customFieldTabs [data-bs-toggle="tab"]').forEach(function (btn) { btn.addEventListener('shown.bs.tab', function () { - var badge = btn.querySelector('.tab-error-badge'); + let badge = btn.querySelector('.tab-error-badge'); if (badge) badge.remove(); }); }); From d57c0c8ceb4861b6bf48c8bb913c1643bfc335ff Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 17 Apr 2026 10:56:47 -0700 Subject: [PATCH 011/134] AB#32694 update applicant profile contact handling --- .../UpdateApplicantContactAddressesDto.cs | 1 + .../Applicants/ApplicantAppService.cs | 44 ++++++ .../Handlers/ContactCreateHandler.cs | 23 --- .../Handlers/ContactEditHandler.cs | 72 +++++---- .../ApplicantContactsViewComponent.cs | 146 +++++++++++++----- .../ApplicantContactsViewModel.cs | 3 + .../ApplicantContacts/Default.cshtml | 1 + .../Components/ApplicantContacts/Default.js | 9 ++ .../GrantsPortal/ContactCreateHandlerTests.cs | 19 --- 9 files changed, 208 insertions(+), 110 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs index 523fbce1a1..8ddb5b4d81 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/UpdateApplicantContactAddressesDto.cs @@ -12,6 +12,7 @@ public class UpdateApplicantContactAddressesDto public class UpdatePrimaryContactDto { public Guid Id { get; set; } + public string? Source { get; set; } public string? FullName { get; set; } public string? Title { get; set; } public string? Email { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index 73c468aa06..cd7469644c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -262,6 +262,20 @@ private async Task UpdatePrimaryContactAsync(Guid applicantId, UpdatePrimaryCont throw new ArgumentException("Contact identifier is required.", nameof(input)); } + switch (input.Source) + { + case "Contact": + await UpdateLinkedContactAsync(applicantId, input); + break; + case "Agent": + default: + await UpdateAgentContactAsync(applicantId, input); + break; + } + } + + private async Task UpdateAgentContactAsync(Guid applicantId, UpdatePrimaryContactDto input) + { var applicantAgent = await applicantAgentRepository.GetAsync(input.Id); if (applicantAgent.ApplicantId != applicantId) { @@ -279,6 +293,36 @@ private async Task UpdatePrimaryContactAsync(Guid applicantId, UpdatePrimaryCont await applicantAgentRepository.UpdateAsync(applicantAgent); } + private async Task UpdateLinkedContactAsync(Guid applicantId, UpdatePrimaryContactDto input) + { + var contactRepository = LazyServiceProvider.LazyGetRequiredService(); + var contactLinkRepository = LazyServiceProvider.LazyGetRequiredService(); + + var linkQuery = await contactLinkRepository.GetQueryableAsync(); + var link = await linkQuery.FirstOrDefaultAsync(l => + l.ContactId == input.Id + && l.RelatedEntityType == "Applicant" + && l.RelatedEntityId == applicantId + && l.IsActive); + + if (link == null) + { + throw new BusinessException("Unity:Applicant:ContactNotFound") + .WithData("ApplicantId", applicantId) + .WithData("ContactId", input.Id); + } + + var contact = await contactRepository.GetAsync(input.Id); + + contact.Name = input.FullName?.Trim() ?? string.Empty; + contact.Title = input.Title?.Trim(); + contact.Email = input.Email?.Trim(); + contact.WorkPhoneNumber = input.BusinessPhone?.Trim(); + contact.MobilePhoneNumber = input.CellPhone?.Trim(); + + await contactRepository.UpdateAsync(contact); + } + private async Task UpdatePrimaryAddressAsync(Guid applicantId, UpdatePrimaryApplicantAddressDto input, GrantApplications.AddressType expectedType) { if (input.Id == Guid.Empty) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactCreateHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactCreateHandler.cs index 769e4b1135..973a64d8c7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactCreateHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactCreateHandler.cs @@ -83,28 +83,5 @@ public virtual async Task HandleAsync(PluginDataPayload payload) logger.LogInformation("Contact {ContactId} created successfully", contactId); return "Contact created successfully"; - } - - /// - /// Normalizes a raw OIDC subject by stripping the IDP suffix (after @) and uppercasing. - /// This matches the format stored in ApplicationFormSubmission.OidcSub. - /// - internal static string? NormalizeOidcSub(string? subject) - { - if (string.IsNullOrWhiteSpace(subject)) - { - return null; - } - - var atIndex = subject.IndexOf('@'); - - if (atIndex == 0) - { - return null; - } - - return atIndex > 0 - ? subject[..atIndex].ToUpperInvariant() - : subject.ToUpperInvariant(); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactEditHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactEditHandler.cs index 856821d7f7..ba33ecce29 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactEditHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Handlers/ContactEditHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -33,50 +34,55 @@ public virtual async Task HandleAsync(PluginDataPayload payload) logger.LogInformation("Editing contact {ContactId} for profile {ProfileId}", contactId, payload.ProfileId); + await UpdateContactAsync(contactId, innerData); + await SyncContactLinkAsync(contactId, innerData); + + logger.LogInformation("Contact {ContactId} updated successfully", contactId); + return "Contact updated successfully"; + } + + private async Task UpdateContactAsync(Guid contactId, ContactEditData data) + { var contact = await contactRepository.GetAsync(contactId); - contact.Name = innerData.Name; - contact.Email = innerData.Email; - contact.Title = innerData.Title; - contact.HomePhoneNumber = innerData.HomePhoneNumber; - contact.MobilePhoneNumber = innerData.MobilePhoneNumber; - contact.WorkPhoneNumber = innerData.WorkPhoneNumber; - contact.WorkPhoneExtension = innerData.WorkPhoneExtension; + contact.Name = data.Name; + contact.Email = data.Email; + contact.Title = data.Title; + contact.HomePhoneNumber = data.HomePhoneNumber; + contact.MobilePhoneNumber = data.MobilePhoneNumber; + contact.WorkPhoneNumber = data.WorkPhoneNumber; + contact.WorkPhoneExtension = data.WorkPhoneExtension; - // Sync contact-link primary flags to match the incoming value + await contactRepository.UpdateAsync(contact); + } + + private async Task SyncContactLinkAsync(Guid contactId, ContactEditData data) + { var contactLinks = await contactLinkRepository.GetListAsync( cl => cl.RelatedEntityType == ApplicantEntityType - && cl.RelatedEntityId == innerData.ApplicantId + && cl.RelatedEntityId == data.ApplicantId && cl.IsActive); - if (innerData.IsPrimary) + if (data.IsPrimary) { - foreach (var stale in contactLinks.Where(cl => cl.IsPrimary && cl.ContactId != contactId)) - { - stale.IsPrimary = false; - await contactLinkRepository.UpdateAsync(stale); - } - - var newPrimary = contactLinks.FirstOrDefault(cl => cl.ContactId == contactId && !cl.IsPrimary); - if (newPrimary != null) - { - newPrimary.IsPrimary = true; - await contactLinkRepository.UpdateAsync(newPrimary); - } + await DemoteOtherPrimaryLinksAsync(contactLinks, contactId); } - else + + var targetLink = contactLinks.FirstOrDefault(cl => cl.ContactId == contactId); + if (targetLink != null) { - var demoted = contactLinks.FirstOrDefault(cl => cl.ContactId == contactId && cl.IsPrimary); - if (demoted != null) - { - demoted.IsPrimary = false; - await contactLinkRepository.UpdateAsync(demoted); - } + targetLink.IsPrimary = data.IsPrimary; + targetLink.Role = data.Role; + await contactLinkRepository.UpdateAsync(targetLink); } + } - await contactRepository.UpdateAsync(contact); - - logger.LogInformation("Contact {ContactId} updated successfully", contactId); - return "Contact updated successfully"; + private async Task DemoteOtherPrimaryLinksAsync(List contactLinks, Guid contactId) + { + foreach (var stale in contactLinks.Where(cl => cl.IsPrimary && cl.ContactId != contactId)) + { + stale.IsPrimary = false; + await contactLinkRepository.UpdateAsync(stale); + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs index 1c529a9f1d..6300519eca 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Unity.GrantManager.Applications; +using Unity.GrantManager.Contacts; using Unity.Modules.Shared; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -20,18 +22,26 @@ namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantContacts AutoInitialize = true)] public class ApplicantContactsViewComponent : AbpViewComponent { + private const string ApplicantEntityType = "Applicant"; + private readonly IApplicantAgentRepository _applicantAgentRepository; private readonly IPermissionChecker _permissionChecker; private readonly IRepository _applicationRepository; + private readonly IContactRepository _contactRepository; + private readonly IContactLinkRepository _contactLinkRepository; public ApplicantContactsViewComponent( IApplicantAgentRepository applicantAgentRepository, IPermissionChecker permissionChecker, - IRepository applicationRepository) + IRepository applicationRepository, + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository) { _applicantAgentRepository = applicantAgentRepository; _permissionChecker = permissionChecker; _applicationRepository = applicationRepository; + _contactRepository = contactRepository; + _contactLinkRepository = contactLinkRepository; } public async Task InvokeAsync(Guid applicantId) @@ -42,62 +52,128 @@ public async Task InvokeAsync(Guid applicantId) } var agents = await _applicantAgentRepository.GetListByApplicantIdAsync(applicantId); - var orderedAgents = agents .OrderByDescending(a => a.LastModificationTime ?? a.CreationTime) .ToList(); - var appIds = new HashSet( - orderedAgents.Where(a => a.ApplicationId.HasValue).Select(a => a.ApplicationId!.Value)); + var appRefMap = await BuildApplicationReferenceMapAsync(orderedAgents); + var linkedContacts = await GetLinkedContactsAsync(applicantId); + var agentContacts = MapAgentContacts(orderedAgents, appRefMap); - var appRefMap = new Dictionary(); - if (appIds.Count > 0) - { - var apps = await _applicationRepository.GetListAsync(a => appIds.Contains(a.Id)); - foreach (var app in apps) - { - appRefMap[app.Id] = app.ReferenceNo; - } - } + var allContacts = agentContacts.Concat(linkedContacts) + .OrderByDescending(c => c.CreationTime) + .ToList(); + + ResolvePrimaryContact(allContacts); var viewModel = new ApplicantContactsViewModel { ApplicantId = applicantId, CanEditContact = await _permissionChecker.IsGrantedAsync(UnitySelector.Applicant.Contact.Update), - Contacts = orderedAgents - .Select((agent, index) => new ApplicantContactItemDto - { - Id = agent.Id, - Name = agent.Name ?? string.Empty, - Email = agent.Email ?? string.Empty, - Phone = !string.IsNullOrWhiteSpace(agent.Phone) - ? agent.Phone! - : agent.Phone2 ?? string.Empty, - Title = agent.Title ?? string.Empty, - Type = index == 0 ? "Primary" : "", - CreationTime = agent.CreationTime, - ApplicationId = agent.ApplicationId, - ReferenceNo = agent.ApplicationId.HasValue ? appRefMap.GetValueOrDefault(agent.ApplicationId.Value, string.Empty) : string.Empty - }) - .ToList() + Contacts = allContacts }; - var primaryContact = orderedAgents.FirstOrDefault(); + var primaryContact = allContacts.FirstOrDefault(c => c.IsPrimary); if (primaryContact != null) { viewModel.PrimaryContact = new ApplicantPrimaryContactViewModel { Id = primaryContact.Id, - FullName = primaryContact.Name ?? string.Empty, - Title = primaryContact.Title ?? string.Empty, - Email = primaryContact.Email ?? string.Empty, - BusinessPhone = primaryContact.Phone ?? string.Empty, - CellPhone = primaryContact.Phone2 ?? string.Empty + Source = primaryContact.Source, + FullName = primaryContact.Name, + Title = primaryContact.Title, + Email = primaryContact.Email, + BusinessPhone = primaryContact.Phone, + CellPhone = string.Empty }; } return View(viewModel); } + + private async Task> BuildApplicationReferenceMapAsync(List agents) + { + var appIds = new HashSet( + agents.Where(a => a.ApplicationId.HasValue).Select(a => a.ApplicationId!.Value)); + + var appRefMap = new Dictionary(); + if (appIds.Count > 0) + { + var apps = await _applicationRepository.GetListAsync(a => appIds.Contains(a.Id)); + foreach (var app in apps) + { + appRefMap[app.Id] = app.ReferenceNo; + } + } + + return appRefMap; + } + + private static List MapAgentContacts( + List agents, + Dictionary appRefMap) + { + return agents + .Select(agent => new ApplicantContactItemDto + { + Id = agent.Id, + Name = agent.Name ?? string.Empty, + Email = agent.Email ?? string.Empty, + Phone = !string.IsNullOrWhiteSpace(agent.Phone) + ? agent.Phone! + : agent.Phone2 ?? string.Empty, + Title = agent.Title ?? string.Empty, + Type = string.Empty, + Source = "Agent", + IsPrimary = false, + CreationTime = agent.CreationTime, + ApplicationId = agent.ApplicationId, + ReferenceNo = agent.ApplicationId.HasValue + ? appRefMap.GetValueOrDefault(agent.ApplicationId.Value, string.Empty) + : string.Empty + }) + .ToList(); + } + + private static void ResolvePrimaryContact(List contacts) + { + var primaryContact = contacts.FirstOrDefault(c => c.IsPrimary) + ?? contacts.FirstOrDefault(); + + if (primaryContact != null) + { + primaryContact.IsPrimary = true; + } + } + + private async Task> GetLinkedContactsAsync(Guid applicantId) + { + var contactLinksQuery = await _contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await _contactRepository.GetQueryableAsync(); + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == ApplicantEntityType + && link.RelatedEntityId == applicantId + && link.IsActive + select new ApplicantContactItemDto + { + Id = contact.Id, + Name = contact.Name, + Email = contact.Email ?? string.Empty, + Phone = !string.IsNullOrWhiteSpace(contact.WorkPhoneNumber) + ? contact.WorkPhoneNumber! + : contact.MobilePhoneNumber ?? string.Empty, + Title = contact.Title ?? string.Empty, + Type = link.Role ?? string.Empty, + Source = "Contact", + IsPrimary = link.IsPrimary, + CreationTime = contact.CreationTime, + ApplicationId = null, + ReferenceNo = string.Empty + }).ToListAsync(); + } } public class ApplicantContactsStyleBundleContributor : BundleContributor diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs index 995f62aaef..74bb3b2916 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs @@ -16,6 +16,7 @@ public class ApplicantContactsViewModel public class ApplicantPrimaryContactViewModel { public Guid Id { get; set; } + public string Source { get; set; } = string.Empty; [Display(Name = "Full Name")] public string FullName { get; set; } = string.Empty; [Display(Name = "Title")] @@ -37,6 +38,8 @@ public class ApplicantContactItemDto public string Phone { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; + public string Source { get; set; } = string.Empty; + public bool IsPrimary { get; set; } public DateTime CreationTime { get; set; } public string ReferenceNo { get; set; } = string.Empty; public Guid? ApplicationId { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml index cc1620fa2d..f29c93bec3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml @@ -19,6 +19,7 @@
+ @if (Model.CanSave) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js index de2d963e45..c784527fee 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js @@ -80,6 +80,12 @@ $(function () { width: '10%', render: (data) => data || nullPlaceholder }, + { + title: 'Primary', + data: 'isPrimary', + width: '8%', + render: (data) => data ? 'Yes' : '' + }, { title: 'Submission #', data: 'referenceNo', @@ -129,9 +135,12 @@ $(function () { return; } + const contactSource = $('#ApplicantContacts_PrimaryContactSource').val(); + const payload = { primaryContact: { id: contactId, + source: contactSource, fullName: form.find('[name="PrimaryContact.FullName"]').val(), title: form.find('[name="PrimaryContact.Title"]').val(), email: form.find('[name="PrimaryContact.Email"]').val(), diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantsPortal/ContactCreateHandlerTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantsPortal/ContactCreateHandlerTests.cs index 852816cc56..28b0ce3b8a 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantsPortal/ContactCreateHandlerTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantsPortal/ContactCreateHandlerTests.cs @@ -277,23 +277,4 @@ public async Task HandleAsync_WhenDataMissing_ShouldThrow() } #endregion - - #region NormalizeOidcSub - - [Theory] - [InlineData("testuser@idir", "TESTUSER")] - [InlineData("abc@bceidbusiness", "ABC")] - [InlineData("ALREADY", "ALREADY")] - [InlineData("mixedCase", "MIXEDCASE")] - [InlineData("user@", "USER")] - [InlineData(null, null)] - [InlineData("", null)] - [InlineData(" ", null)] - [InlineData("@idir", null)] - public void NormalizeOidcSub_ShouldStripIdpSuffixAndUppercase(string? input, string? expected) - { - ContactCreateHandler.NormalizeOidcSub(input).ShouldBe(expected); } - - #endregion -} From 326d07894f4efbe1dd3ab99df4b5282d28963b60 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 17 Apr 2026 14:50:50 -0700 Subject: [PATCH 012/134] feature/AB#32634-Enhance email scheduling --- .../Views/Shared/Components/WorksheetListWidget/Default.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/Default.cshtml index 7d30af67aa..0adee5e422 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/Default.cshtml @@ -25,7 +25,7 @@ @if (worksheet.IsArchived) { - Archived + }
From 838a544f47f57fbbf30b68ae1e239a600eed0e15 Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Fri, 17 Apr 2026 17:11:24 -0700 Subject: [PATCH 013/134] AB#32704 - Fixed Scoring Generate button. --- .../ApplicationScoringAppService.cs | 27 +++++++++++++++++-- .../ApplicationAIScoringGeneratedEvent.cs | 0 .../GenerateApplicationScoringJob.cs | 12 +-------- 3 files changed, 26 insertions(+), 13 deletions(-) rename applications/Unity.GrantManager/src/{Unity.GrantManager.Application => Unity.GrantManager.Application.Contracts}/GrantApplications/Automation/Events/ApplicationAIScoringGeneratedEvent.cs (100%) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs index 329a1ee2c4..753a622148 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs @@ -4,7 +4,9 @@ using Unity.AI; using Unity.AI.Operations; using Unity.AI.Permissions; +using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp; +using Volo.Abp.EventBus.Local; using Volo.Abp.Features; namespace Unity.GrantManager.GrantApplications; @@ -12,10 +14,11 @@ namespace Unity.GrantManager.GrantApplications; [Authorize(AIPermissions.Analysis.GenerateScoring)] public class ApplicationScoringAppService( IApplicationScoringService applicationScoringService, - IFeatureChecker featureChecker) + IFeatureChecker featureChecker, + ILocalEventBus localEventBus) : AIAppService, IApplicationScoringAppService { - public async Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null) + public virtual async Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null) { if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) { @@ -23,6 +26,26 @@ public async Task GenerateApplicationScoringAsync(G } await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion); + + if (UnitOfWorkManager.Current != null) + { + var capturedId = applicationId; + UnitOfWorkManager.Current.OnCompleted(async () => + { + await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent + { + ApplicationId = capturedId + }); + }); + } + else + { + await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent + { + ApplicationId = applicationId + }); + } + return new ApplicationScoringResultDto { Completed = true diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Events/ApplicationAIScoringGeneratedEvent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/Events/ApplicationAIScoringGeneratedEvent.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Events/ApplicationAIScoringGeneratedEvent.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/Events/ApplicationAIScoringGeneratedEvent.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs index 61d87d63a7..d65a56e8d2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -1,15 +1,12 @@ using Microsoft.Extensions.Logging; using System.Threading.Tasks; using Unity.GrantManager.GrantApplications; -using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Local; using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; public class GenerateApplicationScoringJob( IApplicationScoringAppService applicationScoringService, - ILocalEventBus localEventBus, ICurrentTenant currentTenant, ILogger logger) : AsyncBackgroundJob, ITransientDependency { @@ -18,14 +15,7 @@ public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobA using (currentTenant.Change(args.TenantId)) { logger.LogInformation("Executing AI application scoring job for application {ApplicationId}.", args.ApplicationId); - var result = await applicationScoringService.GenerateApplicationScoringAsync(args.ApplicationId, args.PromptVersion); - if (result.Completed) - { - await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent - { - ApplicationId = args.ApplicationId - }); - } + await applicationScoringService.GenerateApplicationScoringAsync(args.ApplicationId, args.PromptVersion); } } } From f3eb53d30d26508396511411c665bf67a2e65abd Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Fri, 17 Apr 2026 17:25:01 -0700 Subject: [PATCH 014/134] AB#32704 - Added spinner --- .../Components/ReviewList/ReviewList.js | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 61c45c5262..7727bbedeb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -1,6 +1,6 @@ const l = abp.localization.getResource('GrantManager'); const pageApplicationId = decodeURIComponent(document.querySelector("#DetailsViewApplicationId").value); -const isAiScoringEnabled = document.querySelector("#ReviewListAIScoringEnabled")?.value === 'True'; +const isAiScoringEnabled = document.querySelector("#ReviewListAIScoringEnabled")?.value === 'True'; const canUseAiScoring = isAiScoringEnabled; const actionButtonConfigMap = { @@ -28,7 +28,7 @@ const finalApplicationStates = [ ]; $(function () { - const nullPlaceholder = '—'; + const nullPlaceholder = '—'; let inputAction = function (requestData, dataTableSettings) { const applicationId = pageApplicationId @@ -76,7 +76,7 @@ $(function () { } }); - $.fn.dataTable.Api.register('row().selectWithParams()', function (params) { + $.fn.dataTable.Api.register('row().selectWithParams()', function (params) { this.params = params; return this.select(); }); @@ -255,7 +255,7 @@ $(function () { $("#AdjudicationTeamLeadActionBar .dt-buttons").contents().unwrap(); updateAiActionButtonsVisibility(reviewListTable); - reviewListTable.on('select', function (e, dt, type, indexes) { + reviewListTable.on('select', function (e, dt, type, indexes) { handleRowSelection(e, dt, type, indexes, reviewListTable); }); @@ -294,7 +294,7 @@ function handleRowSelection(e, dt, type, indexes, reviewListTable) { if (type === 'row') { let selectedData = reviewListTable.row(indexes).data(); document.getElementById("AssessmentId").value = selectedData.id; - if (refreshSidePanel) { + if (refreshSidePanel) { PubSub.publish('select_application_review', selectedData); PubSub.publish('refresh_assessment_attachment_list', selectedData.id); } @@ -456,13 +456,11 @@ function unityWorkflowButtonAction(e, dt, button, config) { } function generateAiButtonAction(e, dt, button, config) { - const triggerButton = button?.node ? $(button.node) : null; + const $btn = $(this.node()); const promptVersion = globalThis.getSelectedPromptVersion?.() || null; - if (triggerButton?.length) { - triggerButton.prop('disabled', true); - triggerButton.html('Queueing...'); - } + this.disable(); + $btn.html('Queueing...'); unity.grantManager.grantApplications.applicationScoring.generateApplicationScoring(pageApplicationId, promptVersion) .done(function () { @@ -471,11 +469,9 @@ function generateAiButtonAction(e, dt, button, config) { .fail(function () { abp.message.error('Failed to queue AI scoring. Please try again.'); }) - .always(function () { - if (triggerButton?.length) { - triggerButton.prop('disabled', false); - triggerButton.html(generateAiButtonText(null, null, null)); - } + .always(() => { + this.enable(); + $btn.html(generateAiButtonText(null, null, null)); }); } From 01321bc5aec28c990c0ada29bb743229b50f6ff6 Mon Sep 17 00:00:00 2001 From: Patrick <135162612+plavoie-BC@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:55:49 -0700 Subject: [PATCH 015/134] [AB#31682] Adjust minimum height for DataTable scroll body --- .../src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index f897b99dda..8bc5385e7c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -692,7 +692,7 @@ function createNumberFormatter() { */ function addDataTableFixCSS() { if (!$('#dt-column-fix-css').length) { - $('').appendTo('head'); + $('').appendTo('head'); } } From dce545b5a983678c718021bc2b3c9ecb188d004b Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 20 Apr 2026 13:20:11 -0700 Subject: [PATCH 016/134] AB#31194 improvements and bugfixes for reporting configuration --- .../WorksheetFieldSchemaParser.cs | 37 +- .../WorksheetFieldSchemaParserTests.cs | 55 +- .../ReportingConfiguration/Default.js | 21 +- ...rksheetViewGenCurrencyCommaFix.Designer.cs | 4947 +++++++++++++++++ ..._UpdateWorksheetViewGenCurrencyCommaFix.cs | 31 + ...ToJsonbAndGuardDataGridLateral.Designer.cs | 4947 +++++++++++++++++ ...2_AddSafeToJsonbAndGuardDataGridLateral.cs | 36 + .../Scripts/get_worksheet_data.sql | 10 +- .../Scripts/safe_to_jsonb.sql | 17 + ...ty.GrantManager.EntityFrameworkCore.csproj | 4 + .../get_worksheet_data_specification.md | 11 +- 11 files changed, 10075 insertions(+), 41 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/safe_to_jsonb.sql diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs index 1e4ab34f13..011b97353d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Reporting/Configuration/WorksheetFieldSchemaParser.cs @@ -121,26 +121,21 @@ private static List ParseDataGridField(Custom var worksheetName = SanitizeName(worksheet.Name); var dataGridName = SanitizeName(field.Key); - // Track whether we successfully extracted columns from CHEFS schema - bool extractedFromChefs = false; - // If dynamic is true, try to extract columns from form schema if (dataGridDefinition.Dynamic) { var headerMappingKey = MatchHeaderMapping(field.Name + ".DataGrid", submissionHeaderMapping); List? dynamicColumns = null; - + if (!string.IsNullOrWhiteSpace(headerMappingKey)) { dynamicColumns = ExtractDynamicDataGridColumns(headerMappingKey, formSchema); } - + if (dynamicColumns != null && dynamicColumns.Count > 0) { - // We found columns in the form schema, use them - // CHEFS schema includes ALL columns (both static and dynamic), so we mark this as extracted - extractedFromChefs = true; - + // We found dynamic columns in the CHEFS form schema; emit them first. + // Any statically-defined columns on the DataGrid are merged in below. foreach (var column in dynamicColumns) { // Use the key for the component Key (becomes PropertyName), sanitize for ID @@ -179,16 +174,25 @@ private static List ParseDataGridField(Custom } } - // Process additional defined columns only if we haven't already extracted them from CHEFS - // When dynamic is true and CHEFS extraction succeeded, the CHEFS schema already includes - // all columns (both static and dynamic), so we skip this to avoid duplicates - if (!extractedFromChefs && dataGridDefinition.Columns != null && dataGridDefinition.Columns.Count > 0) + // Process additional defined columns from the DataGrid definition. + // For mixed grids (Dynamic == true), CHEFS only returns the dynamic wide columns and omits + // statically-defined ones, so we must merge them in here (dynamic wins on key collision). + if (dataGridDefinition.Columns != null && dataGridDefinition.Columns.Count > 0) { - // Create a component for each column in the DataGrid + var existingKeys = new HashSet( + components.Select(c => c.Key ?? string.Empty), + StringComparer.OrdinalIgnoreCase); + foreach (var column in dataGridDefinition.Columns) { + // Skip columns that were already emitted from the CHEFS extraction + if (existingKeys.Contains(column.Name)) + { + continue; + } + var columnName = SanitizeName(column.Name); - + var component = new WorksheetComponentMetaDataItemDto { Id = $"{field.Id}_{columnName}", @@ -199,8 +203,9 @@ private static List ParseDataGridField(Custom TypePath = $"worksheet->section->datagrid->{MapDataGridColumnType(column.Type)}", DataPath = $"({worksheetName}){dataGridName}->{column.Name}" }; - + components.Add(component); + existingKeys.Add(column.Name); } } else if (!dataGridDefinition.Dynamic && (dataGridDefinition.Columns == null || dataGridDefinition.Columns.Count == 0)) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs index 72ca6ade85..c6a31f58d4 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/WorksheetFieldSchemaParserTests.cs @@ -226,7 +226,7 @@ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldExtractColumnsF } [Fact] - public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColumnsWhenChefsExtracted() + public async Task ParseDataGridField_DynamicWithFormSchema_ShouldMergeDefinedColumnsWithChefsExtracted() { // Arrange using var uow = _unitOfWorkManager.Begin(); @@ -238,7 +238,7 @@ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColu await _worksheetRepository.InsertAsync(worksheet, true); await uow.SaveChangesAsync(); - // Definition has both dynamic=true AND static columns defined + // Definition has both dynamic=true AND static columns defined (mixed grid scenario) var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid", CustomFieldType.DataGrid, @"{""dynamic"": true, ""columns"": [{""name"": ""staticCol"", ""type"": ""Text""}], ""summaryOption"": ""None""}"); @@ -264,16 +264,19 @@ public async Task ParseDataGridField_DynamicWithFormSchema_ShouldSkipDefinedColu // Act var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping); - // Assert — CHEFS extraction succeeded, so static columns should be skipped to avoid duplicates + // Assert — CHEFS only returns dynamic columns; statically-defined columns must still be emitted result.ShouldNotBeNull(); - result.Count.ShouldBe(1); - result.ShouldNotContain(c => c.Key == "staticCol"); + result.Count.ShouldBe(2); result.ShouldNotContain(c => c.Key == "dynamic_columns"); - var dynamicCol = result.First(); - dynamicCol.Key.ShouldBe("dynamicCol"); + var dynamicCol = result.FirstOrDefault(c => c.Key == "dynamicCol"); + dynamicCol.ShouldNotBeNull(); dynamicCol.Label.ShouldBe("Dynamic Column"); dynamicCol.Type.ShouldBe("Text"); + + var staticCol = result.FirstOrDefault(c => c.Key == "staticCol"); + staticCol.ShouldNotBeNull(); + staticCol.Type.ShouldBe("Text"); } [Fact] @@ -312,6 +315,44 @@ public async Task ParseDataGridField_DynamicWithNoHeaderMapping_ShouldFallBackTo result.ShouldContain(c => c.Key == "col1"); } + [Fact] + public async Task ParseDataGridField_DynamicOnlyWithNoHeaderMapping_ShouldReturnOnlyPlaceholder() + { + // Arrange + using var uow = _unitOfWorkManager.Begin(); + + var worksheet = new Worksheet(Guid.NewGuid(), "TestWorksheet", "Test Worksheet"); + var section = new WorksheetSection(Guid.NewGuid(), "TestSection"); + worksheet.Sections.Add(section); + + await _worksheetRepository.InsertAsync(worksheet, true); + await uow.SaveChangesAsync(); + + // Purely dynamic grid: dynamic=true and no static columns defined + var field = new CustomField(Guid.NewGuid(), "testDataGrid", "TestWorksheet", "Test DataGrid", + CustomFieldType.DataGrid, + @"{""dynamic"": true, ""columns"": [], ""summaryOption"": ""None""}"); + section.AddField(field); + await uow.SaveChangesAsync(); + + worksheet = await _worksheetRepository.GetAsync(worksheet.Id); + + // Header mapping does NOT contain an entry for this field, so CHEFS extraction cannot resolve + var submissionHeaderMapping = @"{""unrelated_key.DataGrid"": ""someGrid""}"; + var formSchema = @"{ ""components"": [] }"; + + // Act + var result = WorksheetFieldSchemaParser.ParseField(field, worksheet, formSchema, submissionHeaderMapping); + + // Assert — dynamic-only with no CHEFS resolution should yield exactly the placeholder + result.ShouldNotBeNull(); + result.Count.ShouldBe(1); + + var placeholder = result.First(); + placeholder.Key.ShouldBe("dynamic_columns"); + placeholder.Type.ShouldBe("Dynamic"); + } + [Fact] public async Task ParseDataGridField_DynamicWithNestedFormSchema_ShouldFindDataGridInPanel() { diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js index bd0d4be183..5264bc80b2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js @@ -792,18 +792,25 @@ $(function () { if (newValue !== oldValue) { $input.val(newValue); - - // Clear any previous validation state - $input.removeClass('is-valid is-invalid'); - $input.siblings('.invalid-feedback, .valid-feedback').remove(); - - // Validate the new value - validateColumnNameInput($input, newValue, path); updatedCount++; } } }); + // Re-validate every input — uniqueness is a cross-row property, so rows whose + // value did not change can still transition from invalid to valid (and vice versa) + // once the rest of the table has been rewritten. + $('#ReportConfigurationTable .column-name-input').each(function () { + const $input = $(this); + const path = $input.data('path'); + const value = $input.val().trim(); + + $input.removeClass('is-valid is-invalid'); + $input.siblings('.invalid-feedback, .valid-feedback').remove(); + + validateColumnNameInput($input, value, path); + }); + if (updatedCount > 0) { markAsChanged(); abp.message.success(`Successfully generated unique column names. ${updatedCount} column name(s) were updated.`); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs new file mode 100644 index 0000000000..e11d04db19 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs @@ -0,0 +1,4947 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260416000001_UpdateWorksheetViewGenCurrencyCommaFix")] + partial class UpdateWorksheetViewGenCurrencyCommaFix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs new file mode 100644 index 0000000000..30ff6fe7b9 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.IO; +using System.Reflection; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class UpdateWorksheetViewGenCurrencyCommaFix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "Unity.GrantManager.Scripts.get_worksheet_data.sql"; + + using Stream stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}"); + using StreamReader reader = new(stream); + string sql = reader.ReadToEnd(); + migrationBuilder.Sql(sql); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.Designer.cs new file mode 100644 index 0000000000..7ae4387faf --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.Designer.cs @@ -0,0 +1,4947 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + [DbContext(typeof(GrantTenantDbContext))] + [Migration("20260416000002_AddSafeToJsonbAndGuardDataGridLateral")] + partial class AddSafeToJsonbAndGuardDataGridLateral + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("QuestionId") + .HasColumnType("uuid"); + + b.Property("ScoresheetInstanceId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.HasIndex("ScoresheetInstanceId"); + + b.ToTable("Answers", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("Questions", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Scoresheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ScoresheetId"); + + b.ToTable("ScoresheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CustomFieldId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetInstanceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetInstanceId"); + + b.ToTable("CustomFieldValues", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("CurrentValue") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetCorrelationId") + .HasColumnType("uuid"); + + b.Property("WorksheetCorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("WorksheetInstances", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UiAnchor") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetLinks", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Definition") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("SectionId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("CustomFields", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Worksheets", "Flex"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("WorksheetId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorksheetId"); + + b.ToTable("WorksheetSections", "Flex"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("ApproxNumberOfEmployees") + .HasColumnType("text"); + + b.Property("AuditComments") + .HasColumnType("text"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FiscalDay") + .HasColumnType("integer"); + + b.Property("FiscalMonth") + .HasColumnType("text"); + + b.Property("FundingHistoryComments") + .HasColumnType("text"); + + b.Property("IndigenousOrgInd") + .HasColumnType("text"); + + b.Property("IsDuplicated") + .HasColumnType("boolean"); + + b.Property("IssueTrackingComments") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MatchPercentage") + .HasColumnType("numeric"); + + b.Property("NonRegOrgName") + .HasColumnType("text"); + + b.Property("NonRegisteredBusinessName") + .HasColumnType("text"); + + b.Property("OrgName") + .HasColumnType("text"); + + b.Property("OrgNumber") + .HasColumnType("text"); + + b.Property("OrgStatus") + .HasColumnType("text"); + + b.Property("OrganizationSize") + .HasColumnType("text"); + + b.Property("OrganizationType") + .HasColumnType("text"); + + b.Property("RedStop") + .HasColumnType("boolean"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("StartedOperatingDate") + .HasColumnType("date"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SubSector") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UnityApplicantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantName"); + + b.ToTable("Applicants", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AddressType") + .HasColumnType("integer"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Postal") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("Street") + .HasColumnType("text"); + + b.Property("Street2") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Unit") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicantAddresses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("BceidBusinessGuid") + .HasColumnType("uuid"); + + b.Property("BceidBusinessName") + .HasColumnType("text"); + + b.Property("BceidUserGuid") + .HasColumnType("uuid"); + + b.Property("BceidUserName") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactOrder") + .HasColumnType("integer"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IdentityEmail") + .HasColumnType("text"); + + b.Property("IdentityName") + .HasColumnType("text"); + + b.Property("IdentityProvider") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsConfirmed") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSubUser") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Phone2") + .HasColumnType("text"); + + b.Property("Phone2Extension") + .HasColumnType("text"); + + b.Property("PhoneExtension") + .HasColumnType("text"); + + b.Property("RoleForApplicant") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationId") + .IsUnique(); + + b.ToTable("ApplicantAgents", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ApplicantAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AIAnalysis") + .HasColumnType("text"); + + b.Property("AIScoresheetAnswers") + .HasColumnType("jsonb"); + + b.Property("Acquisition") + .HasColumnType("text"); + + b.Property("ApplicantElectoralDistrict") + .HasColumnType("text"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationStatusId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("AssessmentResultDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AssessmentResultStatus") + .HasColumnType("text"); + + b.Property("AssessmentStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Community") + .HasColumnType("text"); + + b.Property("CommunityPopulation") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractExecutionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ContractNumber") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeclineRational") + .HasColumnType("text"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("DueDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DueDiligenceStatus") + .HasColumnType("text"); + + b.Property("EconomicRegion") + .HasColumnType("text"); + + b.Property("ElectoralDistrict") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinalDecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Forestry") + .HasColumnType("text"); + + b.Property("ForestryFocus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LikelihoodOfFunding") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("NotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Payload") + .HasColumnType("jsonb"); + + b.Property("PercentageTotalProjectBudget") + .HasColumnType("double precision"); + + b.Property("Place") + .HasColumnType("text"); + + b.Property("ProjectEndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectFundingTotal") + .HasColumnType("numeric"); + + b.Property("ProjectName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectStartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ProjectSummary") + .HasColumnType("text"); + + b.Property("ProposalDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecommendedAmount") + .HasColumnType("numeric"); + + b.Property("ReferenceNo") + .IsRequired() + .HasColumnType("text"); + + b.Property("RegionalDistrict") + .HasColumnType("text"); + + b.Property("RequestedAmount") + .HasColumnType("numeric"); + + b.Property("RiskRanking") + .HasColumnType("text"); + + b.Property("SigningAuthorityBusinessPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityCellPhone") + .HasColumnType("text"); + + b.Property("SigningAuthorityEmail") + .HasColumnType("text"); + + b.Property("SigningAuthorityFullName") + .HasColumnType("text"); + + b.Property("SigningAuthorityTitle") + .HasColumnType("text"); + + b.Property("SubStatus") + .HasColumnType("text"); + + b.Property("SubmissionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalProjectBudget") + .HasColumnType("numeric"); + + b.Property("TotalScore") + .HasColumnType("integer"); + + b.Property("UnityApplicationId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.HasIndex("ApplicationStatusId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Applications", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Duty") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssigneeId"); + + b.ToTable("ApplicationAssignments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AISummary") + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsFileId") + .HasColumnType("text"); + + b.Property("ChefsSubmissionId") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationChefsFileAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactEmail") + .HasColumnType("text"); + + b.Property("ContactFullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactMobilePhone") + .HasColumnType("text"); + + b.Property("ContactTitle") + .HasColumnType("text"); + + b.Property("ContactType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContactWorkPhone") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationContact", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("ApplicationFormDescription") + .HasColumnType("text"); + + b.Property("ApplicationFormName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AttemptedConnectionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AutomaticallyGenerateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("Category") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsCriteriaFormGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ConnectionHttpStatus") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultPaymentGroup") + .HasColumnType("integer"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ElectoralDistrictAddressType") + .HasColumnType("integer"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormHierarchy") + .HasColumnType("integer"); + + b.Property("IntakeId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsDirectApproval") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ManuallyInitiateAIAnalysis") + .HasColumnType("boolean"); + + b.Property("ParentFormId") + .HasColumnType("uuid"); + + b.Property("Payable") + .HasColumnType("boolean"); + + b.Property("PaymentApprovalThreshold") + .HasColumnType("numeric"); + + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PreventPayment") + .HasColumnType("boolean"); + + b.Property("RenderFormIoToHtml") + .HasColumnType("boolean"); + + b.Property("ScoresheetId") + .HasColumnType("uuid"); + + b.Property("SuffixType") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IntakeId"); + + b.HasIndex("ParentFormId"); + + b.ToTable("ApplicationForms", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("ApplicationFormVersionId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ChefsSubmissionGuid") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormVersionId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("RenderedHTML") + .HasColumnType("text"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Submission") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormSubmissions", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationFormId") + .HasColumnType("uuid"); + + b.Property("AvailableChefsFields") + .HasColumnType("text"); + + b.Property("ChefsApplicationFormGuid") + .HasColumnType("text"); + + b.Property("ChefsFormVersionGuid") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FormSchema") + .HasColumnType("jsonb"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Published") + .HasColumnType("boolean"); + + b.Property("ReportColumns") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportKeys") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubmissionHeaderMapping") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationFormId"); + + b.ToTable("ApplicationFormVersion", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LinkType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Related"); + + b.Property("LinkedApplicationId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.ToTable("ApplicationLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StatusCode") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("StatusCode") + .IsUnique(); + + b.ToTable("ApplicationStatuses", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("TagId"); + + b.ToTable("ApplicationTags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.ToTable("AssessmentAttachments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("AuditDate") + .HasColumnType("timestamp without time zone"); + + b.Property("AuditNote") + .HasColumnType("text"); + + b.Property("AuditTrackingNumber") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("AuditHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApprovedAmount") + .HasColumnType("numeric"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FundingNotes") + .HasColumnType("text"); + + b.Property("FundingYear") + .HasColumnType("text"); + + b.Property("GrantCategory") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OneTimeConsideration") + .HasColumnType("numeric"); + + b.Property("ReconsiderationAmount") + .HasColumnType("numeric"); + + b.Property("RenewedFunding") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TotalGrantAmount") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("FundingHistories", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IssueDescription") + .HasColumnType("text"); + + b.Property("IssueHeading") + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("ResolutionNote") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Year") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("IssueTrackings", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ApprovalRecommended") + .HasColumnType("boolean"); + + b.Property("AssessorId") + .HasColumnType("uuid"); + + b.Property("CleanGrowth") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("EconomicImpact") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FinancialAnalysis") + .HasColumnType("integer"); + + b.Property("InclusiveGrowth") + .HasColumnType("integer"); + + b.Property("IsAiAssessment") + .HasColumnType("boolean"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("AssessorId"); + + b.ToTable("Assessments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicantComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("CommenterId"); + + b.ToTable("ApplicationComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CommenterId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PinDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AssessmentId"); + + b.HasIndex("CommenterId"); + + b.ToTable("AssessmentComments", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("HomePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MobilePhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("WorkPhoneExtension") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("WorkPhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.ToTable("Contacts", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("RelatedEntityId") + .HasColumnType("uuid"); + + b.Property("RelatedEntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Role") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RelatedEntityType", "RelatedEntityId"); + + b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); + + b.ToTable("ContactLinks", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Tags", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Badge") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OidcDisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OidcSub") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("OidcSub"); + + b.ToTable("Persons", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Budget") + .HasColumnType("double precision"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EndDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IntakeName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("StartDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Intakes", (string)null); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("EmailGroupUsers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicantId") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AssessmentId") + .HasColumnType("uuid"); + + b.Property("BCC") + .IsRequired() + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CC") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesHttpStatusCode") + .HasColumnType("text"); + + b.Property("ChesMsgId") + .HasColumnType("uuid"); + + b.Property("ChesResponse") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChesStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryAttempts") + .HasColumnType("integer"); + + b.Property("SendOnDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("SentDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ToAddress") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EmailLogs", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DisplayName") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("EmailLogId") + .HasColumnType("uuid"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("S3ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EmailLogId"); + + b.HasIndex("S3ObjectKey"); + + b.ToTable("EmailLogAttachments", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BodyHTML") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyText") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SendFrom") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("EmailTemplates", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Subscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionGroups", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriberId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.HasIndex("SubscriberId"); + + b.ToTable("SubscriptionGroupSubscribers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MapTo") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TemplateVariables", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Triggers", "Notifications"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("SubscriptionGroupId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TriggerId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionGroupId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("TriggerId"); + + b.ToTable("TriggerSubscriptions", "Notifications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(35) + .HasColumnType("character varying(35)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("MinistryClient") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("Responsibility") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServiceLine") + .IsRequired() + .HasColumnType("text"); + + b.Property("Stob") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AccountCodings", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DefaultAccountCodingId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentIdPrefix") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("PaymentConfigurations", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DecisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DecisionUserId") + .HasColumnType("uuid"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.ToTable("ExpenseApprovals", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountCodingId") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("BatchName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BatchNumber") + .HasColumnType("numeric"); + + b.Property("CasHttpStatusCode") + .HasColumnType("integer"); + + b.Property("CasResponse") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContractNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FsbApNotified") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("FsbNotificationEmailLogId") + .HasColumnType("uuid"); + + b.Property("FsbNotificationSentDate") + .HasColumnType("timestamp without time zone"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("InvoiceStatus") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsRecon") + .HasColumnType("boolean"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PayeeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentDate") + .HasColumnType("text"); + + b.Property("PaymentNumber") + .HasColumnType("text"); + + b.Property("PaymentStatus") + .HasColumnType("text"); + + b.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequesterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SiteId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("SubmissionConfirmationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("SupplierName") + .HasColumnType("text"); + + b.Property("SupplierNumber") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("AccountCodingId"); + + b.HasIndex("FsbNotificationEmailLogId"); + + b.HasIndex("ReferenceNumber") + .IsUnique(); + + b.HasIndex("SiteId"); + + b.ToTable("PaymentRequests", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("PaymentRequestId") + .HasColumnType("uuid"); + + b.Property("TagId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("PaymentRequestId"); + + b.HasIndex("TagId"); + + b.ToTable("PaymentTags", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Threshold") + .HasColumnType("numeric"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("PaymentThresholds", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressLine1") + .HasColumnType("text"); + + b.Property("AddressLine2") + .HasColumnType("text"); + + b.Property("AddressLine3") + .HasColumnType("text"); + + b.Property("BankAccount") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("Country") + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("EFTAdvicePref") + .HasColumnType("text"); + + b.Property("EmailAddress") + .HasColumnType("text"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCas") + .HasColumnType("timestamp without time zone"); + + b.Property("MarkDeletedInUse") + .HasColumnType("boolean"); + + b.Property("Number") + .IsRequired() + .HasColumnType("text"); + + b.Property("PaymentGroup") + .HasColumnType("integer"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SiteProtected") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("SupplierId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.ToTable("Sites", "Payments"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BusinessNumber") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("LastUpdatedInCAS") + .HasColumnType("timestamp without time zone"); + + b.Property("MailingAddress") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("text"); + + b.Property("PostalCode") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("Province") + .HasColumnType("text"); + + b.Property("SIN") + .HasColumnType("text"); + + b.Property("StandardIndustryClassification") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("text"); + + b.Property("Subcategory") + .HasColumnType("text"); + + b.Property("SupplierProtected") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("Suppliers", "Payments"); + }); + + modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("CorrelationProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("Mapping") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RoleStatus") + .HasColumnType("integer"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("ViewName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ViewStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ReportColumnsMaps", "Reporting"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Instances") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") + .WithMany("Answers") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) + .WithMany("Answers") + .HasForeignKey("ScoresheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") + .WithMany("Sections") + .HasForeignKey("ScoresheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Scoresheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => + { + b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) + .WithMany("Values") + .HasForeignKey("WorksheetInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Links") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") + .WithMany("Fields") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") + .WithMany("Sections") + .HasForeignKey("WorksheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Worksheet"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicantAddresses") + .HasForeignKey("ApplicationId"); + + b.Navigation("Applicant"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithOne("ApplicantAgent") + .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") + .WithMany("Applications") + .HasForeignKey("ApplicationStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Applicant"); + + b.Navigation("ApplicationForm"); + + b.Navigation("ApplicationStatus"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationAssignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") + .WithMany() + .HasForeignKey("AssigneeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Assignee"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => + { + b.HasOne("Unity.GrantManager.Intakes.Intake", null) + .WithMany() + .HasForeignKey("IntakeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ParentFormId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => + { + b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) + .WithMany() + .HasForeignKey("ApplicationFormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany("ApplicationLinks") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("ApplicationTags") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId"); + }); + + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", "Application") + .WithMany("Assessments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("AssessorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Applicant", null) + .WithMany() + .HasForeignKey("ApplicantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => + { + b.HasOne("Unity.GrantManager.Applications.Application", null) + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => + { + b.HasOne("Unity.GrantManager.Assessments.Assessment", null) + .WithMany() + .HasForeignKey("AssessmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.Identity.Person", null) + .WithMany() + .HasForeignKey("CommenterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => + { + b.HasOne("Unity.GrantManager.Contacts.Contact", null) + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => + { + b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => + { + b.HasOne("Unity.Notifications.Emails.EmailLog", null) + .WithMany() + .HasForeignKey("EmailLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("GroupId"); + + b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") + .WithMany() + .HasForeignKey("SubscriberId"); + + b.Navigation("Subscriber"); + + b.Navigation("SubscriptionGroup"); + }); + + modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => + { + b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") + .WithMany() + .HasForeignKey("SubscriptionGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") + .WithMany() + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") + .WithMany() + .HasForeignKey("TriggerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmailTemplate"); + + b.Navigation("SubscriptionGroup"); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") + .WithMany("ExpenseApprovals") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentRequest"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") + .WithMany() + .HasForeignKey("AccountCodingId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") + .WithMany() + .HasForeignKey("SiteId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("AccountCoding"); + + b.Navigation("Site"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => + { + b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) + .WithMany("PaymentTags") + .HasForeignKey("PaymentRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => + { + b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") + .WithMany("Sites") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => + { + b.Navigation("Instances"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => + { + b.Navigation("Links"); + + b.Navigation("Sections"); + }); + + modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => + { + b.Navigation("Fields"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => + { + b.Navigation("ApplicantAddresses"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => + { + b.Navigation("ApplicantAddresses"); + + b.Navigation("ApplicantAgent"); + + b.Navigation("ApplicationAssignments"); + + b.Navigation("ApplicationLinks"); + + b.Navigation("ApplicationTags"); + + b.Navigation("Assessments"); + }); + + modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => + { + b.Navigation("Applications"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => + { + b.Navigation("ExpenseApprovals"); + + b.Navigation("PaymentTags"); + }); + + modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => + { + b.Navigation("Sites"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.cs new file mode 100644 index 0000000000..fa1a8dae4d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000002_AddSafeToJsonbAndGuardDataGridLateral.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.IO; +using System.Reflection; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddSafeToJsonbAndGuardDataGridLateral : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + var assembly = Assembly.GetExecutingAssembly(); + + ApplyScript(migrationBuilder, assembly, "Unity.GrantManager.Scripts.safe_to_jsonb.sql"); + ApplyScript(migrationBuilder, assembly, "Unity.GrantManager.Scripts.get_worksheet_data.sql"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + + private static void ApplyScript(MigrationBuilder migrationBuilder, Assembly assembly, string resourceName) + { + using Stream stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}"); + using StreamReader reader = new(stream); + string sql = reader.ReadToEnd(); + migrationBuilder.Sql(sql); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql index 98d5f5ec33..21e58fbed9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/get_worksheet_data.sql @@ -71,10 +71,10 @@ BEGIN row_number() OVER() as row_num FROM jsonb_array_elements( COALESCE( - (SELECT (v_elem->>''value'')::jsonb->''rows'' + (SELECT "Reporting".safe_to_jsonb(v_elem->>''value'')->''rows'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L), - ''[null]''::jsonb + ''[null]''::jsonb ) ) AS row_elem ) AS dg_tbl @@ -91,7 +91,8 @@ BEGIN WHEN um.worksheet_name = uwd.worksheet_name AND um.datagrid_name = uwd.datagrid_name THEN CASE um.column_type WHEN 'currency' THEN - format('(CASE WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) IS NULL THEN NULL WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) ~ ''^-?[0-9]+\.?[0-9]*$'' THEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))::DECIMAL(18,2) ELSE NULL END) AS %I', + -- Strip thousands-separator commas and surrounding whitespace before validating/casting (e.g. "1,470.07" -> "1470.07") + format('(CASE WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) IS NULL THEN NULL WHEN replace(btrim((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)), '','', '''') ~ ''^-?[0-9]+\.?[0-9]*$'' THEN replace(btrim((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)), '','', '''')::DECIMAL(18,2) ELSE NULL END) AS %I', um.field_name, um.field_name, um.field_name, um.column_name) WHEN 'number' THEN format('(CASE WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) IS NULL THEN NULL WHEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L)) ~ ''^-?[0-9]+\.?[0-9]*$'' THEN ((SELECT cell_elem->>''value'' FROM jsonb_array_elements(dg_tbl.dg_data->''cells'') AS cell_elem WHERE cell_elem->>''key'' = %L))::NUMERIC ELSE NULL END) AS %I', @@ -171,7 +172,8 @@ BEGIN WHEN um.worksheet_name = uwr.worksheet_name AND (um.type_path NOT LIKE '%datagrid%' OR um.type_path IS NULL) THEN CASE um.column_type WHEN 'currency' THEN - format('(CASE WHEN ((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)) IS NULL THEN NULL WHEN ((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)) ~ ''^-?[0-9]+\.?[0-9]*$'' THEN ((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L))::DECIMAL(18,2) ELSE NULL END) AS %I', + -- Strip thousands-separator commas and surrounding whitespace before validating/casting (e.g. "1,470.07" -> "1470.07") + format('(CASE WHEN ((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)) IS NULL THEN NULL WHEN replace(btrim((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)), '','', '''') ~ ''^-?[0-9]+\.?[0-9]*$'' THEN replace(btrim((SELECT v_elem->>''value'' FROM jsonb_array_elements(wi."CurrentValue"->''values'') AS v_elem WHERE v_elem->>''key'' = %L)), '','', '''')::DECIMAL(18,2) ELSE NULL END) AS %I', COALESCE(um.clean_data_path, um.property_name), COALESCE(um.clean_data_path, um.property_name), COALESCE(um.clean_data_path, um.property_name), diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/safe_to_jsonb.sql b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/safe_to_jsonb.sql new file mode 100644 index 0000000000..3709bf0f53 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Scripts/safe_to_jsonb.sql @@ -0,0 +1,17 @@ +CREATE OR REPLACE FUNCTION "Reporting".safe_to_jsonb(val text) + RETURNS jsonb + LANGUAGE plpgsql + STABLE +AS $function$ +BEGIN + IF val IS NULL OR btrim(val) = '' THEN + RETURN NULL; + END IF; + + BEGIN + RETURN val::jsonb; + EXCEPTION WHEN OTHERS THEN + RETURN NULL; + END; +END; +$function$; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj index 02e6a2f93a..58bfb739d5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Unity.GrantManager.EntityFrameworkCore.csproj @@ -18,6 +18,7 @@ + @@ -45,6 +46,9 @@ Never + + Never + diff --git a/documentation/reporting/get_worksheet_data_specification.md b/documentation/reporting/get_worksheet_data_specification.md index 90cf407c33..5ea9962c76 100644 --- a/documentation/reporting/get_worksheet_data_specification.md +++ b/documentation/reporting/get_worksheet_data_specification.md @@ -73,7 +73,7 @@ Generates queries for root-level field extraction: ### Simple Field Types - **Text**: Direct string extraction -- **Currency**: Validates numeric format, converts to DECIMAL(10,2) +- **Currency**: Strips thousands-separator commas and surrounding whitespace (e.g. `1,470.07` → `1470.07`), validates numeric format, converts to DECIMAL(18,2) - **Number**: Validates numeric format, converts to NUMERIC - **Date**: Validates and converts to TIMESTAMP @@ -130,7 +130,7 @@ The final output is a UNION query combining: ## Data Type Mapping | Field Type | SQL Type | NULL Type | |------------|----------|-----------| -| Currency | DECIMAL(10,2) | NULL::DECIMAL(10,2) | +| Currency | DECIMAL(18,2) | NULL::DECIMAL(18,2) | | Number | NUMERIC | NULL::NUMERIC | | Date | TIMESTAMP | NULL::TIMESTAMP | | Checkbox | BOOLEAN | NULL::BOOLEAN | @@ -191,8 +191,5 @@ SELECT "Reporting".get_worksheet_data('correlation-id', 'report-map-id'); - **v1.2**: Added checkbox group support - **v1.3**: Fixed radio field handling to return text values - **v1.4**: Enhanced error handling and NULL type consistency - -## Related Documentation -- [Worksheet Schema Parser](./worksheet_field_schema_parser.md) -- [Report Column Mapping](./report_column_mapping.md) -- [Field Type Definitions](./field_type_definitions.md) \ No newline at end of file +- **v1.5**: Increased currency precision from `DECIMAL(10,2)` to `DECIMAL(18,2)` (tenant migration `20251125234153_UpdateViewGenCurrencyPrecision`) +- **v1.6**: Locale-formatted currency values are normalized by stripping commas and whitespace before numeric validation, so values such as `1,470.07` are persisted instead of becoming `NULL`. Paired with the `WorksheetFieldSchemaParser` mixed-DataGrid fix, which now emits mapping rows for statically-defined columns on mixed (dynamic + static) grids so the SQL function actually receives mappings to project. Deployed via tenant migration `20260416000001_UpdateWorksheetViewGenCurrencyCommaFix`. \ No newline at end of file From 333172fee4a23dc675c41ac278a13313199f9e67 Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Mon, 20 Apr 2026 13:28:27 -0700 Subject: [PATCH 017/134] AB#32705 - Removed dead AI queue methods and background jobs. --- .../IApplicationAIGenerationQueue.cs | 4 --- ...ateApplicationAnalysisBackgroundJobArgs.cs | 8 ----- ...rateApplicationScoringBackgroundJobArgs.cs | 8 ----- ...erateAttachmentSummaryBackgroundJobArgs.cs | 9 ------ .../ApplicationAIGenerationQueue.cs | 31 ------------------- .../GenerateApplicationAnalysisJob.cs | 25 --------------- .../GenerateApplicationScoringJob.cs | 21 ------------- .../GenerateAttachmentSummaryJob.cs | 24 -------------- 8 files changed, 130 deletions(-) delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs index b9d5db8a9a..3db64559f0 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs @@ -1,13 +1,9 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace Unity.AI.Automation; public interface IApplicationAIGenerationQueue { - Task QueueAttachmentSummariesAsync(IReadOnlyList attachmentIds, Guid? tenantId, string? promptVersion = null); - Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); - Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs deleted file mode 100644 index d1f71301fa..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -public class GenerateApplicationAnalysisBackgroundJobArgs -{ - public Guid ApplicationId { get; set; } - public Guid? TenantId { get; set; } - public string? PromptVersion { get; set; } -} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs deleted file mode 100644 index 06b0d0cd97..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -public class GenerateApplicationScoringBackgroundJobArgs -{ - public Guid ApplicationId { get; set; } - public Guid? TenantId { get; set; } - public string? PromptVersion { get; set; } -} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs deleted file mode 100644 index bf87e59783..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -public class GenerateAttachmentSummaryBackgroundJobArgs -{ - public List AttachmentIds { get; set; } = []; - public Guid? TenantId { get; set; } - public string? PromptVersion { get; set; } -} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs index e5f52cf296..25abe8819a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Unity.AI.Automation; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; @@ -11,36 +10,6 @@ namespace Unity.GrantManager.GrantApplications.Automation; public class ApplicationAIGenerationQueue(IBackgroundJobManager backgroundJobManager) : IApplicationAIGenerationQueue, ITransientDependency { - public async Task QueueAttachmentSummariesAsync(IReadOnlyList attachmentIds, Guid? tenantId, string? promptVersion = null) - { - await backgroundJobManager.EnqueueAsync(new GenerateAttachmentSummaryBackgroundJobArgs - { - AttachmentIds = [.. attachmentIds], - PromptVersion = promptVersion, - TenantId = tenantId - }); - } - - public async Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) - { - await backgroundJobManager.EnqueueAsync(new GenerateApplicationAnalysisBackgroundJobArgs - { - ApplicationId = applicationId, - PromptVersion = promptVersion, - TenantId = tenantId - }); - } - - public async Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) - { - await backgroundJobManager.EnqueueAsync(new GenerateApplicationScoringBackgroundJobArgs - { - ApplicationId = applicationId, - PromptVersion = promptVersion, - TenantId = tenantId - }); - } - public async Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) { await backgroundJobManager.EnqueueAsync(new RunApplicationAIPipelineJobArgs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs deleted file mode 100644 index c3d3448687..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Unity.GrantManager.GrantApplications; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.DependencyInjection; -using Volo.Abp.MultiTenancy; -namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -public class GenerateApplicationAnalysisJob( - IApplicationAnalysisAppService applicationAnalysisService, - ICurrentTenant currentTenant, - ILogger logger) : AsyncBackgroundJob, ITransientDependency -{ - public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJobArgs args) - { - using (currentTenant.Change(args.TenantId)) - { - logger.LogInformation("Executing AI application analysis job for application {ApplicationId}.", args.ApplicationId); - var result = await applicationAnalysisService.GenerateApplicationAnalysisAsync(args.ApplicationId, args.PromptVersion); - if (result.Completed) - { - logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); - } - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs deleted file mode 100644 index d65a56e8d2..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Unity.GrantManager.GrantApplications; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.DependencyInjection; -using Volo.Abp.MultiTenancy; -namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -public class GenerateApplicationScoringJob( - IApplicationScoringAppService applicationScoringService, - ICurrentTenant currentTenant, - ILogger logger) : AsyncBackgroundJob, ITransientDependency -{ - public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobArgs args) - { - using (currentTenant.Change(args.TenantId)) - { - logger.LogInformation("Executing AI application scoring job for application {ApplicationId}.", args.ApplicationId); - await applicationScoringService.GenerateApplicationScoringAsync(args.ApplicationId, args.PromptVersion); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs deleted file mode 100644 index c47c667005..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Unity.GrantManager.Attachments; -using Volo.Abp.BackgroundJobs; -using Volo.Abp.DependencyInjection; -using Volo.Abp.MultiTenancy; -namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -public class GenerateAttachmentSummaryJob( - IAttachmentSummaryAppService attachmentSummaryService, - ICurrentTenant currentTenant, - ILogger logger) : AsyncBackgroundJob, ITransientDependency -{ - public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobArgs args) - { - using (currentTenant.Change(args.TenantId)) - { - logger.LogInformation( - "Executing AI attachment summary job for {AttachmentCount} attachment(s).", - args.AttachmentIds.Count); - var results = await attachmentSummaryService.GenerateAttachmentSummariesAsync(args.AttachmentIds, args.PromptVersion); - logger.LogInformation("Completed AI attachment summaries for {CompletedCount} attachment(s).", results.Count); - } - } -} From 85c299bf691472a1f83176be2feeb98c79c20aba Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 20 Apr 2026 14:05:04 -0700 Subject: [PATCH 018/134] AB#31194 copilot feedback --- .../ReportingConfiguration/Default.js | 3 - ...rksheetViewGenCurrencyCommaFix.Designer.cs | 4947 ----------------- ..._UpdateWorksheetViewGenCurrencyCommaFix.cs | 31 - .../get_worksheet_data_specification.md | 2 +- 4 files changed, 1 insertion(+), 4982 deletions(-) delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js index 5264bc80b2..fa9d2ff123 100644 --- a/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js @@ -805,9 +805,6 @@ $(function () { const path = $input.data('path'); const value = $input.val().trim(); - $input.removeClass('is-valid is-invalid'); - $input.siblings('.invalid-feedback, .valid-feedback').remove(); - validateColumnNameInput($input, value, path); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs deleted file mode 100644 index e11d04db19..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.Designer.cs +++ /dev/null @@ -1,4947 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Unity.GrantManager.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; - -#nullable disable - -namespace Unity.GrantManager.Migrations.TenantMigrations -{ - [DbContext(typeof(GrantTenantDbContext))] - [Migration("20260416000001_UpdateWorksheetViewGenCurrencyCommaFix")] - partial class UpdateWorksheetViewGenCurrencyCommaFix - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("CorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("ReportData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("ScoresheetId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("ScoresheetId"); - - b.ToTable("ScoresheetInstances", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("CurrentValue") - .HasColumnType("jsonb"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("QuestionId") - .HasColumnType("uuid"); - - b.Property("ScoresheetInstanceId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Version") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.HasIndex("QuestionId"); - - b.HasIndex("ScoresheetInstanceId"); - - b.ToTable("Answers", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Definition") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("Label") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Order") - .HasColumnType("bigint"); - - b.Property("SectionId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("SectionId"); - - b.ToTable("Questions", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Order") - .HasColumnType("bigint"); - - b.Property("Published") - .HasColumnType("boolean"); - - b.Property("ReportColumns") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReportKeys") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReportViewName") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.Property("Version") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("Scoresheets", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Order") - .HasColumnType("bigint"); - - b.Property("ScoresheetId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ScoresheetId"); - - b.ToTable("ScoresheetSections", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("CurrentValue") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("CustomFieldId") - .HasColumnType("uuid"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("WorksheetInstanceId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("WorksheetInstanceId"); - - b.ToTable("CustomFieldValues", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("CorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("CurrentValue") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("ReportData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("UiAnchor") - .IsRequired() - .HasColumnType("text"); - - b.Property("WorksheetCorrelationId") - .HasColumnType("uuid"); - - b.Property("WorksheetCorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("WorksheetId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("WorksheetInstances", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("CorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Order") - .HasColumnType("bigint"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("UiAnchor") - .IsRequired() - .HasColumnType("text"); - - b.Property("WorksheetId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("WorksheetId"); - - b.ToTable("WorksheetLinks", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Definition") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("Enabled") - .HasColumnType("boolean"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("Label") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Order") - .HasColumnType("bigint"); - - b.Property("SectionId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("SectionId"); - - b.ToTable("CustomFields", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Published") - .HasColumnType("boolean"); - - b.Property("ReportColumns") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReportKeys") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReportViewName") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Title") - .IsRequired() - .HasColumnType("text"); - - b.Property("Version") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("Worksheets", "Flex"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Order") - .HasColumnType("bigint"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("WorksheetId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("WorksheetId"); - - b.ToTable("WorksheetSections", "Flex"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantName") - .IsRequired() - .HasMaxLength(600) - .HasColumnType("character varying(600)"); - - b.Property("ApproxNumberOfEmployees") - .HasColumnType("text"); - - b.Property("AuditComments") - .HasColumnType("text"); - - b.Property("BusinessNumber") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FiscalDay") - .HasColumnType("integer"); - - b.Property("FiscalMonth") - .HasColumnType("text"); - - b.Property("FundingHistoryComments") - .HasColumnType("text"); - - b.Property("IndigenousOrgInd") - .HasColumnType("text"); - - b.Property("IsDuplicated") - .HasColumnType("boolean"); - - b.Property("IssueTrackingComments") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("MatchPercentage") - .HasColumnType("numeric"); - - b.Property("NonRegOrgName") - .HasColumnType("text"); - - b.Property("NonRegisteredBusinessName") - .HasColumnType("text"); - - b.Property("OrgName") - .HasColumnType("text"); - - b.Property("OrgNumber") - .HasColumnType("text"); - - b.Property("OrgStatus") - .HasColumnType("text"); - - b.Property("OrganizationSize") - .HasColumnType("text"); - - b.Property("OrganizationType") - .HasColumnType("text"); - - b.Property("RedStop") - .HasColumnType("boolean"); - - b.Property("Sector") - .HasColumnType("text"); - - b.Property("SectorSubSectorIndustryDesc") - .HasColumnType("text"); - - b.Property("SiteId") - .HasColumnType("uuid"); - - b.Property("StartedOperatingDate") - .HasColumnType("date"); - - b.Property("Status") - .HasColumnType("text"); - - b.Property("SubSector") - .HasColumnType("text"); - - b.Property("SupplierId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("UnityApplicantId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantName"); - - b.ToTable("Applicants", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("AddressType") - .HasColumnType("integer"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Postal") - .HasColumnType("text"); - - b.Property("Province") - .HasColumnType("text"); - - b.Property("Street") - .HasColumnType("text"); - - b.Property("Street2") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Unit") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.HasIndex("ApplicationId"); - - b.ToTable("ApplicantAddresses", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("BceidBusinessGuid") - .HasColumnType("uuid"); - - b.Property("BceidBusinessName") - .HasColumnType("text"); - - b.Property("BceidUserGuid") - .HasColumnType("uuid"); - - b.Property("BceidUserName") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ContactOrder") - .HasColumnType("integer"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Email") - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IdentityEmail") - .HasColumnType("text"); - - b.Property("IdentityName") - .HasColumnType("text"); - - b.Property("IdentityProvider") - .HasColumnType("text"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("IsConfirmed") - .HasColumnType("boolean"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("OidcSubUser") - .HasColumnType("text"); - - b.Property("Phone") - .HasColumnType("text"); - - b.Property("Phone2") - .HasColumnType("text"); - - b.Property("Phone2Extension") - .HasColumnType("text"); - - b.Property("PhoneExtension") - .HasColumnType("text"); - - b.Property("RoleForApplicant") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Title") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.HasIndex("ApplicationId") - .IsUnique(); - - b.ToTable("ApplicantAgents", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DisplayName") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FileName") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("S3ObjectKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.ToTable("ApplicantAttachments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("AIAnalysis") - .HasColumnType("text"); - - b.Property("AIScoresheetAnswers") - .HasColumnType("jsonb"); - - b.Property("Acquisition") - .HasColumnType("text"); - - b.Property("ApplicantElectoralDistrict") - .HasColumnType("text"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ApplicationFormId") - .HasColumnType("uuid"); - - b.Property("ApplicationStatusId") - .HasColumnType("uuid"); - - b.Property("ApprovedAmount") - .HasColumnType("numeric"); - - b.Property("AssessmentResultDate") - .HasColumnType("timestamp without time zone"); - - b.Property("AssessmentResultStatus") - .HasColumnType("text"); - - b.Property("AssessmentStartDate") - .HasColumnType("timestamp without time zone"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Community") - .HasColumnType("text"); - - b.Property("CommunityPopulation") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ContractExecutionDate") - .HasColumnType("timestamp without time zone"); - - b.Property("ContractNumber") - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeclineRational") - .HasColumnType("text"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("DueDate") - .HasColumnType("timestamp without time zone"); - - b.Property("DueDiligenceStatus") - .HasColumnType("text"); - - b.Property("EconomicRegion") - .HasColumnType("text"); - - b.Property("ElectoralDistrict") - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FinalDecisionDate") - .HasColumnType("timestamp without time zone"); - - b.Property("Forestry") - .HasColumnType("text"); - - b.Property("ForestryFocus") - .HasColumnType("text"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("LikelihoodOfFunding") - .HasColumnType("text"); - - b.Property("Notes") - .HasColumnType("text"); - - b.Property("NotificationDate") - .HasColumnType("timestamp without time zone"); - - b.Property("OwnerId") - .HasColumnType("uuid"); - - b.Property("Payload") - .HasColumnType("jsonb"); - - b.Property("PercentageTotalProjectBudget") - .HasColumnType("double precision"); - - b.Property("Place") - .HasColumnType("text"); - - b.Property("ProjectEndDate") - .HasColumnType("timestamp without time zone"); - - b.Property("ProjectFundingTotal") - .HasColumnType("numeric"); - - b.Property("ProjectName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ProjectStartDate") - .HasColumnType("timestamp without time zone"); - - b.Property("ProjectSummary") - .HasColumnType("text"); - - b.Property("ProposalDate") - .HasColumnType("timestamp without time zone"); - - b.Property("RecommendedAmount") - .HasColumnType("numeric"); - - b.Property("ReferenceNo") - .IsRequired() - .HasColumnType("text"); - - b.Property("RegionalDistrict") - .HasColumnType("text"); - - b.Property("RequestedAmount") - .HasColumnType("numeric"); - - b.Property("RiskRanking") - .HasColumnType("text"); - - b.Property("SigningAuthorityBusinessPhone") - .HasColumnType("text"); - - b.Property("SigningAuthorityCellPhone") - .HasColumnType("text"); - - b.Property("SigningAuthorityEmail") - .HasColumnType("text"); - - b.Property("SigningAuthorityFullName") - .HasColumnType("text"); - - b.Property("SigningAuthorityTitle") - .HasColumnType("text"); - - b.Property("SubStatus") - .HasColumnType("text"); - - b.Property("SubmissionDate") - .HasColumnType("timestamp without time zone"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("TotalProjectBudget") - .HasColumnType("numeric"); - - b.Property("TotalScore") - .HasColumnType("integer"); - - b.Property("UnityApplicationId") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.HasIndex("ApplicationFormId"); - - b.HasIndex("ApplicationStatusId"); - - b.HasIndex("OwnerId"); - - b.ToTable("Applications", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("AssigneeId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Duty") - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("AssigneeId"); - - b.ToTable("ApplicationAssignments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DisplayName") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FileName") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("S3ObjectKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.ToTable("ApplicationAttachments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("AISummary") - .HasColumnType("text"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ChefsFileId") - .HasColumnType("text"); - - b.Property("ChefsSubmissionId") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DisplayName") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FileName") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.ToTable("ApplicationChefsFileAttachments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ContactEmail") - .HasColumnType("text"); - - b.Property("ContactFullName") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContactMobilePhone") - .HasColumnType("text"); - - b.Property("ContactTitle") - .HasColumnType("text"); - - b.Property("ContactType") - .IsRequired() - .HasColumnType("text"); - - b.Property("ContactWorkPhone") - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.ToTable("ApplicationContact", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("AccountCodingId") - .HasColumnType("uuid"); - - b.Property("ApiKey") - .HasColumnType("text"); - - b.Property("ApplicationFormDescription") - .HasColumnType("text"); - - b.Property("ApplicationFormName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AttemptedConnectionDate") - .HasColumnType("timestamp without time zone"); - - b.Property("AutomaticallyGenerateAIAnalysis") - .HasColumnType("boolean"); - - b.Property("AvailableChefsFields") - .HasColumnType("text"); - - b.Property("Category") - .HasColumnType("text"); - - b.Property("ChefsApplicationFormGuid") - .HasColumnType("text"); - - b.Property("ChefsCriteriaFormGuid") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ConnectionHttpStatus") - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DefaultPaymentGroup") - .HasColumnType("integer"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ElectoralDistrictAddressType") - .HasColumnType("integer"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FormHierarchy") - .HasColumnType("integer"); - - b.Property("IntakeId") - .HasColumnType("uuid"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("IsDirectApproval") - .HasColumnType("boolean"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("ManuallyInitiateAIAnalysis") - .HasColumnType("boolean"); - - b.Property("ParentFormId") - .HasColumnType("uuid"); - - b.Property("Payable") - .HasColumnType("boolean"); - - b.Property("PaymentApprovalThreshold") - .HasColumnType("numeric"); - - b.Property("Prefix") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PreventPayment") - .HasColumnType("boolean"); - - b.Property("RenderFormIoToHtml") - .HasColumnType("boolean"); - - b.Property("ScoresheetId") - .HasColumnType("uuid"); - - b.Property("SuffixType") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IntakeId"); - - b.HasIndex("ParentFormId"); - - b.ToTable("ApplicationForms", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ApplicationFormId") - .HasColumnType("uuid"); - - b.Property("ApplicationFormVersionId") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ChefsSubmissionGuid") - .IsRequired() - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FormVersionId") - .HasColumnType("uuid"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("OidcSub") - .IsRequired() - .HasColumnType("text"); - - b.Property("RenderedHTML") - .HasColumnType("text"); - - b.Property("ReportData") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("Submission") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.HasIndex("ApplicationFormId"); - - b.ToTable("ApplicationFormSubmissions", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationFormId") - .HasColumnType("uuid"); - - b.Property("AvailableChefsFields") - .HasColumnType("text"); - - b.Property("ChefsApplicationFormGuid") - .HasColumnType("text"); - - b.Property("ChefsFormVersionGuid") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FormSchema") - .HasColumnType("jsonb"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Published") - .HasColumnType("boolean"); - - b.Property("ReportColumns") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReportKeys") - .IsRequired() - .HasColumnType("text"); - - b.Property("ReportViewName") - .IsRequired() - .HasColumnType("text"); - - b.Property("SubmissionHeaderMapping") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationFormId"); - - b.ToTable("ApplicationFormVersion", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("LinkType") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasDefaultValue("Related"); - - b.Property("LinkedApplicationId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.ToTable("ApplicationLinks", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExternalStatus") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("InternalStatus") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("StatusCode") - .IsRequired() - .HasMaxLength(250) - .HasColumnType("character varying(250)"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("StatusCode") - .IsUnique(); - - b.ToTable("ApplicationStatuses", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TagId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("TagId"); - - b.ToTable("ApplicationTags", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("AssessmentId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DisplayName") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FileName") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("S3ObjectKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("AssessmentId"); - - b.ToTable("AssessmentAttachments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("AuditDate") - .HasColumnType("timestamp without time zone"); - - b.Property("AuditNote") - .HasColumnType("text"); - - b.Property("AuditTrackingNumber") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.ToTable("AuditHistories", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ApprovedAmount") - .HasColumnType("numeric"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FundingNotes") - .HasColumnType("text"); - - b.Property("FundingYear") - .HasColumnType("text"); - - b.Property("GrantCategory") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("OneTimeConsideration") - .HasColumnType("numeric"); - - b.Property("ReconsiderationAmount") - .HasColumnType("numeric"); - - b.Property("RenewedFunding") - .HasColumnType("boolean"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("TotalGrantAmount") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.ToTable("FundingHistories", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IssueDescription") - .HasColumnType("text"); - - b.Property("IssueHeading") - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("ResolutionNote") - .HasColumnType("text"); - - b.Property("Resolved") - .HasColumnType("boolean"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Year") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.ToTable("IssueTrackings", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("ApprovalRecommended") - .HasColumnType("boolean"); - - b.Property("AssessorId") - .HasColumnType("uuid"); - - b.Property("CleanGrowth") - .HasColumnType("integer"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("EconomicImpact") - .HasColumnType("integer"); - - b.Property("EndDate") - .HasColumnType("timestamp without time zone"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FinancialAnalysis") - .HasColumnType("integer"); - - b.Property("InclusiveGrowth") - .HasColumnType("integer"); - - b.Property("IsAiAssessment") - .HasColumnType("boolean"); - - b.Property("IsComplete") - .HasColumnType("boolean"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("AssessorId"); - - b.ToTable("Assessments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("Comment") - .IsRequired() - .HasColumnType("text"); - - b.Property("CommenterId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PinDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicantId"); - - b.HasIndex("CommenterId"); - - b.ToTable("ApplicantComments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("Comment") - .IsRequired() - .HasColumnType("text"); - - b.Property("CommenterId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PinDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("CommenterId"); - - b.ToTable("ApplicationComments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("AssessmentId") - .HasColumnType("uuid"); - - b.Property("Comment") - .IsRequired() - .HasColumnType("text"); - - b.Property("CommenterId") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PinDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("AssessmentId"); - - b.HasIndex("CommenterId"); - - b.ToTable("AssessmentComments", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Contacts.Contact", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Email") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("HomePhoneNumber") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("MobilePhoneNumber") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Title") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("WorkPhoneExtension") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("WorkPhoneNumber") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.ToTable("Contacts", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ContactId") - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("IsPrimary") - .HasColumnType("boolean"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("RelatedEntityId") - .HasColumnType("uuid"); - - b.Property("RelatedEntityType") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Role") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("RelatedEntityType", "RelatedEntityId"); - - b.HasIndex("ContactId", "RelatedEntityType", "RelatedEntityId"); - - b.ToTable("ContactLinks", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.GlobalTag.Tag", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Tags", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Identity.Person", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("Badge") - .IsRequired() - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FullName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("OidcDisplayName") - .IsRequired() - .HasColumnType("text"); - - b.Property("OidcSub") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("OidcSub"); - - b.ToTable("Persons", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.Intakes.Intake", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("Budget") - .HasColumnType("double precision"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("EndDate") - .HasColumnType("timestamp without time zone"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IntakeName") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("StartDate") - .HasColumnType("timestamp without time zone"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Intakes", (string)null); - }); - - modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("EmailGroups", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("GroupId") - .HasColumnType("uuid"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.ToTable("EmailGroupUsers", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Emails.EmailLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ApplicantId") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid"); - - b.Property("AssessmentId") - .HasColumnType("uuid"); - - b.Property("BCC") - .IsRequired() - .HasColumnType("text"); - - b.Property("Body") - .IsRequired() - .HasColumnType("text"); - - b.Property("BodyType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CC") - .IsRequired() - .HasColumnType("text"); - - b.Property("ChesHttpStatusCode") - .HasColumnType("text"); - - b.Property("ChesMsgId") - .HasColumnType("uuid"); - - b.Property("ChesResponse") - .IsRequired() - .HasColumnType("text"); - - b.Property("ChesStatus") - .IsRequired() - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FromAddress") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PaymentRequestIds") - .IsRequired() - .HasColumnType("text"); - - b.Property("Priority") - .IsRequired() - .HasColumnType("text"); - - b.Property("RetryAttempts") - .HasColumnType("integer"); - - b.Property("SendOnDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("SentDateTime") - .HasColumnType("timestamp without time zone"); - - b.Property("Status") - .IsRequired() - .HasColumnType("text"); - - b.Property("Subject") - .IsRequired() - .HasColumnType("text"); - - b.Property("Tag") - .IsRequired() - .HasColumnType("text"); - - b.Property("TemplateName") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("ToAddress") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("EmailLogs", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ContentType") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DisplayName") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - - b.Property("EmailLogId") - .HasColumnType("uuid"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FileName") - .HasColumnType("text"); - - b.Property("FileSize") - .HasColumnType("bigint"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("S3ObjectKey") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Time") - .HasColumnType("timestamp without time zone"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("EmailLogId"); - - b.HasIndex("S3ObjectKey"); - - b.ToTable("EmailLogAttachments", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.EmailTemplate", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BodyHTML") - .IsRequired() - .HasColumnType("text"); - - b.Property("BodyText") - .IsRequired() - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("SendFrom") - .IsRequired() - .HasColumnType("text"); - - b.Property("Subject") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("EmailTemplates", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.Subscriber", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Email") - .IsRequired() - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Subscribers", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("SubscriptionGroups", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("GroupId") - .HasColumnType("uuid"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("SubscriberId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("GroupId"); - - b.HasIndex("SubscriberId"); - - b.ToTable("SubscriptionGroupSubscribers", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.TemplateVariable", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("MapTo") - .IsRequired() - .HasColumnType("text"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Token") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.ToTable("TemplateVariables", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.Trigger", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Active") - .HasColumnType("boolean"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("InternalName") - .IsRequired() - .HasColumnType("text"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Triggers", "Notifications"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("SubscriptionGroupId") - .HasColumnType("uuid"); - - b.Property("TemplateId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("TriggerId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("SubscriptionGroupId"); - - b.HasIndex("TemplateId"); - - b.HasIndex("TriggerId"); - - b.ToTable("TriggerSubscriptions", "Notifications"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.AccountCodings.AccountCoding", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("Description") - .HasMaxLength(35) - .HasColumnType("character varying(35)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("MinistryClient") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProjectNumber") - .IsRequired() - .HasColumnType("text"); - - b.Property("Responsibility") - .IsRequired() - .HasColumnType("text"); - - b.Property("ServiceLine") - .IsRequired() - .HasColumnType("text"); - - b.Property("Stob") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("AccountCodings", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentConfigurations.PaymentConfiguration", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DefaultAccountCodingId") - .HasColumnType("uuid"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PaymentIdPrefix") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("PaymentConfigurations", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DecisionDate") - .HasColumnType("timestamp without time zone"); - - b.Property("DecisionUserId") - .HasColumnType("uuid"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PaymentRequestId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("PaymentRequestId"); - - b.ToTable("ExpenseApprovals", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AccountCodingId") - .HasColumnType("uuid"); - - b.Property("Amount") - .HasColumnType("numeric"); - - b.Property("BatchName") - .IsRequired() - .HasColumnType("text"); - - b.Property("BatchNumber") - .HasColumnType("numeric"); - - b.Property("CasHttpStatusCode") - .HasColumnType("integer"); - - b.Property("CasResponse") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("ContractNumber") - .IsRequired() - .HasColumnType("text"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("CorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FsbApNotified") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("FsbNotificationEmailLogId") - .HasColumnType("uuid"); - - b.Property("FsbNotificationSentDate") - .HasColumnType("timestamp without time zone"); - - b.Property("InvoiceNumber") - .IsRequired() - .HasColumnType("text"); - - b.Property("InvoiceStatus") - .HasColumnType("text"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("IsRecon") - .HasColumnType("boolean"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Note") - .HasColumnType("text"); - - b.Property("PayeeName") - .IsRequired() - .HasColumnType("text"); - - b.Property("PaymentDate") - .HasColumnType("text"); - - b.Property("PaymentNumber") - .HasColumnType("text"); - - b.Property("PaymentStatus") - .HasColumnType("text"); - - b.Property("ReferenceNumber") - .IsRequired() - .HasColumnType("text"); - - b.Property("RequesterName") - .IsRequired() - .HasColumnType("text"); - - b.Property("SiteId") - .HasColumnType("uuid"); - - b.Property("Status") - .HasColumnType("integer"); - - b.Property("SubmissionConfirmationCode") - .IsRequired() - .HasColumnType("text"); - - b.Property("SupplierName") - .HasColumnType("text"); - - b.Property("SupplierNumber") - .IsRequired() - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("AccountCodingId"); - - b.HasIndex("FsbNotificationEmailLogId"); - - b.HasIndex("ReferenceNumber") - .IsUnique(); - - b.HasIndex("SiteId"); - - b.ToTable("PaymentRequests", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("PaymentRequestId") - .HasColumnType("uuid"); - - b.Property("TagId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("PaymentRequestId"); - - b.HasIndex("TagId"); - - b.ToTable("PaymentTags", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentThresholds.PaymentThreshold", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("Threshold") - .HasColumnType("numeric"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.ToTable("PaymentThresholds", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AddressLine1") - .HasColumnType("text"); - - b.Property("AddressLine2") - .HasColumnType("text"); - - b.Property("AddressLine3") - .HasColumnType("text"); - - b.Property("BankAccount") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("Country") - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("EFTAdvicePref") - .HasColumnType("text"); - - b.Property("EmailAddress") - .HasColumnType("text"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("LastUpdatedInCas") - .HasColumnType("timestamp without time zone"); - - b.Property("MarkDeletedInUse") - .HasColumnType("boolean"); - - b.Property("Number") - .IsRequired() - .HasColumnType("text"); - - b.Property("PaymentGroup") - .HasColumnType("integer"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("ProviderId") - .HasColumnType("text"); - - b.Property("Province") - .HasColumnType("text"); - - b.Property("SiteProtected") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("text"); - - b.Property("SupplierId") - .HasColumnType("uuid"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("SupplierId"); - - b.ToTable("Sites", "Payments"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("BusinessNumber") - .HasColumnType("text"); - - b.Property("City") - .HasColumnType("text"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("CorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("LastUpdatedInCAS") - .HasColumnType("timestamp without time zone"); - - b.Property("MailingAddress") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("Number") - .HasColumnType("text"); - - b.Property("PostalCode") - .HasColumnType("text"); - - b.Property("ProviderId") - .HasColumnType("text"); - - b.Property("Province") - .HasColumnType("text"); - - b.Property("SIN") - .HasColumnType("text"); - - b.Property("StandardIndustryClassification") - .HasColumnType("text"); - - b.Property("Status") - .HasColumnType("text"); - - b.Property("Subcategory") - .HasColumnType("text"); - - b.Property("SupplierProtected") - .HasColumnType("text"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.ToTable("Suppliers", "Payments"); - }); - - modelBuilder.Entity("Unity.Reporting.Domain.Configuration.ReportColumnsMap", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CorrelationId") - .HasColumnType("uuid"); - - b.Property("CorrelationProvider") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("Mapping") - .IsRequired() - .HasColumnType("jsonb"); - - b.Property("RoleStatus") - .HasColumnType("integer"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.Property("ViewName") - .IsRequired() - .HasColumnType("text"); - - b.Property("ViewStatus") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("ReportColumnsMaps", "Reporting"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => - { - b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") - .WithMany("Instances") - .HasForeignKey("ScoresheetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Scoresheet"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Answer", b => - { - b.HasOne("Unity.Flex.Domain.Scoresheets.Question", "Question") - .WithMany("Answers") - .HasForeignKey("QuestionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", null) - .WithMany("Answers") - .HasForeignKey("ScoresheetInstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Question"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => - { - b.HasOne("Unity.Flex.Domain.Scoresheets.ScoresheetSection", "Section") - .WithMany("Fields") - .HasForeignKey("SectionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Section"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => - { - b.HasOne("Unity.Flex.Domain.Scoresheets.Scoresheet", "Scoresheet") - .WithMany("Sections") - .HasForeignKey("ScoresheetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Scoresheet"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.CustomFieldValue", b => - { - b.HasOne("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", null) - .WithMany("Values") - .HasForeignKey("WorksheetInstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.Flex.Domain.WorksheetLinks.WorksheetLink", b => - { - b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") - .WithMany("Links") - .HasForeignKey("WorksheetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Worksheet"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.CustomField", b => - { - b.HasOne("Unity.Flex.Domain.Worksheets.WorksheetSection", "Section") - .WithMany("Fields") - .HasForeignKey("SectionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Section"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => - { - b.HasOne("Unity.Flex.Domain.Worksheets.Worksheet", "Worksheet") - .WithMany("Sections") - .HasForeignKey("WorksheetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Worksheet"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAddress", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") - .WithMany("ApplicantAddresses") - .HasForeignKey("ApplicantId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Applications.Application", "Application") - .WithMany("ApplicantAddresses") - .HasForeignKey("ApplicationId"); - - b.Navigation("Applicant"); - - b.Navigation("Application"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAgent", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Applications.Application", "Application") - .WithOne("ApplicantAgent") - .HasForeignKey("Unity.GrantManager.Applications.ApplicantAgent", "ApplicationId"); - - b.Navigation("Application"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicantAttachment", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", "Applicant") - .WithMany() - .HasForeignKey("ApplicantId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Applications.ApplicationForm", "ApplicationForm") - .WithMany() - .HasForeignKey("ApplicationFormId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Applications.ApplicationStatus", "ApplicationStatus") - .WithMany("Applications") - .HasForeignKey("ApplicationStatusId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Identity.Person", "Owner") - .WithMany() - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.NoAction); - - b.Navigation("Applicant"); - - b.Navigation("ApplicationForm"); - - b.Navigation("ApplicationStatus"); - - b.Navigation("Owner"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAssignment", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", "Application") - .WithMany("ApplicationAssignments") - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Identity.Person", "Assignee") - .WithMany() - .HasForeignKey("AssigneeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Application"); - - b.Navigation("Assignee"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationAttachment", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", null) - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationChefsFileAttachment", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", null) - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationContact", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", null) - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationForm", b => - { - b.HasOne("Unity.GrantManager.Intakes.Intake", null) - .WithMany() - .HasForeignKey("IntakeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) - .WithMany() - .HasForeignKey("ParentFormId") - .OnDelete(DeleteBehavior.NoAction); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormSubmission", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) - .WithMany() - .HasForeignKey("ApplicationFormId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationFormVersion", b => - { - b.HasOne("Unity.GrantManager.Applications.ApplicationForm", null) - .WithMany() - .HasForeignKey("ApplicationFormId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationLink", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", null) - .WithMany("ApplicationLinks") - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationTags", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", "Application") - .WithMany("ApplicationTags") - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") - .WithMany() - .HasForeignKey("TagId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Application"); - - b.Navigation("Tag"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.AssessmentAttachment", b => - { - b.HasOne("Unity.GrantManager.Assessments.Assessment", null) - .WithMany() - .HasForeignKey("AssessmentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.AuditHistory", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.FundingHistory", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.IssueTracking", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId"); - }); - - modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", "Application") - .WithMany("Assessments") - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Identity.Person", null) - .WithMany() - .HasForeignKey("AssessorId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Application"); - }); - - modelBuilder.Entity("Unity.GrantManager.Comments.ApplicantComment", b => - { - b.HasOne("Unity.GrantManager.Applications.Applicant", null) - .WithMany() - .HasForeignKey("ApplicantId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Identity.Person", null) - .WithMany() - .HasForeignKey("CommenterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Comments.ApplicationComment", b => - { - b.HasOne("Unity.GrantManager.Applications.Application", null) - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Identity.Person", null) - .WithMany() - .HasForeignKey("CommenterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Comments.AssessmentComment", b => - { - b.HasOne("Unity.GrantManager.Assessments.Assessment", null) - .WithMany() - .HasForeignKey("AssessmentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.Identity.Person", null) - .WithMany() - .HasForeignKey("CommenterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.GrantManager.Contacts.ContactLink", b => - { - b.HasOne("Unity.GrantManager.Contacts.Contact", null) - .WithMany() - .HasForeignKey("ContactId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.Notifications.EmailGroups.EmailGroupUser", b => - { - b.HasOne("Unity.Notifications.EmailGroups.EmailGroup", null) - .WithMany() - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.Notifications.Emails.EmailLogAttachment", b => - { - b.HasOne("Unity.Notifications.Emails.EmailLog", null) - .WithMany() - .HasForeignKey("EmailLogId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.SubscriptionGroupSubscription", b => - { - b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") - .WithMany() - .HasForeignKey("GroupId"); - - b.HasOne("Unity.Notifications.Templates.Subscriber", "Subscriber") - .WithMany() - .HasForeignKey("SubscriberId"); - - b.Navigation("Subscriber"); - - b.Navigation("SubscriptionGroup"); - }); - - modelBuilder.Entity("Unity.Notifications.Templates.TriggerSubscription", b => - { - b.HasOne("Unity.Notifications.Templates.SubscriptionGroup", "SubscriptionGroup") - .WithMany() - .HasForeignKey("SubscriptionGroupId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.Notifications.Templates.EmailTemplate", "EmailTemplate") - .WithMany() - .HasForeignKey("TemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.Notifications.Templates.Trigger", "Trigger") - .WithMany() - .HasForeignKey("TriggerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("EmailTemplate"); - - b.Navigation("SubscriptionGroup"); - - b.Navigation("Trigger"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.ExpenseApproval", b => - { - b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", "PaymentRequest") - .WithMany("ExpenseApprovals") - .HasForeignKey("PaymentRequestId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("PaymentRequest"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => - { - b.HasOne("Unity.Payments.Domain.AccountCodings.AccountCoding", "AccountCoding") - .WithMany() - .HasForeignKey("AccountCodingId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Unity.Payments.Domain.Suppliers.Site", "Site") - .WithMany() - .HasForeignKey("SiteId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("AccountCoding"); - - b.Navigation("Site"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentTags.PaymentTag", b => - { - b.HasOne("Unity.Payments.Domain.PaymentRequests.PaymentRequest", null) - .WithMany("PaymentTags") - .HasForeignKey("PaymentRequestId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Unity.GrantManager.GlobalTag.Tag", "Tag") - .WithMany() - .HasForeignKey("TagId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Tag"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Site", b => - { - b.HasOne("Unity.Payments.Domain.Suppliers.Supplier", "Supplier") - .WithMany("Sites") - .HasForeignKey("SupplierId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Supplier"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.ScoresheetInstances.ScoresheetInstance", b => - { - b.Navigation("Answers"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Question", b => - { - b.Navigation("Answers"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.Scoresheet", b => - { - b.Navigation("Instances"); - - b.Navigation("Sections"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Scoresheets.ScoresheetSection", b => - { - b.Navigation("Fields"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.WorksheetInstances.WorksheetInstance", b => - { - b.Navigation("Values"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.Worksheet", b => - { - b.Navigation("Links"); - - b.Navigation("Sections"); - }); - - modelBuilder.Entity("Unity.Flex.Domain.Worksheets.WorksheetSection", b => - { - b.Navigation("Fields"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.Applicant", b => - { - b.Navigation("ApplicantAddresses"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.Application", b => - { - b.Navigation("ApplicantAddresses"); - - b.Navigation("ApplicantAgent"); - - b.Navigation("ApplicationAssignments"); - - b.Navigation("ApplicationLinks"); - - b.Navigation("ApplicationTags"); - - b.Navigation("Assessments"); - }); - - modelBuilder.Entity("Unity.GrantManager.Applications.ApplicationStatus", b => - { - b.Navigation("Applications"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.PaymentRequests.PaymentRequest", b => - { - b.Navigation("ExpenseApprovals"); - - b.Navigation("PaymentTags"); - }); - - modelBuilder.Entity("Unity.Payments.Domain.Suppliers.Supplier", b => - { - b.Navigation("Sites"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs deleted file mode 100644 index 30ff6fe7b9..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260416000001_UpdateWorksheetViewGenCurrencyCommaFix.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using System; -using System.IO; -using System.Reflection; - -#nullable disable - -namespace Unity.GrantManager.Migrations.TenantMigrations -{ - /// - public partial class UpdateWorksheetViewGenCurrencyCommaFix : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = "Unity.GrantManager.Scripts.get_worksheet_data.sql"; - - using Stream stream = assembly.GetManifestResourceStream(resourceName) - ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}"); - using StreamReader reader = new(stream); - string sql = reader.ReadToEnd(); - migrationBuilder.Sql(sql); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - } - } -} diff --git a/documentation/reporting/get_worksheet_data_specification.md b/documentation/reporting/get_worksheet_data_specification.md index 5ea9962c76..97e5b398db 100644 --- a/documentation/reporting/get_worksheet_data_specification.md +++ b/documentation/reporting/get_worksheet_data_specification.md @@ -192,4 +192,4 @@ SELECT "Reporting".get_worksheet_data('correlation-id', 'report-map-id'); - **v1.3**: Fixed radio field handling to return text values - **v1.4**: Enhanced error handling and NULL type consistency - **v1.5**: Increased currency precision from `DECIMAL(10,2)` to `DECIMAL(18,2)` (tenant migration `20251125234153_UpdateViewGenCurrencyPrecision`) -- **v1.6**: Locale-formatted currency values are normalized by stripping commas and whitespace before numeric validation, so values such as `1,470.07` are persisted instead of becoming `NULL`. Paired with the `WorksheetFieldSchemaParser` mixed-DataGrid fix, which now emits mapping rows for statically-defined columns on mixed (dynamic + static) grids so the SQL function actually receives mappings to project. Deployed via tenant migration `20260416000001_UpdateWorksheetViewGenCurrencyCommaFix`. \ No newline at end of file +- **v1.6**: Locale-formatted currency values are normalized by stripping commas and whitespace before numeric validation, so values such as `1,470.07` are persisted instead of becoming `NULL`. Paired with the `WorksheetFieldSchemaParser` mixed-DataGrid fix, which now emits mapping rows for statically-defined columns on mixed (dynamic + static) grids so the SQL function actually receives mappings to project. This version also calls `Reporting.safe_to_jsonb(...)` for safe JSONB casting. Deployed via tenant migration `20260416000002_AddSafeToJsonbAndGuardDataGridLateral`, which applies `safe_to_jsonb.sql` first and then `get_worksheet_data.sql` in the correct dependency order. \ No newline at end of file From c70ddb16ed7e9a46ea3e9ab73d78540ecf0b390f Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 20 Apr 2026 15:05:42 -0700 Subject: [PATCH 019/134] AB#32709: Bugfix - Download All Is Not Working --- .../Components/ChefsAttachments/ChefsAttachments.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 158501f49a..e69751afbd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -6,7 +6,7 @@ $(function () { .trigger('click'); }; - const downloadAll = $('#downloadAll'); + const downloadAll = $('#downloadSelected'); const dt = $('#ChefsAttachmentsTable'); let chefsDataTable; let selectedAtttachments = []; @@ -158,7 +158,7 @@ $(function () { function resetAttachmentSelectionState() { selectedAtttachments = []; $('.select-all-chefs-files').prop('checked', false); - $('.chkbox').prop('checked', false); + chefsDataTable.$('.chkbox').prop('checked', false); $(downloadAll).prop('disabled', true); $generateAISummariesButton.prop('disabled', true); } @@ -329,8 +329,8 @@ $(function () { chefsDataTable.on('select', function (e, dt, type, indexes) { if (indexes?.length) { indexes.forEach((index) => { - $('#row_' + index).prop('checked', true); - if ($('.chkbox:checked').length == $('.chkbox').length) { + $(chefsDataTable.row(index).node()).find('.chkbox').prop('checked', true); + if (chefsDataTable.$('.chkbox:checked').length == chefsDataTable.$('.chkbox').length) { $('.select-all-chefs-files').prop('checked', true); } selectAttachment(type, index, 'select_chefs_file'); @@ -341,8 +341,8 @@ $(function () { chefsDataTable.on('deselect', function (e, dt, type, indexes) { if (indexes?.length) { indexes.forEach((index) => { - $('#row_' + index).prop('checked', false); - if ($('.chkbox:checked').length != $('.chkbox').length) { + $(chefsDataTable.row(index).node()).find('.chkbox').prop('checked', false); + if (chefsDataTable.$('.chkbox:checked').length != chefsDataTable.$('.chkbox').length) { $('.select-all-chefs-files').prop('checked', false); } selectAttachment(type, index, 'deselect_chefs_file'); From a8abfb620bd4c70c2e02efd21380e90e6461c8f3 Mon Sep 17 00:00:00 2001 From: Armin Hasanpour Date: Mon, 20 Apr 2026 16:52:17 -0700 Subject: [PATCH 020/134] AB#32301 - Fixed auth error in AI pipeline job via ForPipeline internal methods. --- .../Attachments/IAttachmentSummaryAppService.cs | 1 + .../IApplicationAnalysisAppService.cs | 1 + .../IApplicationScoringAppService.cs | 1 + .../Attachments/AttachmentSummaryAppService.cs | 16 ++++++++++++++++ .../ApplicationAnalysisAppService.cs | 11 ++++++++++- .../ApplicationScoringAppService.cs | 9 +++++++++ .../RunApplicationAIPipelineJob.cs | 6 +++--- .../RunApplicationAIPipelineJobTests.cs | 14 +++++++------- 8 files changed, 48 insertions(+), 11 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs index b9eb70ae3a..7b572a0703 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs @@ -9,4 +9,5 @@ public interface IAttachmentSummaryAppService : IApplicationService { Task GenerateAttachmentSummaryAsync(Guid attachmentId, string? promptVersion = null); Task> GenerateAttachmentSummariesAsync(List attachmentIds, string? promptVersion = null); + Task> GenerateAttachmentSummariesForPipelineAsync(List attachmentIds, string? promptVersion = null); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs index 21bf7c7867..9796792385 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs @@ -7,5 +7,6 @@ namespace Unity.GrantManager.GrantApplications public interface IApplicationAnalysisAppService : IApplicationService { Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + Task GenerateApplicationAnalysisForPipelineAsync(Guid applicationId, string? promptVersion = null); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs index c9a7fbab08..158f04b607 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs @@ -7,5 +7,6 @@ namespace Unity.GrantManager.GrantApplications public interface IApplicationScoringAppService : IApplicationService { Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null); + Task GenerateApplicationScoringForPipelineAsync(Guid applicationId, string? promptVersion = null); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs index 4f57fd4832..9a97ee9210 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Attachments/AttachmentSummaryAppService.cs @@ -48,4 +48,20 @@ public async Task> GenerateAttachmentSummariesA return results; } + + // Internal-only: no HTTP endpoint, no auth check — safe for background job callers + [AllowAnonymous] + [RemoteService(IsEnabled = false)] + public virtual async Task> GenerateAttachmentSummariesForPipelineAsync(List attachmentIds, string? promptVersion = null) + { + if (attachmentIds.Count == 0) return []; + + var results = new List(); + foreach (var attachmentId in attachmentIds) + { + await attachmentSummaryService.GenerateAndSaveAsync(attachmentId, promptVersion); + results.Add(new AttachmentSummaryResultDto { Completed = true }); + } + return results; + } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs index fad67fc536..c5d8814534 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs @@ -15,7 +15,7 @@ public class ApplicationAnalysisAppService( IFeatureChecker featureChecker) : AIAppService, IApplicationAnalysisAppService { - public async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) + public virtual async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) { if (!await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis")) { @@ -25,4 +25,13 @@ public async Task GenerateApplicationAnalysisAsync await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion); return new ApplicationAnalysisResultDto { Completed = true }; } + + // Internal-only: no HTTP endpoint, no auth check — safe for background job callers + [AllowAnonymous] + [RemoteService(IsEnabled = false)] + public virtual async Task GenerateApplicationAnalysisForPipelineAsync(Guid applicationId, string? promptVersion = null) + { + await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion); + return new ApplicationAnalysisResultDto { Completed = true }; + } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs index 753a622148..ff79dfef68 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationScoringAppService.cs @@ -51,4 +51,13 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent Completed = true }; } + + // Internal-only: no HTTP endpoint, no auth check — safe for background job callers + [AllowAnonymous] + [RemoteService(IsEnabled = false)] + public virtual async Task GenerateApplicationScoringForPipelineAsync(Guid applicationId, string? promptVersion = null) + { + await applicationScoringService.RegenerateAndSaveAsync(applicationId, promptVersion); + return new ApplicationScoringResultDto { Completed = true }; + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs index 9c06a650f5..3b55cf8093 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -43,7 +43,7 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) if (attachmentSummariesEnabled) { - var attachmentResults = await attachmentSummaryAppService.GenerateAttachmentSummariesAsync( + var attachmentResults = await attachmentSummaryAppService.GenerateAttachmentSummariesForPipelineAsync( await GetAttachmentIdsAsync(args.ApplicationId), args.PromptVersion); @@ -57,7 +57,7 @@ await GetAttachmentIdsAsync(args.ApplicationId), { try { - var analysisResult = await applicationAnalysisAppService.GenerateApplicationAnalysisAsync(args.ApplicationId, args.PromptVersion); + var analysisResult = await applicationAnalysisAppService.GenerateApplicationAnalysisForPipelineAsync(args.ApplicationId, args.PromptVersion); if (analysisResult.Completed) { logger.LogInformation("Completed AI application analysis stage for application {ApplicationId}.", args.ApplicationId); @@ -74,7 +74,7 @@ await GetAttachmentIdsAsync(args.ApplicationId), { try { - var result = await applicationScoringAppService.GenerateApplicationScoringAsync(args.ApplicationId, args.PromptVersion); + var result = await applicationScoringAppService.GenerateApplicationScoringForPipelineAsync(args.ApplicationId, args.PromptVersion); if (result.Completed) { await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs index 047d2a807d..64884e5c65 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs @@ -23,11 +23,11 @@ private static RunApplicationAIPipelineJob BuildJob( IApplicationScoringAppService? scoringService = null) { var attachmentService = Substitute.For(); - attachmentService.GenerateAttachmentSummariesAsync(Arg.Any>(), Arg.Any()) + attachmentService.GenerateAttachmentSummariesForPipelineAsync(Arg.Any>(), Arg.Any()) .Returns(Task.FromResult(new List())); var analysisService = Substitute.For(); - analysisService.GenerateApplicationAnalysisAsync(Arg.Any(), Arg.Any()) + analysisService.GenerateApplicationAnalysisForPipelineAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new ApplicationAnalysisResultDto { Completed = true })); return new RunApplicationAIPipelineJob( @@ -50,14 +50,14 @@ public async Task ExecuteAsync_Should_Skip_Scoring_When_Feature_Disabled() featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(false); var scoringService = Substitute.For(); - scoringService.GenerateApplicationScoringAsync(Arg.Any(), Arg.Any()) + scoringService.GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new ApplicationScoringResultDto { Completed = true })); var job = BuildJob(featureChecker, scoringService); await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = Guid.NewGuid() }); - await scoringService.DidNotReceive().GenerateApplicationScoringAsync(Arg.Any(), Arg.Any()); + await scoringService.DidNotReceive().GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()); } [Fact] @@ -69,14 +69,14 @@ public async Task ExecuteAsync_Should_Run_Scoring_When_Feature_Enabled() featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(false); var scoringService = Substitute.For(); - scoringService.GenerateApplicationScoringAsync(Arg.Any(), Arg.Any()) + scoringService.GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new ApplicationScoringResultDto { Completed = true })); var job = BuildJob(featureChecker, scoringService); await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = Guid.NewGuid() }); - await scoringService.Received(1).GenerateApplicationScoringAsync(Arg.Any(), Arg.Any()); + await scoringService.Received(1).GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()); } [Fact] @@ -88,7 +88,7 @@ public async Task ExecuteAsync_Should_Publish_Scoring_Event_When_Scoring_Complet featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(false); var scoringService = Substitute.For(); - scoringService.GenerateApplicationScoringAsync(Arg.Any(), Arg.Any()) + scoringService.GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new ApplicationScoringResultDto { Completed = true })); var eventBus = Substitute.For(); From cefdcf4c298af5385f4b50779a920594a140378b Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 21 Apr 2026 10:43:34 -0700 Subject: [PATCH 021/134] AB#32583 refine chefs toolbar layout --- .../ChefsAttachments/ChefsAttachments.js | 14 ++++------ .../ChefsAttachments/Default.cshtml | 26 ++++++++++--------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index e69751afbd..5466a20147 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -233,9 +233,7 @@ $(function () { $toggleAllAISummariesButton.on('click', function () { const $button = $(this); const $icon = $button.find('i'); - const $text = $button.contents().filter(function () { - return this.nodeType === 3; - }); + const $label = $button.find('.toggle-ai-summaries-label'); // Don't do anything if button is disabled if ($button.prop('disabled')) { @@ -262,7 +260,7 @@ $(function () { } }); $icon.removeClass('fa-chevron-up').addClass('fa-chevron-down'); - $text.replaceWith('Show Summaries'); + $label.text('Show Summaries'); $button.attr('title', 'Show AI Summaries'); allAISummariesExpanded = false; } else { @@ -290,7 +288,7 @@ $(function () { } }); $icon.removeClass('fa-chevron-down').addClass('fa-chevron-up'); - $text.replaceWith('Hide Summaries'); + $label.text('Hide Summaries'); $button.attr('title', 'Hide AI Summaries'); allAISummariesExpanded = true; } @@ -302,11 +300,9 @@ $(function () { if (allAISummariesExpanded) { const $button = $('#toggleAllAISummaries'); const $icon = $button.find('i'); - const $text = $button.contents().filter(function () { - return this.nodeType === 3; - }); + const $label = $button.find('.toggle-ai-summaries-label'); $icon.removeClass('fa-chevron-up').addClass('fa-chevron-down'); - $text.replaceWith('Show Summaries'); + $label.text('Show Summaries'); $button.attr('title', 'Show AI Summaries'); allAISummariesExpanded = false; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml index 3a2b2aa3a0..543a748c11 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml @@ -7,34 +7,36 @@
Submission Attachments
+ + + @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled) { } @if (ViewBag.IsAIAttachmentSummariesEnabled) { - } - - -
From b79ed0a54aad57e0272fbb89c9fb661067ba08b1 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 21 Apr 2026 11:09:06 -0700 Subject: [PATCH 022/134] AB#32583 keep chefs toolbar on one line --- .../ChefsAttachments/ChefsAttachments.css | 36 +++++++++++++++---- .../ChefsAttachments/Default.cshtml | 34 ++++++++++-------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.css index 6a9a913c3d..d3008b860e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.css @@ -5,13 +5,21 @@ } .attachments__title-button-split { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 8px; + margin-bottom: 8px; +} + +.submission-title-and-actions { display: flex; flex-direction: row; justify-content: space-between; - align-items: flex-start; - flex-wrap: wrap; - gap: 8px 8px; - margin-bottom: 8px; + align-items: center; + gap: 8px; + flex-wrap: nowrap; } .sorting_disabled:before, @@ -24,18 +32,28 @@ align-items: center; height: 36px; padding-top: 0; + flex: 0 0 auto; } .submission-button-section { display: flex; - flex: 1 1 0; - margin-left: auto; - flex-wrap: wrap; + flex: 1 1 auto; + flex-wrap: nowrap; align-items: center; align-content: center; justify-content: flex-end; gap: 6px; min-width: 0; + overflow-x: auto; + overflow-y: hidden; +} + +.submission-button-section--top { + justify-content: flex-end; +} + +.submission-button-section--bottom { + justify-content: flex-end; } .submission-button-section .btn { @@ -48,6 +66,10 @@ white-space: nowrap; } +.submission-button-section .btn + .btn { + margin-left: 0; +} + .submission-button-section .button-content { display: inline-flex; align-items: center; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml index 543a748c11..e59115427a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml @@ -3,22 +3,26 @@ @inject IStringLocalizer L
-
-
Submission Attachments
+
+
+
Submission Attachments
+
+
+ + + +
-
- - - +
@if (ViewBag.IsAIAttachmentSummariesGenerateEnabled) { + } + @if (Model.ShowPaymentSettings) + { + + } +
+ + +
+ + + @if (Model.ShowPaymentAccountCoding) + { + + } + + + @if (Model.ShowPaymentSettings) + { + + } + +
+
+
+ } + + + @if (Model.ShowCustomFields) + { +
+ +
+
+
+ @await Component.InvokeAsync(typeof(WorksheetListWidget)) +
+
+
+
+ + +

Preview

+
+

No sections to display.

+
+
+
+
+
+
+ } + + + @if (Model.ShowScoresheets) + { +
+
+
+
+ @await Component.InvokeAsync("Scoresheet") +
+
+
+
+ + +

Preview

+
+
+
+
+
+
+
+ } + + + @if (Model.ShowTags) + { +
+ @await Component.InvokeAsync(typeof(Unity.GrantManager.Web.Views.Settings.TagManagement.TagManagementViewComponent)) +
+ } + + + @if (Model.ShowAI) + { +
+ @await Component.InvokeAsync(typeof(Unity.AI.Web.Views.Settings.AISettingGroup.AISettingViewComponent)) +
+ } +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs new file mode 100644 index 0000000000..b60b82d183 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +using System; +using System.Threading.Tasks; + +using Unity.AI.Permissions; +using Unity.GrantManager.Permissions; +using Unity.Modules.Shared; +using Unity.Notifications.Permissions; +using Unity.Payments.PaymentConfigurations; +using Unity.Payments.Permissions; + +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Features; + +namespace Unity.GrantManager.Web.Pages.ConfigurationManagement; + +[Authorize(UnitySettingManagementPermissions.UserInterface)] +public class IndexModel( + IPaymentConfigurationAppService paymentConfigurationAppService, + IConfiguration configuration, + IFeatureChecker featureChecker, + IPermissionChecker permissionChecker) : GrantManagerPageModel +{ + public Guid? AccountCodingId { get; set; } + public string? PaymentIdPrefix { get; set; } + public string MaxFileSize { get; set; } = string.Empty; + + // Visibility flags + public bool ShowNotifications { get; set; } + public bool ShowPayments { get; set; } + public bool ShowPaymentAccountCoding { get; set; } + public bool ShowPaymentSettings { get; set; } + public bool ShowCustomFields { get; set; } + public bool ShowScoresheets { get; set; } + public bool ShowTags { get; set; } + public bool ShowAI { get; set; } + + public async Task OnGetAsync() + { + MaxFileSize = configuration["S3:MaxFileSize"] ?? ""; + + // Resolve feature + permission flags + ShowNotifications = await featureChecker.IsEnabledAsync("Unity.Notifications") + && await permissionChecker.IsGrantedAsync(NotificationsPermissions.Settings); + + bool isPaymentsFeatureEnabled = await featureChecker.IsEnabledAsync("Unity.Payments"); + bool isAuthorizedForPaymentConfiguration = await permissionChecker.IsGrantedAsync(UnitySettingManagementPermissions.ConfigurePayments); + ShowPayments = isPaymentsFeatureEnabled && isAuthorizedForPaymentConfiguration; + + ShowPaymentAccountCoding = ShowPayments + && await permissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.Default); + ShowPaymentSettings = ShowPayments + && await permissionChecker.IsGrantedAsync(UnitySelector.Payment.Summary.Default); + + ShowCustomFields = await featureChecker.IsEnabledAsync("Unity.Flex"); + ShowScoresheets = await featureChecker.IsEnabledAsync("Unity.Flex"); + + ShowTags = await permissionChecker.IsGrantedAsync(UnitySelector.SettingManagement.Tags.Default); + + ShowAI = await featureChecker.IsEnabledAsync("Unity.AI.Scoring") + && await permissionChecker.IsGrantedAsync(AIPermissions.Configuration.ConfigureAI); + + // Load payment data only if visible + if (ShowPayments) + { + var paymentConfiguration = await paymentConfigurationAppService.GetAsync(); + if (paymentConfiguration != null) + { + AccountCodingId = paymentConfiguration.DefaultAccountCodingId; + PaymentIdPrefix = paymentConfiguration.PaymentIdPrefix; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css new file mode 100644 index 0000000000..3e2796dc65 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css @@ -0,0 +1,117 @@ +/* Page layout: side menu + content side-by-side */ +.config-page-layout { + display: flex; + align-items: flex-start; + width: 100%; + gap: 0; +} + +#ConfigurationManagementSideMenu.side-menu { + position: sticky; + margin-top: 20px; + top: 100px; + flex-shrink: 0; + z-index: 10; +} + +#ConfigurationManagementSideMenu ul { + padding-left: 0; +} + +#ConfigurationManagementSideMenu li { + height: 40px; + padding: 10px; + min-width: 200px; + margin-top: 2px; + border-radius: 0 100em 100em 0; +} + +#ConfigurationManagementSideMenu .nav-item { + justify-content: left; +} + +.config-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; +} + +.config-management { + padding: 1rem; + max-height: 80vh; + overflow: auto; + margin-top: 20px; + width: 95%; +} + +.unity-app-main-container { + margin: auto; + display: flex; + align-items: flex-start; +} + +.hide { + display: none !important; +} + +/* Resizable split layout for Worksheets and Scoresheets */ +.config-split-container { + display: flex; + height: calc(80vh - 60px); + width: 100%; +} + +.config-split-left, +.config-split-right { + overflow-y: auto; +} + +.config-split-right { + width: 67%; + padding-left: 0; +} + +.config-split-left { + width: 33%; + padding-right: 0; +} + +.config-split-divider { + width: 10px; + background-color: #ccc; + cursor: col-resize; + border-radius: 10px; + flex-shrink: 0; +} + + .config-split-divider:hover { + background-color: #aaa; + } + +/* Preview panel (shared by Worksheets and Scoresheets) */ +.sticky-preview { + position: sticky; + top: 0; +} + +.preview-section { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: space-evenly; + align-items: center; +} + + .preview-section:has(> :nth-child(1)) .preview-field { + flex: 0 0 99%; + } + + .preview-section:has(> :nth-child(2)) .preview-field { + flex: 0 0 45%; + } + + .preview-section:has(> :nth-child(3)) .preview-field { + flex: 0 0 30%; + } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js new file mode 100644 index 0000000000..45bd4463e4 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js @@ -0,0 +1,122 @@ +$(function () { + const menuItems = $('#ConfigurationManagementSideMenu .nav-item'); + const configSections = $('.config-section'); + + init(); + + function init() { + menuItems.on('click', menuItemClick); + + // Adjust DataTables when Payments internal tabs are shown + $('button[data-bs-toggle="tab"]').on('shown.bs.tab', function () { + adjustDataTables(); + }); + + // Auto-select the first visible menu item + const firstMenuItem = menuItems.first(); + if (firstMenuItem.length) { + firstMenuItem.addClass('active'); + const targetId = firstMenuItem.data('target'); + $('#' + targetId).removeClass('hide'); + } + + // Auto-activate the first Payment tab (if rendered) + const firstPaymentTab = $('#payments-nav-tab .nav-link').first(); + if (firstPaymentTab.length) { + firstPaymentTab.addClass('active'); + const targetPane = $(firstPaymentTab.data('bs-target')); + if (targetPane.length) { + targetPane.addClass('show active'); + } + } + } + + const splitRestoreMap = { + 'custom-fields-div': initResizableSplit('worksheet-split-container', 'worksheet-left', 'worksheet-divider', 'worksheet-right', 'worksheetSplitWidth'), + 'scoresheets-div': initResizableSplit('scoresheet-split-container', 'scoresheet-left', 'scoresheet-divider', 'scoresheet-right', 'scoresheetSplitWidth') + }; + + function menuItemClick(e) { + const clickedItem = $(e.currentTarget); + const targetId = clickedItem.data('target'); + + // Update active menu item + menuItems.removeClass('active'); + clickedItem.addClass('active'); + + // Toggle content sections + configSections.addClass('hide'); + $('#' + targetId).removeClass('hide'); + + // Restore split widths now that the section is visible + if (splitRestoreMap[targetId]) { + splitRestoreMap[targetId](); + } + + adjustDataTables(); + } + + function adjustDataTables() { + // Adjust any visible DataTables after tab/section switch + if (typeof accountCodingDataTable !== 'undefined' && accountCodingDataTable) { + accountCodingDataTable.columns.adjust().draw(); + } + if (typeof paymentSettingsDataTable !== 'undefined' && paymentSettingsDataTable) { + paymentSettingsDataTable.columns.adjust().draw(); + } + $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust(); + } +}); +function initResizableSplit(containerId, leftId, dividerId, rightId, storageKey) { + const container = document.getElementById(containerId); + const leftDiv = document.getElementById(leftId); + const divider = document.getElementById(dividerId); + const rightDiv = document.getElementById(rightId); + + if (!container || !leftDiv || !divider || !rightDiv) { + return null; + } + + function restoreSavedWidth() { + const saved = localStorage.getItem(storageKey); + if (saved && container.clientWidth > 0) { + const containerWidth = container.clientWidth; + const percentage = Number.parseFloat(saved); + const leftWidth = containerWidth * percentage; + const rightWidth = containerWidth - leftWidth - divider.offsetWidth; + leftDiv.style.width = leftWidth + 'px'; + rightDiv.style.width = rightWidth + 'px'; + } + } + + function resize(e) { + const containerRect = container.getBoundingClientRect(); + const leftWidth = e.clientX - containerRect.left; + const rightWidth = containerRect.right - e.clientX - divider.offsetWidth; + + if (leftWidth > 200 && rightWidth > 200) { + leftDiv.style.width = leftWidth + 'px'; + rightDiv.style.width = rightWidth + 'px'; + + // Save as percentage for responsive recalculation + localStorage.setItem(storageKey, (leftWidth / container.clientWidth).toString()); + } + } + + divider.addEventListener('mousedown', function (e) { + e.preventDefault(); + + function stopResize() { + document.removeEventListener('mousemove', resize); + document.removeEventListener('mouseup', stopResize); + } + + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', stopResize); + }); + + // Recalculate on window resize (guard prevents no-op on hidden sections) + globalThis.addEventListener('resize', restoreSavedWidth); + + return restoreSavedWidth; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css new file mode 100644 index 0000000000..41c1a36acd --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css @@ -0,0 +1,73 @@ +.v-scroll { + max-height: 600px; + overflow-y: auto; + padding-right: 10px; + scroll-behavior: smooth; +} + +.tui-editor-body { + min-height: 250px; +} + +.note-text { + font-size: 12px; +} + +/* Group Users Table - Push remove button to far right */ +#groupUsersTable td:last-child, +#createGroupUsersTable td:last-child { + text-align: right !important; +} + +#groupUsersTable th:last-child, +#createGroupUsersTable th:last-child { + width: 30px !important; + min-width: 30px !important; + max-width: 30px !important; +} + +/* Fix DataTable empty message positioning */ +#groupUsersTable td.dataTables_empty, +#createGroupUsersTable td.dataTables_empty, +#groupUsersTable td.dt-empty, +#createGroupUsersTable td.dt-empty { + text-align: center !important; +} + +/* Ensure table maintains proper width */ +#createGroupUsersTable { + width: 100% !important; +} + +/* Add User Button Styling */ +.btn-add-user { + background-color: white !important; + border: 2px solid var(--bs-primary) !important; + color: var(--bs-primary) !important; + font-weight: 700; + padding: 0.25rem 0.5rem; + white-space: nowrap; + transition: all 0.15s ease-in-out; + border-radius: 4px; + font-size: 0.875rem; +} + + .btn-add-user:hover:not(:disabled) { + background-color: var(--bs-primary) !important; + color: white !important; + } + + .btn-add-user:disabled { + opacity: 0.5; + cursor: not-allowed; + border-color: var(--bs-secondary) !important; + color: var(--bs-secondary) !important; + } + + .btn-add-user i { + font-size: 0.8rem; + } + +.modal-footer { + margin-top: 10px; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css new file mode 100644 index 0000000000..07651b7b9e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css @@ -0,0 +1,92 @@ +/* PaymentConfigurations styles - local copy for ConfigurationManagement */ + +.readonly { + color: var(--bc-colors-grey-text-500); + background-color: var(--bc-colors-blue-100, #D8EAFD); + width: 332px !important; +} + +.currencyinput { + float: left; +} + +.currency { + text-align: right; + width: 100%; + margin-top: -31px; +} + +.dm-approval { + width: 100%; + max-width: 100%; +} + +.payment-form h4 { + padding: 4px; + font-weight: 700; +} + +.payment-form .mb-3:nth-child(-n+3) { + width: 30%; +} + +.payment-form .mb-3:nth-child(n+4) { + width: 45%; +} + +.payment-form .mb-3:nth-child(n+6) { + width: 100%; +} + +.submit-section { + display: inline-flex; + gap: 1rem; +} + +.note { + margin-left: 5px; +} + +.payment-prefix { + width: 200px; + margin-top: -7px; +} + +.field-validation-error { + float: inline-start; + display: block; + max-width: 100%; + width: 100%; +} + +#payments-div .modal-dialog { + max-width: 850px !important; + min-width: 850px !important; +} + +#payments-div input.form-control:focus, #payments-div input.form-control:active, #payments-div textarea.form-control:focus, #payments-div textarea.form-control:active, #payments-div .form-select:focus, #payments-div .form-select:active { + font-weight: inherit !important; +} + +#payment-settings-div { + width: 750px; +} + +#PaymentThreshold_Threshold { + text-align: right; +} + +label.display-input-label { + font-size: var(--bc-font-size-sm); + color: var(--bc-colors-grey-text-300); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 0; +} + +@media only screen and (max-width: 850px) { + .field-validation-error { + float: none; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js new file mode 100644 index 0000000000..8cd446f471 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js @@ -0,0 +1,429 @@ +let accountCodingDataTable; +let paymentSettingsDataTable; + +$(function () { + let createModal = new abp.ModalManager(abp.appPath + 'AccountCoding/CreateModal'); + let updateModal = new abp.ModalManager(abp.appPath + 'AccountCoding/UpdateModal'); + let updateThresholdModal = new abp.ModalManager(abp.appPath + 'PaymentThresholds/UpdateModal'); + const formatter = createNumberFormatter(); + + const l = abp.localization.getResource('GrantManager'); + toastr.options.positionClass = 'toast-top-center'; + + const UIElements = { + accountCodingDT: $('#AccountCodesDataTable'), + paymentSettingsDT: $('#PaymentSettingsDataTable'), + accountCodingId: $('#AccountCodingId'), + paymentPrefixSaveButton: $('#PaymentPrefixSaveButton'), + paymentPrefixDiscardButton: $('#PaymentPrefixDiscardButton'), + paymentPrefixInput: $('#payment-id-prefix'), + originalPaymentPrefix: $('#payment-id-prefix-original') + }; + + init(); + + function init() { + const hasAccountCodesDataTable = UIElements.accountCodingDT.length > 0; + const hasPaymentSettingsDataTable = UIElements.paymentSettingsDT.length > 0; + if (!hasAccountCodesDataTable && !hasPaymentSettingsDataTable) { + return; + } + if (hasAccountCodesDataTable) { + accountCodingDataTable = initializeAccountCodesDataTable(); + } + if (hasPaymentSettingsDataTable) { + paymentSettingsDataTable = initializePaymentSettingsDataTable(); + } + bindUIElements(); + } + + function bindUIElements() { + UIElements.paymentPrefixSaveButton.on('click', updatePaymentPrefix); + UIElements.paymentPrefixDiscardButton.on('click', discardPaymentPrefix); + UIElements.paymentPrefixInput.on('keyup', checkEnableDiscard); + } + + function bindModalElements() { + const UIElements = { + inputMinistryClient: $('input[name="AccountCoding.MinistryClient"]'), + inputResponsibility: $('input[name="AccountCoding.Responsibility"]'), + inputServiceLine: $('input[name="AccountCoding.ServiceLine"]'), + inputStob: $('input[name="AccountCoding.Stob"]'), + inputProjectNumber: $('input[name="AccountCoding.ProjectNumber"]'), + inputPaymentThreshold: $('#PaymentThreshold_Threshold'), + readOnlyAccountCoding: $('#account-coding') + }; + + UIElements.inputMinistryClient.on('keyup', setAccountCodingDisplay); + UIElements.inputResponsibility.on('keyup', setAccountCodingDisplay); + UIElements.inputServiceLine.on('keyup', setAccountCodingDisplay); + UIElements.inputStob.on('keyup', setAccountCodingDisplay); + UIElements.inputProjectNumber.on('keyup', setAccountCodingDisplay); + + UIElements.inputPaymentThreshold.on('keyup', preventDecimalKeyUp); + UIElements.inputPaymentThreshold.on('keypress', preventNonCurrencyKeyPress); + + + + + function setAccountCodingDisplay() { + let currentAccount = $(UIElements.inputMinistryClient).val() + "." + + $(UIElements.inputResponsibility).val() + "." + + $(UIElements.inputServiceLine).val() + "." + + $(UIElements.inputStob).val() + "." + + $(UIElements.inputProjectNumber).val(); + + $(UIElements.readOnlyAccountCoding).val(currentAccount); + } + + setAccountCodingDisplay(); + } + + function initializePaymentSettingsDataTable() { + let actionButtons = []; + const listColumns = getPaymentSettingsColumns(); + + const defaultVisibleColumns = [ + 'userName', + 'paymentThreshold', + 'description' + ]; + + let dt = UIElements.paymentSettingsDT; + return initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: { + name: 'userName', + dir: 'asc' + }, + dataEndpoint: unity.grantManager.payments.paymentSettings.getL2ApproversThresholds, + data: {}, + responseCallback: paymentSettingsResponseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'PaymentSettingsDataTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + disableColumnSelect: true, + externalSearchId: 'search-data-table' + }); + + + } + + function initializeAccountCodesDataTable() { + $.fn.dataTable.Buttons.defaults.dom.button.className = 'btn flex-none'; + let actionButtons = [ + { + text: ' ' + l('Common:Command:Create') + '', + titleAttr: l('Common:Command:Create'), + id: 'CreateButton', + className: 'btn-light rounded-1', + action: (e, dt, node, config) => createAccountCodingBtn(e) + } + ]; + + const listColumns = getAccountCodingColumns(); + + const defaultVisibleColumns = [ + 'ministryClient', + 'responsibility', + 'serviceLine', + 'stob', + 'projectNumber', + 'description', + 'defaultRadio', + 'rowActions', + ]; + + let dt = UIElements.accountCodingDT; + return initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: { + name: 'ministryClient', + dir: 'asc' + }, + dataEndpoint: unity.grantManager.payments.accountCoding.getList, + data: {}, + responseCallback: accountCodesResponseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'AccountCodesDataTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + disableColumnSelect: true, + externalSearchId: 'search-data-table' + }); + } + + function getAccountCodingColumns() { + let index = 0; + return [ + { + title: 'Ministry Client', + name: "ministryClient", + data: "ministryClient", + visible: true, + index: index++ + }, + { + title: 'Responsibility', + name: "responsibility", + data: "responsibility", + visible: true, + index: index++ + }, + { + title: 'Service Line', + name: "serviceLine", + data: "serviceLine", + visible: true, + index: index++ + }, + { + title: 'Stob', + name: "stob", + data: "stob", + visible: true, + index: index++ + }, + { + title: 'Project #', + name: "projectNumber", + data: "projectNumber", + visible: true, + index: index++ + }, + { + title: 'Description', + name: "description", + data: "description", + visible: true, + index: index++ + }, + { + title: 'Default', + orderable: false, + visible: true, + className: 'notexport text-center', + name: 'defaultRadio', + index: index++, + data: 'id', + render: function (data, type, full, meta) { + let checked = UIElements.accountCodingId.val() == data ? 'checked' : ''; + return ``; + } + }, + { + title: 'Action', + orderable: false, + sortable: false, + data: 'id', + className: 'notexport text-center', + name: 'rowActions', + visible: true, + index: index++, + rowAction: { + items: + [ + { + text: 'Edit', + action: (data) => editAccountCodingBtn(data.record.id) + } + ] + } + } + ]; + } + + createModal.onResult(function () { + accountCodingDataTable.ajax.reload(); + }); + + updateModal.onResult(function () { + accountCodingDataTable.ajax.reload(); + }); + + updateThresholdModal.onResult(function () { + paymentSettingsDataTable.ajax.reload(); + }); + + function editThresholdBtn(id, userName) { + updateThresholdModal.open({ id: id, userName: userName }); + updateThresholdModal.onOpen(function () { + bindModalElements(); + }); + } + + function editAccountCodingBtn(id) { + updateModal.open({ id: id }); + updateModal.onOpen(function () { + bindModalElements(); + }); + } + + function createAccountCodingBtn(e) { + e.preventDefault(); + createModal.open(); + createModal.onOpen(function () { + bindModalElements(); + }); + } + + function updatePaymentPrefix() { + unity.payments.paymentConfigurations.paymentConfiguration.updatePaymentPrefix(UIElements.paymentPrefixInput.val()) + .done(function () { + toastr.success('Payment prefix updated successfully.'); + $('#payment-id-prefix-original').val(UIElements.paymentPrefixInput.val()); + checkEnableDiscard(); + }) + .fail(function () { + toastr.error('Failed to update payment prefix.'); + }); + } + + function checkEnableDiscard() { + const originalPrefix = UIElements.originalPaymentPrefix.val(); + const currentPrefix = UIElements.paymentPrefixInput.val(); + UIElements.paymentPrefixDiscardButton.prop('disabled', currentPrefix === originalPrefix); + } + + function discardPaymentPrefix() { + UIElements.paymentPrefixInput.val(UIElements.originalPaymentPrefix.val()); + toastr.info('Payment prefix changes discarded.'); + checkEnableDiscard(); + } + + function getPaymentSettingsColumns() { + let index = 0; + return [ + { + title: 'Id', + name: "id", + data: "id", + visible: false, + index: index++ + }, + { + title: 'User Id', + name: "userId", + data: "userId", + visible: false, + index: index++ + }, + { + title: 'Expense Authority', + name: "userName", + data: "userName", + visible: true, + index: index++ + }, + { + title: 'Approval Threshold', + name: "paymentThreshold", + className: 'dt-body-right', + data: "threshold", + visible: true, + index: index++, + render: function (data, type, row) { + if (data == null || data === '') return ''; + return formatter.format(data); + } + }, + { + title: 'Description', + name: "description", + data: "description", + visible: true, + index: index++ + }, + { + title: 'Action', + orderable: false, + sortable: false, + data: 'id', + className: 'notexport text-center', + name: 'rowActions', + visible: true, + index: index++, + rowAction: { + items: + [ + { + text: 'Edit', + action: (data) => editThresholdBtn(data.record.id, data.record.userName) + } + ] + } + } + ]; + } +}); + +function paymentSettingsResponseCallback(result) { + return { + recordsTotal: result.length, + recordsFiltered: result.length, + data: result + }; +} + +function accountCodesResponseCallback(result) { + return { + recordsTotal: result.totalCount, + recordsFiltered: result.items.length, + data: result.items + }; +} + +function clearFilter() { + $('#search-data-table').val(''); + $('#search-data-table').trigger("keyup"); +} + +function handleDefaultAccountCodeRadioClick(id) { + $('#AccountCodingId').val(id); + unity.payments.paymentConfigurations.paymentConfiguration.setDefaultAccountCode(id).done(function () { + toastr.success('Successfully set default account code. Reloading account codes.'); + clearAccountCodesSearchAndReload(); + }).fail(function () { + toastr.error('Failed to set default account code.'); + }); +} + +function clearAccountCodesSearchAndReload() { + clearFilter(); + accountCodingDataTable.search('').draw(); + + localStorage.removeItem('DataTables_AccountCodesDataTable_/ConfigurationManagement'); + localStorage.removeItem('DataTables_PaymentSettingsDataTable_/ConfigurationManagement'); + + accountCodingDataTable.ajax.reload(); +} + +function preventNonCurrencyKeyPress(e) { + if (/[a-zA-Z]/.test(e.key) || e.key === ' ' || e.key === '-' || e.keyCode === 45) { + e.preventDefault(); + } +} + +function preventDecimalKeyUp(e) { + const input = e.target; + const cursorPosition = input.selectionStart; + const decimalMatch = input.value.match(/\.(\d+)/); + + if (decimalMatch && decimalMatch[1].length > 2) { + input.value = input.value.replace(/\.(\d{2}).*/, '.$1'); + input.setSelectionRange(cursorPosition, cursorPosition); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js new file mode 100644 index 0000000000..b60cbf7393 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js @@ -0,0 +1,103 @@ +/* ScoresheetConfiguration JS - local copy for ConfigurationManagement */ + +(function ($) { + const scoresheetModal = new abp.ModalManager({ + viewUrl: 'ScoresheetConfiguration/ScoresheetModal' + }); + + const cloneScoresheetModal = new abp.ModalManager({ + viewUrl: 'ScoresheetConfiguration/CloneScoresheetModal' + }); + + const publishScoresheetModal = new abp.ModalManager({ + viewUrl: 'ScoresheetConfiguration/PublishScoresheetModal' + }); + + let scoresheetToEditId = null; + + scoresheetModal.onResult(function (response) { + const actionType = $(response.currentTarget).find('#ActionType').val(); + if (actionType.startsWith('Delete')) { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: null }); + } else { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: scoresheetToEditId }); + } + abp.notify.success( + actionType + ' is successful.', + 'Scoresheet' + ); + }); + + cloneScoresheetModal.onResult(function (response) { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: null }); + abp.notify.success( + 'Scoresheet cloning is successful.', + 'Scoresheet' + ); + }); + + publishScoresheetModal.onResult(function (response) { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: scoresheetToEditId }); + abp.notify.success( + 'Scoresheet publishing is successful.', + 'Scoresheet' + ); + }); + + // Exposed globally — called from inline onclick attributes in Scoresheet component HTML + window.openScoresheetModal = function (scoresheetId, actionType) { + scoresheetToEditId = scoresheetId; + scoresheetModal.open({ + scoresheetId: scoresheetId, + actionType: actionType + }); + }; + + window.openCloneScoresheetModal = function (scoresheetId) { + scoresheetToEditId = scoresheetId; + cloneScoresheetModal.open({ + scoresheetId: scoresheetId + }); + }; + + window.openPublishScoresheetModal = function (scoresheetId) { + scoresheetToEditId = scoresheetId; + publishScoresheetModal.open({ + scoresheetId: scoresheetId + }); + }; + + function showAccordion(scoresheetId) { + if (!scoresheetId) { + return; + } + const accordionId = 'collapse-' + scoresheetId; + const accordion = document.getElementById(accordionId); + accordion.classList.add('show'); + + const buttonId = 'accordion-button-' + scoresheetId; + const accordionButton = document.getElementById(buttonId); + accordionButton.classList.remove('collapsed'); + } + + function refreshScoresheetInfoWidget(scoresheetId) { + const url = `../Flex/Widget/Scoresheet/Refresh`; + fetch(url) + .then(response => response.text()) + .then(data => { + document.getElementById('scoresheet-info-widget').innerHTML = data; + showAccordion(scoresheetId); + PubSub.publish('refresh_scoresheet_configuration_page'); + }) + .catch(error => { + console.error('Error refreshing scoresheet-info-widget:', error); + }); + } + + PubSub.subscribe( + 'refresh_scoresheet_list', + (msg, data) => { + refreshScoresheetInfoWidget(data.scoresheetId); + } + ); +})(jQuery); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js index 127d0257a0..3cfaf63254 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js @@ -1,9 +1,7 @@ const TagTypes = {}; -let userCanUpdate = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Update'); -let userCanDelete = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Delete'); -let addNewTagModal = new abp.ModalManager({ - viewUrl: 'Tags/CreateTagsModal' -}); +let userCanUpdate; +let userCanDelete; +let addNewTagModal; function defineTagSummaryColumnDefs() { const columnDefs = [ @@ -162,6 +160,12 @@ function getUnifiedTagSummaryAjax(requestData, callback, settings) { }); } $(function () { + userCanUpdate = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Update'); + userCanDelete = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Delete'); + addNewTagModal = new abp.ModalManager({ + viewUrl: 'Tags/CreateTagsModal' + }); + abp.log.debug('TagManagement.js initialized!'); abp.modals.RenameTag = function () { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml index 6e0cb90dd0..861e5710d7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml @@ -3,8 +3,6 @@ @inject IPermissionChecker PermissionChecker - -
From 26df1145c93a717c24c5c2946915bc466aa882af Mon Sep 17 00:00:00 2001 From: David Bright Date: Wed, 22 Apr 2026 13:55:54 -0700 Subject: [PATCH 027/134] Updating so the submitter column is using the 'createdBy' data from CHEFs instead of the user name which is usually blank --- .../src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js index 992a1d98f3..f8ac6a2f21 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js @@ -145,7 +145,7 @@ $(function () { }, { title: l('Submitter'), - data: 'name', + data: 'createdBy', render: function (data) { return data; } From 030dd9f97ce659cd3d3e11c475522f287a764dd3 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 22 Apr 2026 14:49:49 -0700 Subject: [PATCH 028/134] AB#32465 update OpenAI endpoint config --- .../appsettings.Development.json | 32 ++++++++--------- .../Unity.GrantManager.Web/appsettings.json | 35 +++++++++++++++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json index 074261e6ca..ac56ee2a5c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json @@ -152,24 +152,24 @@ "ApplicationScoring": { "MaxCompletionTokens": 5000 } - }, - "OpenAI": { - "ApiKey": "", - "Endpoint": "", - "Profiles": { - "Gpt4oMini": { - "ApiUrl": "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01", - "MaxTokensParameter": "max_tokens", - "Temperature": 0.3 - }, - "Gpt5Mini": { - "ApiUrl": "/openai/deployments/gpt-5-mini/chat/completions?api-version=2024-10-01-preview", - "MaxTokensParameter": "max_completion_tokens" - } - } + }, + "OpenAI": { + "ApiKey": "", + "Endpoint": "", + "Profiles": { + "Gpt4oMini": { + "ApiUrl": "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01", + "MaxTokensParameter": "max_tokens", + "Temperature": 0.3 + }, + "Gpt5Mini": { + "ApiUrl": "/openai/deployments/gpt-5-mini/chat/completions?api-version=2024-10-01-preview", + "MaxTokensParameter": "max_completion_tokens" + } + } }, "Logging": { - "EnablePromptFileLog": true + "EnablePromptFileLog": false } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index d25142ba6a..50a4bfa585 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -145,5 +145,40 @@ "MiniProfiler": { "Disabled": false, "CacheDuration": 30 + }, + + "Azure": { + "Operations": { + "Defaults": { + "Provider": "OpenAI", + "Profile": "Gpt4oMini", + "PromptVersion": "v1" + }, + "AttachmentSummary": { + "MaxCompletionTokens": 2000 + }, + "ApplicationAnalysis": { + "MaxCompletionTokens": 4000 + }, + "ApplicationScoring": { + "MaxCompletionTokens": 8000 + } + }, + "OpenAI": { + "Profiles": { + "Gpt4oMini": { + "ApiUrl": "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01", + "MaxTokensParameter": "max_tokens", + "Temperature": 0.3 + }, + "Gpt5Mini": { + "ApiUrl": "/openai/deployments/gpt-5-mini/chat/completions?api-version=2024-10-01-preview", + "MaxTokensParameter": "max_completion_tokens" + } + } + }, + "Logging": { + "EnablePromptFileLog": false + } } } \ No newline at end of file From b6c46926418221c5af22187bdb34190413048541 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 22 Apr 2026 14:57:02 -0700 Subject: [PATCH 029/134] Max token update --- .../src/Unity.GrantManager.Web/appsettings.Development.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json index ac56ee2a5c..da702b7109 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.Development.json @@ -144,13 +144,13 @@ "PromptVersion": "v1" }, "AttachmentSummary": { - "MaxCompletionTokens": 1500 + "MaxCompletionTokens": 2000 }, "ApplicationAnalysis": { - "MaxCompletionTokens": 2500 + "MaxCompletionTokens": 4000 }, "ApplicationScoring": { - "MaxCompletionTokens": 5000 + "MaxCompletionTokens": 8000 } }, "OpenAI": { From 5c7950be18357864cdeb60cffac1b02cd9287f64 Mon Sep 17 00:00:00 2001 From: David Bright Date: Wed, 22 Apr 2026 15:33:02 -0700 Subject: [PATCH 030/134] Implement consistent disabling of save buttons during async save to prevent duplicate operations, as well as additional re-enabling of buttons on failure or completion. Core method in zone-extensions.js for setSaving --- .../wwwroot/themes/ux2/zone-extensions.js | 20 +++++++++++++------ .../Pages/ApplicationForms/Mapping.js | 10 ++++++++++ .../Pages/GrantApplications/Details.js | 3 +++ .../Components/ApplicantContacts/Default.js | 2 ++ .../Components/ApplicantHistory/Default.js | 2 ++ .../Components/ApplicantInfo/Default.js | 3 +++ .../AssessmentScoresWidget/Default.js | 9 ++++++--- .../Shared/Components/EmailsWidget/Default.js | 2 ++ .../FundingAgreementInfo/Default.js | 4 ++++ .../PaymentConfiguration/Default.js | 5 +++++ .../Shared/Components/ProjectInfo/Default.js | 6 +++++- 11 files changed, 56 insertions(+), 10 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js index 1003da21fe..188fe000fc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js @@ -4,7 +4,7 @@ } /** - * Unflatten dot separated JSON objects into nested objects + * Unflatten dot separated JSON objects into nested objects */ $.fn.unflattenObject = function(flatObj) { const result = {}; @@ -268,6 +268,14 @@ class UnityChangeTrackingForm { this.saveButton.prop('disabled', this.modifiedFields.size === 0); } + setSaving(isSaving) { + if (isSaving) { + this.saveButton.prop('disabled', true); + } else { + this.updateSaveButtonState(); + } + } + /** * Reset tracking without changing values */ @@ -354,7 +362,7 @@ class UnityZoneForm extends UnityChangeTrackingForm { // NOTE Get field value by name or id isValid() { - return this.form.valid(); + return this.form.valid(); } initializeNumericFields() { @@ -441,10 +449,10 @@ class UnityZoneForm extends UnityChangeTrackingForm { let expandedProperties = { 'name': name, 'tag': this.tagName.toLowerCase(), - 'type': this.type - }; - - tableOutput = { ...tableOutput, ...expandedProperties }; + 'type': this.type + }; + + tableOutput = { ...tableOutput, ...expandedProperties }; } let changeProperties = { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js index f24fa44924..1cc5e6af97 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js @@ -136,6 +136,7 @@ let jsonText = $('#jsonText').val(); $.parseJSON(jsonText); let mappingJsonStr = jsonText.replace(/\s+/g, ' ').replace(/(\r\n|\n|\r)/gm, ""); + UIElements.btnSaveMapping.prop('disabled', true); handleSaveMapping($.parseJSON(mappingJsonStr)); handleCancelMapping(); @@ -150,6 +151,7 @@ } catch (err) { + UIElements.btnSaveMapping.prop('disabled', false); abp.notify.error( '', 'The JSON is not valid:' + err @@ -313,6 +315,7 @@ formData["availableChefsFields"] = document.getElementById('availableChefsFields').value; formData["ChefsApplicationFormGuid"] = document.getElementById('applicationFormId').value; + UIElements.btnSave.prop('disabled', true); $.ajax( { url: "/api/app/application-form-version/" + formVersionId, @@ -332,6 +335,9 @@ data.responseText, 'Mapping Not Saved Successful' ); + }, + complete: function () { + UIElements.btnSave.prop('disabled', false); } } ); @@ -640,6 +646,7 @@ }; btnSaveAIConfig.addEventListener('click', function () { + btnSaveAIConfig.disabled = true; abp.ajax({ url: `/api/app/application-form/${aiFormId}/ai-config`, type: 'PATCH', @@ -658,6 +665,9 @@ }) .fail(function () { abp.notify.error('Failed to save AI configuration.'); + }) + .always(function () { + btnSaveAIConfig.disabled = false; }); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index 41ecd22540..d0d87af7c8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -1305,6 +1305,9 @@ function updateCustomForm( .update(customFormUpdate) .done(function () { abp.notify.success('Information has been updated.'); + }) + .fail(function () { + $(`#${saveId}`).prop('disabled', false); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js index de2d963e45..e59d9a1039 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js @@ -140,6 +140,7 @@ $(function () { } }; + zoneForm.setSaving(true); unity.grantManager.applicants.applicant .updateApplicantContactAddresses(applicantId, payload) .done(function () { @@ -149,6 +150,7 @@ $(function () { }) .fail(function () { abp.notify.error('Failed to update contact.'); + zoneForm.setSaving(false); }); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js index 61450ef2d4..c9688e999e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js @@ -23,6 +23,7 @@ $(function () { return; } + zoneForm.setSaving(true); unity.grantManager.applicantProfile.applicantHistory .saveNotes(applicantId, { fundingHistoryComments: $('#FundingHistoryComments').val(), @@ -35,6 +36,7 @@ $(function () { }) .fail(function () { abp.notify.error('Failed to save history notes.'); + zoneForm.setSaving(false); }); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js index f85c5e42b4..b782da861c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js @@ -90,6 +90,7 @@ abp.widgets.ApplicantInfo = function ($wrapper) { let applicationId = document.getElementById('ApplicantInfo_ApplicationId').value; let applicantInfoSubmission = self.getPartialUpdate(); + self.zoneForm.setSaving(true); try { unity.grantManager.grantApplications.applicationApplicant .updatePartialApplicantInfo(applicationId, applicantInfoSubmission) @@ -102,10 +103,12 @@ abp.widgets.ApplicantInfo = function ($wrapper) { .fail(function (error) { abp.notify.error('Failed to update Applicant Info.'); console.log(error); + self.zoneForm.setSaving(false); }); } catch (error) { abp.notify.error('An unexpected error occurred.'); console.log(error); + self.zoneForm.setSaving(false); } }); }, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 428d691d21..fb6bcdc61c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -34,6 +34,8 @@ function saveScoresSection(formId, sectionId) { }; //Calls an enpoint and disabled buttons + secSaveButton.disabled = true; + secDiscardButton.disabled = true; unity.grantManager.assessments.assessment .saveScoresheetSectionAnswers(data) .done(function () { @@ -52,14 +54,15 @@ function saveScoresSection(formId, sectionId) { } } - secSaveButton.disabled = true; - secDiscardButton.disabled = true; - updateSubtotal(); PubSub.publish( 'refresh_review_list_without_sidepanel', assessmentId ); + }) + .fail(function () { + secSaveButton.disabled = false; + secDiscardButton.disabled = false; }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js index fadcee3017..375a71bf30 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js @@ -334,6 +334,7 @@ templateName = $('#templateText').val(); } + UIElements.btnSave.prop('disabled', true); unity.grantManager.emails.email .saveDraft({ emailId: UIElements.inputEmailId[0].value, @@ -353,6 +354,7 @@ abp.notify.success('Your email has been saved.'); PubSub.publish('refresh_application_emails'); }).catch(function () { + UIElements.btnSave.prop('disabled', false); abp.notify.error('An error ocurred your email could not be saved.'); }); } else { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js index 285a05f607..f74ebbc6b9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js @@ -49,6 +49,7 @@ } function updateFundingAgreementInfo(applicationId, fundingAgreementInfoObj) { + $('#saveFundingAgreementInfoBtn').prop('disabled', true); try { unity.grantManager.grantApplications.grantApplication .updateFundingAgreementInfo(applicationId, fundingAgreementInfoObj) @@ -59,6 +60,9 @@ $('#saveFundingAgreementInfoBtn').prop('disabled', true); PubSub.publish('funding_agreement_info_saved', fundingAgreementInfoObj); PubSub.publish('refresh_detail_panel_summary'); + }) + .fail(function () { + $('#saveFundingAgreementInfoBtn').prop('disabled', false); }); } catch (error) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js index 44e7a40851..c9d3e5bbe8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js @@ -197,6 +197,8 @@ return; } + UIElements.btnSave.prop('disabled', true); + const hierarchyValue = UIElements.formHierarchy.val(); const formHierarchy = hierarchyValue ? parseInt(hierarchyValue, 10) : null; const parentFormId = UIElements.parentFormSelect.val(); @@ -235,6 +237,9 @@ confirmButton: 'btn btn-primary' } }); + }) + .catch(() => { + UIElements.btnSave.prop('disabled', false); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js index f2101f7c80..190f22e6fb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js @@ -90,6 +90,7 @@ abp.widgets.ProjectInfo = function ($wrapper) { data: modifiedFieldData }; + self.zoneForm.setSaving(true); try { unity.grantManager.grantApplications.grantApplication .updatePartialProjectInfo(applicationId, projectInfoSubmission) @@ -98,11 +99,14 @@ abp.widgets.ProjectInfo = function ($wrapper) { self.zoneForm.resetTracking(); PubSub.publish('project_info_saved', projectInfoObj); PubSub.publish('refresh_detail_panel_summary'); + }) + .fail(function () { + self.zoneForm.setSaving(false); }); } catch (error) { console.log(error); - self.zoneForm.resetTracking(); + self.zoneForm.setSaving(false); } }); From 77e148afaca5a89ff94ae8b425f3fbbae23c109a Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 22 Apr 2026 17:31:25 -0700 Subject: [PATCH 031/134] AB#32465 update OpenAI endpoint resolution config --- .../AI/Runtime/OpenAIConfigurationResolver.cs | 16 ++-------- .../Unity.GrantManager.Web/appsettings.json | 2 ++ .../OpenAIConfigurationResolverTests.cs | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index 1f8da1b91b..25fb917d21 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -139,18 +139,8 @@ private static string CombineEndpointAndPath(string endpoint, string profilePath { const char UrlPathSeparator = '/'; - if (Uri.TryCreate(profilePath, UriKind.Absolute, out var absoluteUri)) - { - return absoluteUri.ToString(); - } - - var trimmedEndpoint = endpoint.Trim().TrimEnd(UrlPathSeparator); - var trimmedPath = profilePath.Trim(); - if (!trimmedPath.StartsWith(UrlPathSeparator)) - { - trimmedPath = string.Concat(UrlPathSeparator, trimmedPath); - } - - return trimmedEndpoint + trimmedPath; + return endpoint.Trim().TrimEnd(UrlPathSeparator) + + UrlPathSeparator + + profilePath.Trim().TrimStart(UrlPathSeparator); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index 50a4bfa585..b74c91d2f4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -165,6 +165,8 @@ } }, "OpenAI": { + "ApiKey": "", + "Endpoint": "", "Profiles": { "Gpt4oMini": { "ApiUrl": "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01", diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs new file mode 100644 index 0000000000..aa95ac7b8a --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using Shouldly; +using System.Collections.Generic; +using Unity.AI.Runtime; +using Xunit; + +namespace Unity.GrantManager.AI.Runtime; + +public class OpenAIConfigurationResolverTests +{ + [Fact] + public void ResolveApiUrl_Should_CombineEndpointWithLeadingSlashProfilePath() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:Operations:Defaults:Profile"] = "Gpt4oMini", + ["Azure:OpenAI:Endpoint"] = "https://d837ad-test-recap-webapp.azurewebsites.net", + ["Azure:OpenAI:Profiles:Gpt4oMini:ApiUrl"] = "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01" + }) + .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + resolver.ResolveApiUrl().ShouldBe( + "https://d837ad-test-recap-webapp.azurewebsites.net/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01"); + } +} From 0c9a3e39df0507598bf2356fb986478601c55691 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Wed, 22 Apr 2026 18:48:36 -0700 Subject: [PATCH 032/134] AB#32694 applicant profile contacts alignment and refactor --- .../Contacts/ApplicantContactRoleOptions.cs | 25 + .../Contacts/IApplicantContactAppService.cs | 38 ++ .../IApplicantContactQueryService.cs} | 17 +- .../Contacts/UpdateApplicantContactDto.cs | 42 ++ .../IApplicantProfileDataProvider.cs | 0 .../ProfileData/ContactInfoItemDto.cs | 1 + .../{ => Queries}/ApplicantProfileDto.cs | 0 .../{ => Queries}/ApplicantProfileRequest.cs | 0 .../IApplicantProfileQueryService.cs} | 9 +- .../Contacts/IContactAppService.cs | 13 + .../Contacts/UpdateContactDto.cs | 35 ++ ...ApplicationPermissionDefinitionProvider.cs | 1 + .../AppServices/ApplicantContactAppService.cs | 73 +++ .../ApplicantProfileAppService.cs | 213 --------- .../ApplicantProfileContactService.cs | 145 ------ .../AddressInfoDataProvider.cs | 0 .../ContactInfoDataProvider.cs | 9 +- .../OrgInfoDataProvider.cs | 0 .../PaymentInfoDataProvider.cs | 0 .../SubmissionInfoDataProvider.cs | 0 .../Queries/ApplicantContactQueryService.cs | 324 +++++++++++++ .../Queries/ApplicantProfileQueryService.cs | 114 +++++ .../ApplicantTenantMapReconciler.cs | 121 +++++ .../IApplicantTenantMapReconciler.cs | 19 + .../ApplicantTenantMapReconciliationWorker.cs | 10 +- .../Contacts/ContactAppService.cs | 122 +++-- .../Localization/GrantManager/en.json | 76 ++- .../Contacts/ContactInput.cs | 14 + .../Contacts/ContactManager.cs | 162 +++++++ .../Contacts/IContactManager.cs | 42 ++ .../Controllers/ApplicantProfileController.cs | 6 +- .../GrantManagerWebAutoMapperProfile.cs | 17 + .../ApplicantContactModalViewModel.cs | 72 +++ .../Pages/ApplicantContact/EditModal.cshtml | 45 ++ .../ApplicantContact/EditModal.cshtml.cs | 59 +++ .../ApplicantContactsController.cs | 1 - .../ApplicantContactsViewComponent.cs | 154 +------ .../ApplicantContactsViewModel.cs | 47 +- .../ApplicantContacts/Default.cshtml | 137 +++--- .../Components/ApplicantContacts/Default.css | 106 +++++ .../Components/ApplicantContacts/Default.js | 434 ++++++++++++------ .../ChefsAttachments/ChefsAttachments.cs | 44 +- .../ChefsAttachments/Default.cshtml | 4 +- .../ApplicantContactAppServiceTests.cs | 129 ++++++ .../ApplicantProfileDataProviderTests.cs | 10 +- .../ContactInfoDataProviderTests.cs | 48 +- ...licantContactQueryServiceApplicantTests.cs | 300 ++++++++++++ ...plicantContactQueryServiceSubjectTests.cs} | 8 +- .../ApplicantProfileQueryServiceTests.cs | 121 +++++ .../Contacts/ContactAppServiceTests.cs | 409 +++++------------ .../ApplicantContactsWidgetTests.cs | 78 ++++ 51 files changed, 2661 insertions(+), 1193 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/{IApplicantProfileContactService.cs => Contacts/IApplicantContactQueryService.cs} (69%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/{ => DataProviders}/IApplicantProfileDataProvider.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/{ => Queries}/ApplicantProfileDto.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/{ => Queries}/ApplicantProfileRequest.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/{IApplicantProfileAppService.cs => Queries/IApplicantProfileQueryService.cs} (50%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/{ => DataProviders}/AddressInfoDataProvider.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/{ => DataProviders}/ContactInfoDataProvider.cs (77%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/{ => DataProviders}/OrgInfoDataProvider.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/{ => DataProviders}/PaymentInfoDataProvider.cs (100%) rename applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/{ => DataProviders}/SubmissionInfoDataProvider.cs (100%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/AppServices/ApplicantContactAppServiceTests.cs rename applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/{Applicants => ApplicantProfile/DataProviders}/ApplicantProfileDataProviderTests.cs (95%) rename applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/{Contacts => ApplicantProfile/DataProviders}/ContactInfoDataProviderTests.cs (78%) create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/Queries/ApplicantContactQueryServiceApplicantTests.cs rename applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/{Contacts/ContactInfoServiceTests.cs => ApplicantProfile/Queries/ApplicantContactQueryServiceSubjectTests.cs} (98%) create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/ApplicantProfile/Queries/ApplicantProfileQueryServiceTests.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicantContactsWidgetTests.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs new file mode 100644 index 0000000000..ecc917b600 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/ApplicantContactRoleOptions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Canonical set of contact role/type options exposed to the internal +/// Applicant Contacts UI. Mirrors the options defined by the Applicant Portal. +/// +public static class ApplicantContactRoleOptions +{ + /// Machine key used when storing the role on . + public static readonly IReadOnlyList Options = + [ + new("General", "General"), + new("Primary", "Primary Contact"), + new("Financial", "Financial Officer"), + new("SigningAuthority", "Additional Signing Authority"), + new("Executive", "Executive") + ]; +} + +/// Applicant contact role option (code + human-readable label). +/// The role key stored in . +/// The label displayed in the UI. +public record ApplicantContactRoleOption(string Value, string Label); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs new file mode 100644 index 0000000000..73bf5a52d8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactAppService.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Contacts; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Authorized application service that exposes Create / Update / SetPrimary +/// operations for applicant-scoped contacts (those linked to the Applicant aggregate +/// via with RelatedEntityType = "Applicant"), +/// and aggregated read access that combines applicant-linked, application, and +/// applicant-agent contacts into a single view model. +/// +/// This is the HTTP-facing surface used by the internal Applicant Contacts widget. +/// The underlying remains unexposed so that existing +/// Applicant Portal message handlers (which write directly to the repositories) are +/// unaffected by authorization. +/// +/// +public interface IApplicantContactAppService : IApplicationService +{ + /// + /// Retrieves the aggregated contact info for the specified applicant. + /// + Task GetByApplicantIdAsync(Guid applicantId); + + /// + /// Updates an existing applicant-linked contact. + /// + Task UpdateAsync(Guid applicantId, Guid contactId, UpdateApplicantContactDto input); + + /// + /// Flags the specified contact as primary for the given applicant. + /// + Task SetPrimaryAsync(Guid applicantId, Guid contactId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactQueryService.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactQueryService.cs index 779b4f7148..c0e7c9b9c6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/IApplicantContactQueryService.cs @@ -7,11 +7,11 @@ namespace Unity.GrantManager.ApplicantProfile; /// /// Provides applicant-profile-specific contact retrieval operations. -/// This service aggregates contacts from three sources: profile-linked contacts, +/// This query service aggregates contacts from three sources: profile-linked contacts, /// application-level contacts matched by OIDC subject, and applicant agent /// contacts derived from the submission login token. /// -public interface IApplicantProfileContactService +public interface IApplicantContactQueryService { /// /// Retrieves contacts linked to the applicant profile by resolving applicant IDs from @@ -39,4 +39,17 @@ public interface IApplicantProfileContactService /// The OIDC subject identifier (e.g. "user@idir"). /// A list of with IsEditable set to false. Task> GetApplicantAgentContactsBySubjectAsync(string subject); + + /// + /// Retrieves the aggregated contact info for the specified applicant, combining + /// applicant-linked contacts ( with + /// RelatedEntityType = "Applicant"), application contacts, and applicant agent contacts + /// for every application owned by the applicant. Applicant-linked contacts are marked editable; + /// application and agent contacts are always read-only. The primary flag is resolved either from + /// an explicit IsPrimary contact link or, if none exists, by falling back to the most + /// recently created contact. + /// + /// The unique identifier of the applicant. + /// A populated . + Task GetByApplicantIdAsync(Guid applicantId); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs new file mode 100644 index 0000000000..da092fda00 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Contacts/UpdateApplicantContactDto.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Input DTO for updating an existing applicant-scoped contact. +/// +public class UpdateApplicantContactDto +{ + /// Role/type key — see . + [Required] + [MaxLength(100)] + public string? Role { get; set; } + + /// Full name. + [Required] + [MinLength(2)] + [MaxLength(250)] + public string Name { get; set; } = string.Empty; + + /// Job title. + [MaxLength(200)] + public string? Title { get; set; } + + /// Email address. + [EmailAddress] + public string? Email { get; set; } + + /// Mobile phone number. + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$")] + public string? MobilePhoneNumber { get; set; } + + /// Work phone number. + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$")] + public string? WorkPhoneNumber { get; set; } + + /// Work phone extension. + public string? WorkPhoneExtension { get; set; } + + /// When true, other primary contact links for the applicant are demoted. + public bool IsPrimary { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/DataProviders/IApplicantProfileDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/DataProviders/IApplicantProfileDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs index ac0ec9b771..065223bd34 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs @@ -15,6 +15,7 @@ public class ContactInfoItemDto public string? ContactType { get; set; } public string? Role { get; set; } public bool IsPrimary { get; set; } + public bool IsPrimaryInferred { get; set; } public bool IsEditable { get; set; } public Guid? ApplicationId { get; set; } public string? ReferenceNo { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileDto.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileDto.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileRequest.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ApplicantProfileRequest.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/ApplicantProfileRequest.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/IApplicantProfileQueryService.cs similarity index 50% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/IApplicantProfileQueryService.cs index feb3645823..b678390e7f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/Queries/IApplicantProfileQueryService.cs @@ -4,11 +4,14 @@ namespace Unity.GrantManager.ApplicantProfile { - public interface IApplicantProfileAppService + /// + /// Internal query service that aggregates applicant profile data and tenant mappings for the + /// ApplicantProfileController API surface. Not an ABP application service — the + /// controller handles routing, authorization and API exposure directly. + /// + public interface IApplicantProfileQueryService { Task GetApplicantProfileAsync(ApplicantProfileInfoRequest request); Task> GetApplicantTenantsAsync(ApplicantProfileRequest request); - Task<(int Created, int Updated)> ReconcileApplicantTenantMapsAsync(); } } - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs index 4a07057c5d..3aeee4c050 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/IContactAppService.cs @@ -27,6 +27,19 @@ public interface IContactAppService /// The created . Task CreateContactAsync(CreateContactLinkDto input); + /// + /// Updates an existing contact and synchronizes the primary / role flags on its link + /// to the specified related entity. If is true, + /// any other active primary links for the same entity are demoted. + /// + /// The type of the related entity. + /// The unique identifier of the related entity. + /// The unique identifier of the contact to update. + /// The updated contact details. + /// The updated . + /// Thrown when no active contact link is found for the given parameters. + Task UpdateContactAsync(string entityType, Guid entityId, Guid contactId, UpdateContactDto input); + /// /// Sets the specified contact as the primary contact for the given entity. /// Only one primary contact is allowed per entity type and entity ID; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs new file mode 100644 index 0000000000..782c9562e7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Contacts/UpdateContactDto.cs @@ -0,0 +1,35 @@ +namespace Unity.GrantManager.Contacts; + +/// +/// Input DTO for updating an existing contact and (optionally) the primary/role flags +/// of its link to a related entity. +/// +public class UpdateContactDto +{ + /// The full name of the contact. + public string Name { get; set; } = string.Empty; + + /// The job title of the contact. + public string? Title { get; set; } + + /// The email address of the contact. + public string? Email { get; set; } + + /// The home phone number of the contact. + public string? HomePhoneNumber { get; set; } + + /// The mobile phone number of the contact. + public string? MobilePhoneNumber { get; set; } + + /// The work phone number of the contact. + public string? WorkPhoneNumber { get; set; } + + /// The work phone extension of the contact. + public string? WorkPhoneExtension { get; set; } + + /// The role of the contact within the linked entity context. + public string? Role { get; set; } + + /// Whether this contact should be flagged as the primary contact. When true, other primary links for the same entity are demoted. + public bool IsPrimary { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs index 5dae52cf3e..73236d3859 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Permissions/GrantApplications/GrantApplicationPermissionDefinitionProvider.cs @@ -172,6 +172,7 @@ public static void AddApplication_ApplicantInfo_Permissions(this PermissionGroup var upx_Applicant_Summary_Update = upx_Applicant_Summary.AddUnityChild(UnitySelector.Applicant.Summary.Update); var upx_Applicant_Contact = upx_Applicant.AddUnityChild(UnitySelector.Applicant.Contact.Default); + var upx_Applicant_Contact_Create = upx_Applicant_Contact.AddUnityChild(UnitySelector.Applicant.Contact.Create); var upx_Applicant_Contact_Update = upx_Applicant_Contact.AddUnityChild(UnitySelector.Applicant.Contact.Update); var upx_Applicant_Authority = upx_Applicant.AddUnityChild(UnitySelector.Applicant.Authority.Default); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs new file mode 100644 index 0000000000..a24bcd3391 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AppServices/ApplicantContactAppService.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Contacts; +using Unity.Modules.Shared; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Authorized facade over (reads) and +/// (writes) for the internal Applicant Contacts widget. +/// All writes are scoped to RelatedEntityType = "Applicant". +/// +[Authorize] +public class ApplicantContactAppService( + IApplicantContactQueryService applicantContactQueryService, + IContactManager contactManager) + : GrantManagerAppService, IApplicantContactAppService +{ + private const string ApplicantEntityType = "Applicant"; + + /// + [Authorize(UnitySelector.Applicant.Contact.Default)] + public virtual Task GetByApplicantIdAsync(Guid applicantId) + { + return applicantContactQueryService.GetByApplicantIdAsync(applicantId); + } + + /// + [Authorize(UnitySelector.Applicant.Contact.Update)] + public virtual async Task UpdateAsync(Guid applicantId, Guid contactId, UpdateApplicantContactDto input) + { + ArgumentNullException.ThrowIfNull(input); + + var (contact, link) = await contactManager.UpdateAsync( + ApplicantEntityType, + applicantId, + contactId, + new ContactInput( + input.Name, + input.Title, + input.Email, + HomePhoneNumber: null, + input.MobilePhoneNumber, + input.WorkPhoneNumber, + input.WorkPhoneExtension), + input.Role, + input.IsPrimary); + + return new ContactDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + Role = link.Role, + IsPrimary = link.IsPrimary + }; + } + + /// + [Authorize(UnitySelector.Applicant.Contact.Update)] + public virtual async Task SetPrimaryAsync(Guid applicantId, Guid contactId) + { + await contactManager.SetPrimaryAsync(ApplicantEntityType, applicantId, contactId); + return true; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs deleted file mode 100644 index bcb856bb16..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileAppService.cs +++ /dev/null @@ -1,213 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Unity.GrantManager.Applicants; -using Unity.GrantManager.Applications; -using Unity.Notifications.Settings; -using Volo.Abp; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Settings; -using Volo.Abp.TenantManagement; - -namespace Unity.GrantManager.ApplicantProfile -{ - [RemoteService(false)] - public class ApplicantProfileAppService( - ICurrentTenant currentTenant, - ITenantRepository tenantRepository, - IRepository applicantTenantMapRepository, - IRepository applicationFormSubmissionRepository, - IEnumerable dataProviders, - ISettingProvider settingProvider) - : ApplicationService, IApplicantProfileAppService - { - private readonly Dictionary _providersByKey - = dataProviders.ToDictionary(p => p.Key, StringComparer.OrdinalIgnoreCase); - - /// - /// Retrieves the applicant's profile information based on the specified request. - /// - /// An object containing the criteria used to identify the applicant profile to retrieve. Must not be null. - /// A task that represents the asynchronous operation. The task result contains an with the applicant's profile data. - public async Task GetApplicantProfileAsync(ApplicantProfileInfoRequest request) - { - var dto = new ApplicantProfileDto - { - ProfileId = request.ProfileId, - Subject = request.Subject, - TenantId = request.TenantId, - Key = request.Key - }; - - if (_providersByKey.TryGetValue(request.Key, out var provider)) - { - dto.Data = await provider.GetDataAsync(request); - } - else - { - Logger.LogWarning("Unknown applicant profile key provided"); - } - - return dto; - } - - /// - /// Retrieves a list of tenants associated with the specified applicant profile. - /// - /// The method extracts the username portion from the subject identifier in the request - /// to match tenant mappings. This operation is asynchronous and queries the host database for relevant tenant - /// associations. - /// An object containing applicant profile information, including the subject identifier used to locate tenant - /// mappings. - /// A list of objects representing the tenants linked to the applicant. The - /// list will be empty if no tenant associations are found. - public async Task> GetApplicantTenantsAsync(ApplicantProfileRequest request) - { - // Extract the username part from the OIDC sub (part before '@') - var subUsername = SubjectNormalizer.Normalize(request.Subject); - if (subUsername is null) return []; - List mappings = []; - - // Query the ApplicantTenantMaps table in the host database - using (currentTenant.Change(null)) - { - var queryable = await applicantTenantMapRepository.GetQueryableAsync(); - mappings = await queryable - .Where(m => m.OidcSubUsername == subUsername) - .Select(m => new ApplicantTenantDto - { - TenantId = m.TenantId, - TenantName = m.TenantName - }) - .ToListAsync(); - } - - // Apply tenant specific metadata - foreach (var map in mappings) - { - await AddTenantMetadataAsync(map); - } - - return mappings; - } - - /// - /// Add on any relevant tenant specific metadata - /// - /// The applicant tenant DTO to enrich with tenant-specific metadata. - private async Task AddTenantMetadataAsync(ApplicantTenantDto tenantMap) - { - using (currentTenant.Change(tenantMap.TenantId)) - { - var defaultEmailAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); - tenantMap.Metadata[ApplicantTenantMetadataKeys.DefaultFromAddress] = defaultEmailAddress ?? "NoReply@gov.bc.ca"; - } - } - - /// - /// Reconciles ApplicantTenantMaps by scanning all tenants for submissions - /// and ensuring mappings exist in the host database. - /// Phase 1: Collects all distinct OidcSub-to-tenant associations into memory. - /// Phase 2: Switches to host DB once and reconciles all mappings. - /// - /// Tuple of (created count, updated count) - public async Task<(int Created, int Updated)> ReconcileApplicantTenantMapsAsync() - { - Logger.LogInformation("Starting ApplicantTenantMap reconciliation..."); - - // Phase 1: Collect all OidcSub-to-tenant associations from each tenant DB - var desiredMappings = new List<(string SubUsername, Guid TenantId, string TenantName)>(); - var tenants = await tenantRepository.GetListAsync(); - - foreach (var tenant in tenants) - { - try - { - Logger.LogDebug("Collecting submissions from tenant: {TenantName}", tenant.Name); - - using (currentTenant.Change(tenant.Id)) - { - var submissionQueryable = await applicationFormSubmissionRepository.GetQueryableAsync(); - var distinctOidcSubs = await submissionQueryable - .Where(s => !string.IsNullOrWhiteSpace(s.OidcSub) && s.OidcSub != Guid.Empty.ToString()) - .Select(s => s.OidcSub) - .Distinct() - .ToListAsync(); - - foreach (var oidcSub in distinctOidcSubs) - { - var subUsername = oidcSub.Contains('@') - ? oidcSub[..oidcSub.IndexOf('@')].ToUpperInvariant() - : oidcSub.ToUpperInvariant(); - - desiredMappings.Add((subUsername, tenant.Id, tenant.Name)); - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error collecting submissions for tenant {TenantName}", tenant.Name); - } - } - - if (desiredMappings.Count == 0) - { - Logger.LogInformation("ApplicantTenantMap reconciliation completed. No submissions found across tenants."); - return (0, 0); - } - - // Phase 2: Switch to host DB once, load existing mappings, and reconcile - int totalMappingsCreated = 0; - int totalMappingsUpdated = 0; - - using (currentTenant.Change(null)) - { - var allSubUsernames = desiredMappings.Select(m => m.SubUsername).Distinct().ToList(); - - var mapQueryable = await applicantTenantMapRepository.GetQueryableAsync(); - var existingMappings = await mapQueryable - .Where(m => allSubUsernames.Contains(m.OidcSubUsername)) - .ToListAsync(); - - var existingByKey = existingMappings - .ToDictionary(m => (m.OidcSubUsername, m.TenantId)); - - foreach (var (subUsername, tenantId, tenantName) in desiredMappings) - { - if (existingByKey.TryGetValue((subUsername, tenantId), out var existing)) - { - existing.LastUpdated = DateTime.UtcNow; - await applicantTenantMapRepository.UpdateAsync(existing); - totalMappingsUpdated++; - } - else - { - var newMapping = new ApplicantTenantMap - { - OidcSubUsername = subUsername, - TenantId = tenantId, - TenantName = tenantName, - LastUpdated = DateTime.UtcNow - }; - await applicantTenantMapRepository.InsertAsync(newMapping); - existingByKey[(subUsername, tenantId)] = newMapping; - totalMappingsCreated++; - Logger.LogInformation("Created missing ApplicantTenantMap for {SubUsername} in tenant {TenantName}", - subUsername, tenantName); - } - } - } - - Logger.LogInformation("ApplicantTenantMap reconciliation completed. Created: {Created}, Updated: {Updated}", - totalMappingsCreated, totalMappingsUpdated); - - return (totalMappingsCreated, totalMappingsUpdated); - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs deleted file mode 100644 index 06dd26366d..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs +++ /dev/null @@ -1,145 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Unity.GrantManager.ApplicantProfile.ProfileData; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Contacts; -using Unity.GrantManager.GrantApplications; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Unity.GrantManager.ApplicantProfile; - -/// -/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles, -/// application-level contacts matched by OIDC subject, and applicant agent contacts derived from -/// the submission login token. Profile contacts are resolved by looking up form submissions that -/// match the OIDC subject to obtain applicant IDs, then querying -/// records against those IDs. When a single applicant ID is resolved the contacts are editable; -/// when multiple IDs are found the contacts are read-only. This service operates independently from the -/// generic and queries repositories directly. -/// -public class ApplicantProfileContactService( - IContactRepository contactRepository, - IContactLinkRepository contactLinkRepository, - IRepository applicationFormSubmissionRepository, - IRepository applicationContactRepository, - IRepository applicantAgentRepository, - IRepository applicationRepository) - : IApplicantProfileContactService, ITransientDependency -{ - private const string ApplicantEntityType = "Applicant"; - - /// - public async Task> GetApplicantContactsAsync(string subject) - { - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); - var contactsQuery = await contactRepository.GetQueryableAsync(); - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); - - var applicantIds = await submissionsQuery - .Where(s => s.OidcSub == subject) - .Select(s => s.ApplicantId) - .Distinct() - .ToListAsync(); - - var isEditable = applicantIds.Count <= 1; - - return await ( - from link in contactLinksQuery - join contact in contactsQuery on link.ContactId equals contact.Id - where link.RelatedEntityType == ApplicantEntityType - && applicantIds.Contains(link.RelatedEntityId) - && link.IsActive - select new ContactInfoItemDto - { - ContactId = contact.Id, - Name = contact.Name, - Title = contact.Title, - Email = contact.Email, - HomePhoneNumber = contact.HomePhoneNumber, - MobilePhoneNumber = contact.MobilePhoneNumber, - WorkPhoneNumber = contact.WorkPhoneNumber, - WorkPhoneExtension = contact.WorkPhoneExtension, - ContactType = link.RelatedEntityType, - Role = link.Role, - IsPrimary = link.IsPrimary, - IsEditable = isEditable, - ReferenceNo = null, - CreationTime = contact.CreationTime - }).ToListAsync(); - } - - /// - public async Task> GetApplicationContactsBySubjectAsync(string subject) - { - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); - var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); - var applicationsQuery = await applicationRepository.GetQueryableAsync(); - - var applicationContacts = await ( - from submission in submissionsQuery - join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId - join application in applicationsQuery on submission.ApplicationId equals application.Id - where submission.OidcSub == subject - select new ContactInfoItemDto - { - ContactId = appContact.Id, - Name = appContact.ContactFullName, - Title = appContact.ContactTitle, - Email = appContact.ContactEmail, - MobilePhoneNumber = appContact.ContactMobilePhone, - WorkPhoneNumber = appContact.ContactWorkPhone, - Role = GetMatchingRole(appContact.ContactType), - ContactType = "Application", - IsPrimary = false, - IsEditable = false, - ApplicationId = appContact.ApplicationId, - ReferenceNo = application.ReferenceNo, - CreationTime = appContact.CreationTime - }).ToListAsync(); - - return applicationContacts; - } - - /// - public async Task> GetApplicantAgentContactsBySubjectAsync(string subject) - { - var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); - var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); - var applicationsQuery = await applicationRepository.GetQueryableAsync(); - - var agentContacts = await ( - from submission in submissionsQuery - join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId - join application in applicationsQuery on submission.ApplicationId equals application.Id - where submission.OidcSub == subject - select new ContactInfoItemDto - { - ContactId = agent.Id, - Name = agent.Name, - Title = agent.Title, - Email = agent.Email, - WorkPhoneNumber = agent.Phone, - WorkPhoneExtension = agent.PhoneExtension, - MobilePhoneNumber = agent.Phone2, - Role = agent.RoleForApplicant, - ContactType = "ApplicantAgent", - IsPrimary = false, - IsEditable = false, - ApplicationId = agent.ApplicationId, - ReferenceNo = application.ReferenceNo, - CreationTime = agent.CreationTime - }).ToListAsync(); - - return agentContacts; - } - - private static string GetMatchingRole(string contactType) - { - return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value) - ? value : contactType; - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/AddressInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/AddressInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/AddressInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/ContactInfoDataProvider.cs similarity index 77% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/ContactInfoDataProvider.cs index 8b7cbe547d..40253327a4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/ContactInfoDataProvider.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.ApplicantProfile [ExposeServices(typeof(IApplicantProfileDataProvider))] public class ContactInfoDataProvider( ICurrentTenant currentTenant, - IApplicantProfileContactService applicantProfileContactService) + IApplicantContactQueryService applicantContactQueryService) : IApplicantProfileDataProvider, ITransientDependency { /// @@ -34,13 +34,13 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ using (currentTenant.Change(tenantId)) { - var applicantContacts = await applicantProfileContactService.GetApplicantContactsAsync(normalizedSubject); + var applicantContacts = await applicantContactQueryService.GetApplicantContactsAsync(normalizedSubject); dto.Contacts.AddRange(applicantContacts); - var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(normalizedSubject); + var applicationContacts = await applicantContactQueryService.GetApplicationContactsBySubjectAsync(normalizedSubject); dto.Contacts.AddRange(applicationContacts); - var agentContacts = await applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(normalizedSubject); + var agentContacts = await applicantContactQueryService.GetApplicantAgentContactsBySubjectAsync(normalizedSubject); dto.Contacts.AddRange(agentContacts); } @@ -50,6 +50,7 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ .OrderByDescending(c => c.CreationTime) .First(); latest.IsPrimary = true; + latest.IsPrimaryInferred = true; } return dto; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/OrgInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/OrgInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/OrgInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/PaymentInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/PaymentInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/PaymentInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/SubmissionInfoDataProvider.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/DataProviders/SubmissionInfoDataProvider.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs new file mode 100644 index 0000000000..c06fedebe1 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantContactQueryService.cs @@ -0,0 +1,324 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Contacts; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Applicant-profile-specific contact query service. Retrieves contacts linked to applicant profiles, +/// application-level contacts matched by OIDC subject, and applicant agent contacts derived from +/// the submission login token. Profile contacts are resolved by looking up form submissions that +/// match the OIDC subject to obtain applicant IDs, then querying +/// records against those IDs. When a single applicant ID is resolved the contacts are editable; +/// when multiple IDs are found the contacts are read-only. This service operates independently from the +/// generic and queries repositories directly. +/// +public class ApplicantContactQueryService( + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository, + IRepository applicationFormSubmissionRepository, + IRepository applicationContactRepository, + IRepository applicantAgentRepository, + IRepository applicationRepository) + : IApplicantContactQueryService, ITransientDependency +{ + private const string ApplicantEntityType = "Applicant"; + + /// + public async Task> GetApplicantContactsAsync(string subject) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await contactRepository.GetQueryableAsync(); + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + + var applicantIds = await submissionsQuery + .Where(s => s.OidcSub == subject) + .Select(s => s.ApplicantId) + .Distinct() + .ToListAsync(); + + var isEditable = applicantIds.Count <= 1; + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == ApplicantEntityType + && applicantIds.Contains(link.RelatedEntityId) + && link.IsActive + select new ContactInfoItemDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + ContactType = link.RelatedEntityType, + Role = link.Role, + IsPrimary = link.IsPrimary, + IsEditable = isEditable, + ReferenceNo = null, + CreationTime = contact.CreationTime + }).ToListAsync(); + } + + /// + public async Task> GetApplicationContactsBySubjectAsync(string subject) + { + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var applicationContacts = await ( + from submission in submissionsQuery + join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId + join application in applicationsQuery on submission.ApplicationId equals application.Id + where submission.OidcSub == subject + select new ContactInfoItemDto + { + ContactId = appContact.Id, + Name = appContact.ContactFullName, + Title = appContact.ContactTitle, + Email = appContact.ContactEmail, + MobilePhoneNumber = appContact.ContactMobilePhone, + WorkPhoneNumber = appContact.ContactWorkPhone, + Role = GetMatchingRole(appContact.ContactType), + ContactType = "Application", + IsPrimary = false, + IsEditable = false, + ApplicationId = appContact.ApplicationId, + ReferenceNo = application.ReferenceNo, + CreationTime = appContact.CreationTime + }).ToListAsync(); + + return applicationContacts; + } + + /// + public async Task> GetApplicantAgentContactsBySubjectAsync(string subject) + { + var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync(); + var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var agentContacts = await ( + from submission in submissionsQuery + join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId + join application in applicationsQuery on submission.ApplicationId equals application.Id + where submission.OidcSub == subject + select new ContactInfoItemDto + { + ContactId = agent.Id, + Name = agent.Name, + Title = agent.Title, + Email = agent.Email, + WorkPhoneNumber = agent.Phone, + WorkPhoneExtension = agent.PhoneExtension, + MobilePhoneNumber = agent.Phone2, + Role = agent.RoleForApplicant, + ContactType = "ApplicantAgent", + IsPrimary = false, + IsEditable = false, + ApplicationId = agent.ApplicationId, + ReferenceNo = application.ReferenceNo, + CreationTime = agent.CreationTime + }).ToListAsync(); + + return agentContacts; + } + + /// + public async Task GetByApplicantIdAsync(Guid applicantId) + { + var dto = new ApplicantContactInfoDto { Contacts = [] }; + if (applicantId == Guid.Empty) + { + return dto; + } + + dto.Contacts.AddRange(await GetApplicantLinkedContactsAsync([applicantId], isEditable: true)); + dto.Contacts.AddRange(await GetApplicationContactsByApplicantIdAsync(applicantId)); + dto.Contacts.AddRange(await GetApplicantAgentContactsByApplicantIdAsync(applicantId)); + + ResolvePrimary(dto.Contacts); + + return dto; + } + + private async Task> GetApplicantLinkedContactsAsync( + IReadOnlyCollection applicantIds, + bool isEditable) + { + if (applicantIds.Count == 0) + { + return []; + } + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var contactsQuery = await contactRepository.GetQueryableAsync(); + + return await ( + from link in contactLinksQuery + join contact in contactsQuery on link.ContactId equals contact.Id + where link.RelatedEntityType == ApplicantEntityType + && applicantIds.Contains(link.RelatedEntityId) + && link.IsActive + select new ContactInfoItemDto + { + ContactId = contact.Id, + Name = contact.Name, + Title = contact.Title, + Email = contact.Email, + HomePhoneNumber = contact.HomePhoneNumber, + MobilePhoneNumber = contact.MobilePhoneNumber, + WorkPhoneNumber = contact.WorkPhoneNumber, + WorkPhoneExtension = contact.WorkPhoneExtension, + ContactType = ApplicantEntityType, + Role = link.Role, + IsPrimary = link.IsPrimary, + IsEditable = isEditable, + ReferenceNo = null, + CreationTime = contact.CreationTime + }).ToListAsync(); + } + + private async Task> GetApplicationContactsByApplicantIdAsync(Guid applicantId) + { + var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + // Resolve this applicant's applications up-front so we can map ReferenceNo by ApplicationId + // without relying on EF join translation (mirrors the ApplicantAddresses widget pattern). + var applicationRefMap = await applicationsQuery + .Where(a => a.ApplicantId == applicantId) + .Select(a => new { a.Id, a.ReferenceNo }) + .ToDictionaryAsync(a => a.Id, a => a.ReferenceNo); + + if (applicationRefMap.Count == 0) + { + return []; + } + + var applicationIds = applicationRefMap.Keys.ToList(); + + var contacts = await applicationContactsQuery + .Where(c => applicationIds.Contains(c.ApplicationId)) + .Select(appContact => new ContactInfoItemDto + { + ContactId = appContact.Id, + Name = appContact.ContactFullName, + Title = appContact.ContactTitle, + Email = appContact.ContactEmail, + MobilePhoneNumber = appContact.ContactMobilePhone, + WorkPhoneNumber = appContact.ContactWorkPhone, + Role = GetMatchingRole(appContact.ContactType), + ContactType = "Application", + IsPrimary = false, + IsEditable = false, + ApplicationId = appContact.ApplicationId, + CreationTime = appContact.CreationTime + }).ToListAsync(); + + foreach (var contact in contacts) + { + if (contact.ApplicationId.HasValue + && applicationRefMap.TryGetValue(contact.ApplicationId.Value, out var referenceNo)) + { + contact.ReferenceNo = referenceNo; + } + } + + return contacts; + } + + private async Task> GetApplicantAgentContactsByApplicantIdAsync(Guid applicantId) + { + var agentsQuery = await applicantAgentRepository.GetQueryableAsync(); + var applicationsQuery = await applicationRepository.GetQueryableAsync(); + + var agents = await agentsQuery + .Where(a => a.ApplicantId == applicantId) + .Select(agent => new ContactInfoItemDto + { + ContactId = agent.Id, + Name = agent.Name, + Title = agent.Title, + Email = agent.Email, + WorkPhoneNumber = agent.Phone, + WorkPhoneExtension = agent.PhoneExtension, + MobilePhoneNumber = agent.Phone2, + Role = agent.RoleForApplicant, + ContactType = "ApplicantAgent", + IsPrimary = false, + IsEditable = false, + ApplicationId = agent.ApplicationId, + CreationTime = agent.CreationTime + }).ToListAsync(); + + if (agents.Count == 0) + { + return agents; + } + + var applicationIds = agents + .Where(a => a.ApplicationId.HasValue) + .Select(a => a.ApplicationId!.Value) + .Distinct() + .ToList(); + + if (applicationIds.Count == 0) + { + return agents; + } + + var applicationRefMap = await applicationsQuery + .Where(a => applicationIds.Contains(a.Id)) + .Select(a => new { a.Id, a.ReferenceNo }) + .ToDictionaryAsync(a => a.Id, a => a.ReferenceNo); + + foreach (var agent in agents) + { + if (agent.ApplicationId.HasValue + && applicationRefMap.TryGetValue(agent.ApplicationId.Value, out var referenceNo)) + { + agent.ReferenceNo = referenceNo; + } + } + + return agents; + } + + /// + /// Ensures exactly one contact is flagged as primary. If none are explicitly flagged, + /// the most recently created contact is elected. + /// + private static void ResolvePrimary(List contacts) + { + if (contacts.Count == 0 || contacts.Any(c => c.IsPrimary)) + { + return; + } + + var latest = contacts + .OrderByDescending(c => c.CreationTime) + .First(); + latest.IsPrimary = true; + latest.IsPrimaryInferred = true; + } + + private static string GetMatchingRole(string contactType) + { + return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value) + ? value : contactType; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs new file mode 100644 index 0000000000..99bb33d239 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/Queries/ApplicantProfileQueryService.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applicants; +using Unity.Notifications.Settings; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; + +namespace Unity.GrantManager.ApplicantProfile +{ + /// + /// Internal query service that backs ApplicantProfileController. Aggregates applicant profile + /// data from registered implementations and resolves + /// the applicant's tenant mappings. Not exposed as an ABP application service: the HTTP surface + /// lives in the controller, which applies its own routing and API-key authorization. + /// + public class ApplicantProfileQueryService( + ICurrentTenant currentTenant, + IRepository applicantTenantMapRepository, + IEnumerable dataProviders, + ISettingProvider settingProvider, + ILogger logger) + : IApplicantProfileQueryService, ITransientDependency + { + private readonly Dictionary _providersByKey + = dataProviders.ToDictionary(p => p.Key, StringComparer.OrdinalIgnoreCase); + + /// + /// Retrieves the applicant's profile information based on the specified request. + /// + /// An object containing the criteria used to identify the applicant profile to retrieve. Must not be null. + /// A task that represents the asynchronous operation. The task result contains an with the applicant's profile data. + public async Task GetApplicantProfileAsync(ApplicantProfileInfoRequest request) + { + var dto = new ApplicantProfileDto + { + ProfileId = request.ProfileId, + Subject = request.Subject, + TenantId = request.TenantId, + Key = request.Key + }; + + if (_providersByKey.TryGetValue(request.Key, out var provider)) + { + dto.Data = await provider.GetDataAsync(request); + } + else + { + logger.LogWarning("Unknown applicant profile key provided"); + } + + return dto; + } + + /// + /// Retrieves a list of tenants associated with the specified applicant profile. + /// + /// The method extracts the username portion from the subject identifier in the request + /// to match tenant mappings. This operation is asynchronous and queries the host database for relevant tenant + /// associations. + /// An object containing applicant profile information, including the subject identifier used to locate tenant + /// mappings. + /// A list of objects representing the tenants linked to the applicant. The + /// list will be empty if no tenant associations are found. + public async Task> GetApplicantTenantsAsync(ApplicantProfileRequest request) + { + // Extract the username part from the OIDC sub (part before '@') + var subUsername = SubjectNormalizer.Normalize(request.Subject); + if (subUsername is null) return []; + List mappings = []; + + // Query the ApplicantTenantMaps table in the host database + using (currentTenant.Change(null)) + { + var queryable = await applicantTenantMapRepository.GetQueryableAsync(); + mappings = await queryable + .Where(m => m.OidcSubUsername == subUsername) + .Select(m => new ApplicantTenantDto + { + TenantId = m.TenantId, + TenantName = m.TenantName + }) + .ToListAsync(); + } + + // Apply tenant specific metadata + foreach (var map in mappings) + { + await AddTenantMetadataAsync(map); + } + + return mappings; + } + + /// + /// Add on any relevant tenant specific metadata + /// + /// The applicant tenant DTO to enrich with tenant-specific metadata. + private async Task AddTenantMetadataAsync(ApplicantTenantDto tenantMap) + { + using (currentTenant.Change(tenantMap.TenantId)) + { + var defaultEmailAddress = await settingProvider.GetOrNullAsync(NotificationsSettings.Mailing.DefaultFromAddress); + tenantMap.Metadata[ApplicantTenantMetadataKeys.DefaultFromAddress] = defaultEmailAddress ?? "NoReply@gov.bc.ca"; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs new file mode 100644 index 0000000000..8d43fe61ec --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs @@ -0,0 +1,121 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applicants; +using Unity.GrantManager.Applications; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TenantManagement; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Internal reconciler for records. Not exposed on any +/// application-service contract — background workers depend on this directly. +/// +public class ApplicantTenantMapReconciler( + ICurrentTenant currentTenant, + ITenantRepository tenantRepository, + IRepository applicantTenantMapRepository, + IRepository applicationFormSubmissionRepository, + ILogger logger) + : IApplicantTenantMapReconciler, ITransientDependency +{ + /// + public async Task<(int Created, int Updated)> ReconcileAsync() + { + logger.LogInformation("Starting ApplicantTenantMap reconciliation..."); + + // Phase 1: Collect all OidcSub-to-tenant associations from each tenant DB + var desiredMappings = new List<(string SubUsername, Guid TenantId, string TenantName)>(); + var tenants = await tenantRepository.GetListAsync(); + + foreach (var tenant in tenants) + { + try + { + logger.LogDebug("Collecting submissions from tenant: {TenantName}", tenant.Name); + + using (currentTenant.Change(tenant.Id)) + { + var submissionQueryable = await applicationFormSubmissionRepository.GetQueryableAsync(); + var distinctOidcSubs = await submissionQueryable + .Where(s => !string.IsNullOrWhiteSpace(s.OidcSub) && s.OidcSub != Guid.Empty.ToString()) + .Select(s => s.OidcSub) + .Distinct() + .ToListAsync(); + + foreach (var oidcSub in distinctOidcSubs) + { + var subUsername = oidcSub.Contains('@') + ? oidcSub[..oidcSub.IndexOf('@')].ToUpperInvariant() + : oidcSub.ToUpperInvariant(); + + desiredMappings.Add((subUsername, tenant.Id, tenant.Name)); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error collecting submissions for tenant {TenantName}", tenant.Name); + } + } + + if (desiredMappings.Count == 0) + { + logger.LogInformation("ApplicantTenantMap reconciliation completed. No submissions found across tenants."); + return (0, 0); + } + + // Phase 2: Switch to host DB once, load existing mappings, and reconcile + int totalMappingsCreated = 0; + int totalMappingsUpdated = 0; + + using (currentTenant.Change(null)) + { + var allSubUsernames = desiredMappings.Select(m => m.SubUsername).Distinct().ToList(); + + var mapQueryable = await applicantTenantMapRepository.GetQueryableAsync(); + var existingMappings = await mapQueryable + .Where(m => allSubUsernames.Contains(m.OidcSubUsername)) + .ToListAsync(); + + var existingByKey = existingMappings + .ToDictionary(m => (m.OidcSubUsername, m.TenantId)); + + foreach (var (subUsername, tenantId, tenantName) in desiredMappings) + { + if (existingByKey.TryGetValue((subUsername, tenantId), out var existing)) + { + existing.LastUpdated = DateTime.UtcNow; + await applicantTenantMapRepository.UpdateAsync(existing); + totalMappingsUpdated++; + } + else + { + var newMapping = new ApplicantTenantMap + { + OidcSubUsername = subUsername, + TenantId = tenantId, + TenantName = tenantName, + LastUpdated = DateTime.UtcNow + }; + await applicantTenantMapRepository.InsertAsync(newMapping); + existingByKey[(subUsername, tenantId)] = newMapping; + totalMappingsCreated++; + logger.LogInformation("Created missing ApplicantTenantMap for {SubUsername} in tenant {TenantName}", + subUsername, tenantName); + } + } + } + + logger.LogInformation("ApplicantTenantMap reconciliation completed. Created: {Created}, Updated: {Updated}", + totalMappingsCreated, totalMappingsUpdated); + + return (totalMappingsCreated, totalMappingsUpdated); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs new file mode 100644 index 0000000000..4d8475bb1e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.GrantManager.ApplicantProfile; + +/// +/// Internal helper that reconciles records by +/// scanning submissions across all tenants and syncing the host-database mapping table. +/// Intended for background-worker use only; not part of any public application-service contract. +/// +public interface IApplicantTenantMapReconciler +{ + /// + /// Phase 1: Collects all distinct OidcSub-to-tenant associations across all tenants. + /// Phase 2: Switches to the host DB once and reconciles all mappings. + /// + /// Tuple of (created count, updated count). + Task<(int Created, int Updated)> ReconcileAsync(); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs index 61d07b1ff4..d5f13cb6c5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/BackgroundWorkers/ApplicantTenantMapReconciliationWorker.cs @@ -13,7 +13,7 @@ namespace Unity.GrantManager.Applicants.BackgroundWorkers [DisallowConcurrentExecution] public class ApplicantTenantMapReconciliationWorker : QuartzBackgroundWorkerBase { - private readonly IApplicantProfileAppService _applicantProfileAppService; + private readonly IApplicantTenantMapReconciler _reconciler; private readonly ILogger _logger; /// @@ -23,15 +23,15 @@ public class ApplicantTenantMapReconciliationWorker : QuartzBackgroundWorkerBase /// The scheduling behavior of the worker is determined by a cron expression retrieved /// from application settings. If the setting is unavailable or cannot be read, a default schedule is used. /// Logging is performed for any issues encountered during initialization. - /// The service used to access and manage applicant profile data. + /// The reconciler used to sync applicant-tenant maps across tenants. /// The setting manager used to retrieve configuration values, including the cron expression for scheduling. /// The logger used to record diagnostic and operational information for this worker. public ApplicantTenantMapReconciliationWorker( - IApplicantProfileAppService applicantProfileAppService, + IApplicantTenantMapReconciler reconciler, ISettingManager settingManager, ILogger logger) { - _applicantProfileAppService = applicantProfileAppService; + _reconciler = reconciler; _logger = logger; // 2 AM PST = 10 AM UTC @@ -93,7 +93,7 @@ public override async Task Execute(IJobExecutionContext context) try { - var (created, updated) = await _applicantProfileAppService.ReconcileApplicantTenantMapsAsync(); + var (created, updated) = await _reconciler.ReconcileAsync(); _logger.LogInformation("ApplicantTenantMapReconciliationWorker completed. Created: {Created}, Updated: {Updated}", created, updated); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs index f8fcc31f74..52b3d0d9da 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Contacts/ContactAppService.cs @@ -20,7 +20,8 @@ namespace Unity.GrantManager.Contacts; [ExposeServices(typeof(ContactAppService), typeof(IContactAppService))] public class ContactAppService( IContactRepository contactRepository, - IContactLinkRepository contactLinkRepository) + IContactLinkRepository contactLinkRepository, + IContactManager contactManager) : GrantManagerAppService, IContactAppService { /// @@ -53,33 +54,60 @@ join contact in contactsQuery on link.ContactId equals contact.Id /// public async Task CreateContactAsync(CreateContactLinkDto input) { - var contact = await contactRepository.InsertAsync(new Contact - { - Name = input.Name, - Title = input.Title, - Email = input.Email, - HomePhoneNumber = input.HomePhoneNumber, - MobilePhoneNumber = input.MobilePhoneNumber, - WorkPhoneNumber = input.WorkPhoneNumber, - WorkPhoneExtension = input.WorkPhoneExtension - }, autoSave: true); + ArgumentNullException.ThrowIfNull(input); - if (input.IsPrimary) - { - await ClearPrimaryAsync(input.RelatedEntityType, input.RelatedEntityId); - } + var (contact, link) = await contactManager.CreateAsync( + input.RelatedEntityType, + input.RelatedEntityId, + ToContactInput(input), + input.Role, + input.IsPrimary); - await contactLinkRepository.InsertAsync(new ContactLink - { - ContactId = contact.Id, - RelatedEntityType = input.RelatedEntityType, - RelatedEntityId = input.RelatedEntityId, - Role = input.Role, - IsPrimary = input.IsPrimary, - IsActive = true - }, autoSave: true); + return MapToDto(contact, link); + } + + /// + public async Task UpdateContactAsync(string entityType, Guid entityId, Guid contactId, UpdateContactDto input) + { + ArgumentNullException.ThrowIfNull(input); + + var (contact, link) = await contactManager.UpdateAsync( + entityType, + entityId, + contactId, + ToContactInput(input), + input.Role, + input.IsPrimary); + + return MapToDto(contact, link); + } + + /// + public Task SetPrimaryContactAsync(string entityType, Guid entityId, Guid contactId) + { + return contactManager.SetPrimaryAsync(entityType, entityId, contactId); + } - return new ContactDto + private static ContactInput ToContactInput(CreateContactLinkDto input) => + new(input.Name, + input.Title, + input.Email, + input.HomePhoneNumber, + input.MobilePhoneNumber, + input.WorkPhoneNumber, + input.WorkPhoneExtension); + + private static ContactInput ToContactInput(UpdateContactDto input) => + new(input.Name, + input.Title, + input.Email, + input.HomePhoneNumber, + input.MobilePhoneNumber, + input.WorkPhoneNumber, + input.WorkPhoneExtension); + + private static ContactDto MapToDto(Contact contact, ContactLink link) => + new() { ContactId = contact.Id, Name = contact.Name, @@ -89,47 +117,7 @@ await contactLinkRepository.InsertAsync(new ContactLink MobilePhoneNumber = contact.MobilePhoneNumber, WorkPhoneNumber = contact.WorkPhoneNumber, WorkPhoneExtension = contact.WorkPhoneExtension, - Role = input.Role, - IsPrimary = input.IsPrimary + Role = link.Role, + IsPrimary = link.IsPrimary }; - } - - /// - public async Task SetPrimaryContactAsync(string entityType, Guid entityId, Guid contactId) - { - await ClearPrimaryAsync(entityType, entityId); - - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); - var link = await contactLinksQuery - .Where(l => l.RelatedEntityType == entityType - && l.RelatedEntityId == entityId - && l.ContactId == contactId - && l.IsActive) - .FirstOrDefaultAsync() ?? throw new BusinessException("Contacts:ContactLinkNotFound") - .WithData("contactId", contactId) - .WithData("entityType", entityType) - .WithData("entityId", entityId); - link.IsPrimary = true; - await contactLinkRepository.UpdateAsync(link, autoSave: true); - } - - /// - /// Clears the primary flag on all active contact links for the specified entity. - /// - private async Task ClearPrimaryAsync(string entityType, Guid entityId) - { - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); - var currentPrimaryLinks = await contactLinksQuery - .Where(l => l.RelatedEntityType == entityType - && l.RelatedEntityId == entityId - && l.IsPrimary - && l.IsActive) - .ToListAsync(); - - foreach (var existing in currentPrimaryLinks) - { - existing.IsPrimary = false; - await contactLinkRepository.UpdateAsync(existing, autoSave: true); - } - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 89d5df054e..9ccf13c8bb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -102,20 +102,20 @@ "ReviewerList:Subtotal": "Subtotal", "ReviewerList:CloneAssessment": "Clone Assessment", - "AssessmentResultAttachments:Id": "#", - "AssessmentResultAttachments:DocumentName": "Document Name", - "AssessmentResultAttachments:UploadedDate": "Date", - "AssessmentResultAttachments:AttachedBy": "Attached by", - "ChefsAttachments:Title": "Submission Attachments", - "ChefsAttachments:Filter": "Filter", - "ChefsAttachments:Download": "Download", - "ChefsAttachments:GenerateSummaries": "Generate AI Summaries", - "ChefsAttachments:GenerateSummary": "Generate Summary", - "ChefsAttachments:HideAISummaries": "Hide AI Summaries", - "ChefsAttachments:ShowAISummaries": "Show AI Summaries", - "ChefsAttachments:HideSummaries": "Hide Summaries", - "ChefsAttachments:ShowSummaries": "Show Summaries", - "ChefsAttachments:NoSummariesAvailable": "No summaries available", + "AssessmentResultAttachments:Id": "#", + "AssessmentResultAttachments:DocumentName": "Document Name", + "AssessmentResultAttachments:UploadedDate": "Date", + "AssessmentResultAttachments:AttachedBy": "Attached by", + "ChefsAttachments:Title": "Submission Attachments", + "ChefsAttachments:Filter": "Filter", + "ChefsAttachments:Download": "Download", + "ChefsAttachments:GenerateSummaries": "Generate AI Summaries", + "ChefsAttachments:GenerateSummary": "Generate Summary", + "ChefsAttachments:HideAISummaries": "Hide AI Summaries", + "ChefsAttachments:ShowAISummaries": "Show AI Summaries", + "ChefsAttachments:HideSummaries": "Hide Summaries", + "ChefsAttachments:ShowSummaries": "Show Summaries", + "ChefsAttachments:NoSummariesAvailable": "No summaries available", "Enum:AssessmentState.IN_PROGRESS": "In Progress", "Enum:AssessmentState.IN_REVIEW": "Under Review by Team Lead", @@ -473,34 +473,24 @@ "ApplicationContact:MobilePhone": "Mobile Phone Number", "ApplicationContact:WorkPhone": "Work Phone Number", "ApplicationContact:Delete": "Delete This Contact", - - "ApplicationBatchApprovalRequest:Title": "Approve Applications", - "ApplicationBatchApprovalRequest:SubmitButtonText": "Approve", - "ApplicationBatchApprovalRequest:CancelButtonText": "Cancel", - "ApplicationBatchApprovalRequest:DecisionDateDefaulted": "Decision Date has been defaulted", - "ApplicationBatchApprovalRequest:ApprovedAmountDefaulted": "Approved Amount has been defaulted", - "ApplicationBatchApprovalRequest:InvalidStatus": "The assessment for the selected item is not in the Assessment Completed state", - "ApplicationBatchApprovalRequest:InvalidPermissions": "Invalid permissions", - "ApplicationBatchApprovalRequest:InvalidApprovedAmount": "Invalid Approved Amount, it must be greater than 0.00", - "ApplicationBatchApprovalRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk approval. Please reduce the number to {0} or fewer", - "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00", - - "ApplicationLinks:Category": "Category", - "ApplicationLinks:ID": "ID", - "ApplicationLinks:Status": "Status", - "ApplicationLinks:LinkType": "Link Type", - - "ApplicantPortalSettings:Title": "Applicant Portal Configuration", - "ApplicantPortalSettings:ManageStatuses": "Manage Statuses", - "ApplicantPortalSettings:PortalStatusHeading": "Applicant Portal Status", - "ApplicantPortalSettings:InternalStatus": "Internal Status", - "ApplicantPortalSettings:PortalStatusLabel": "Portal Status Label", - "ApplicantPortalSettings:SaveChanges": "Save Changes", - "ApplicantPortalSettings:ResetChanges": "Reset", - "ApplicantPortalSettings:SaveSuccess": "Portal status labels updated successfully.", - "ApplicantPortalSettings:SaveError": "An error occurred while saving portal status labels.", - "ApplicantPortalSettings:ValidationRequired": "Portal status label cannot be empty.", - "ApplicantPortalSettings:NoChanges": "No changes to save.", - "ApplicantPortalSettings:ChangesReset": "Changes have been reset to original values." + "ApplicantContact:SetAsPrimary": "Set as Primary", + "ApplicantContact:PrimaryHint": "This contact is currently primary because it was auto-selected by most recent timestamp. Saving with Set as Primary checked will explicitly set it as primary.", + "ApplicantContacts:ApplicantNotFound": "Applicant information not found.", + "ApplicantContacts:PrimaryContact": "Primary Contact", + "ApplicantContacts:NoPrimaryContact": "No primary contact on record.", + "ApplicantContacts:ContactsTitle": "Contacts", + "ApplicantContacts:PrimaryExplicitTooltip": "Primary contact", + "ApplicantContacts:PrimaryInferredTooltip": "Primary contact (auto-selected by most recent timestamp; not explicitly set).", + "ApplicantContacts:SourceInfoApplication": "Sourced from the Application submission. Managed on the Application Details form and cannot be edited here.", + "ApplicantContacts:SourceInfoApplicantAgent": "Sourced from the Applicant Agent on the CHEFS submission. Captured at intake and cannot be edited here.", + "ApplicantContacts:SourceInfoGeneric": "Sourced from {0} and cannot be edited here.", + "ApplicantContacts:View": "View", + "ApplicantContacts:ContactSaved": "Contact saved.", + "ApplicantContacts:ContactSetPrimary": "Contact set as primary.", + "ApplicantContacts:ServiceUnavailable": "Applicant contact service is not available.", + "ApplicantContacts:SetPrimaryFailed": "Failed to set contact as primary.", + "ApplicantContacts:ColumnName": "Name", + "ApplicantContacts:ColumnPhone": "Phone (Work)", + "ApplicantContacts:ColumnSubmission": "Submission #" } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs new file mode 100644 index 0000000000..df4bf78334 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactInput.cs @@ -0,0 +1,14 @@ +namespace Unity.GrantManager.Contacts; + +/// +/// Domain-layer input for creating or updating a . +/// Kept free of application-layer DTO dependencies so Domain remains self-contained. +/// +public record ContactInput( + string Name, + string? Title, + string? Email, + string? HomePhoneNumber, + string? MobilePhoneNumber, + string? WorkPhoneNumber, + string? WorkPhoneExtension); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs new file mode 100644 index 0000000000..5ab378ac8b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace Unity.GrantManager.Contacts; + +/// +/// Domain service that owns / write invariants +/// (create, update, set-primary). Application services delegate here so they do not need to +/// inject each other. +/// +public class ContactManager( + IContactRepository contactRepository, + IContactLinkRepository contactLinkRepository) : DomainService, IContactManager +{ + /// + public virtual async Task<(Contact Contact, ContactLink Link)> CreateAsync( + string entityType, + Guid entityId, + ContactInput input, + string? role, + bool isPrimary) + { + ArgumentException.ThrowIfNullOrWhiteSpace(entityType); + ArgumentNullException.ThrowIfNull(input); + + var contact = await contactRepository.InsertAsync(new Contact + { + Name = input.Name, + Title = input.Title, + Email = input.Email, + HomePhoneNumber = input.HomePhoneNumber, + MobilePhoneNumber = input.MobilePhoneNumber, + WorkPhoneNumber = input.WorkPhoneNumber, + WorkPhoneExtension = input.WorkPhoneExtension + }, autoSave: true); + + if (isPrimary) + { + await ClearPrimaryAsync(entityType, entityId); + } + + var link = await contactLinkRepository.InsertAsync(new ContactLink + { + ContactId = contact.Id, + RelatedEntityType = entityType, + RelatedEntityId = entityId, + Role = role, + IsPrimary = isPrimary, + IsActive = true + }, autoSave: true); + + return (contact, link); + } + + /// + public virtual async Task<(Contact Contact, ContactLink Link)> UpdateAsync( + string entityType, + Guid entityId, + Guid contactId, + ContactInput input, + string? role, + bool isPrimary) + { + ArgumentException.ThrowIfNullOrWhiteSpace(entityType); + ArgumentNullException.ThrowIfNull(input); + + var contact = await contactRepository.GetAsync(contactId); + + contact.Name = input.Name; + contact.Title = input.Title; + contact.Email = input.Email; + contact.HomePhoneNumber = input.HomePhoneNumber; + contact.MobilePhoneNumber = input.MobilePhoneNumber; + contact.WorkPhoneNumber = input.WorkPhoneNumber; + contact.WorkPhoneExtension = input.WorkPhoneExtension; + + await contactRepository.UpdateAsync(contact, autoSave: true); + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var links = await AsyncExecuter.ToListAsync(contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.IsActive)); + + var targetLink = links.FirstOrDefault(l => l.ContactId == contactId) + ?? throw new BusinessException("Contacts:ContactLinkNotFound") + .WithData("contactId", contactId) + .WithData("entityType", entityType) + .WithData("entityId", entityId); + + if (isPrimary) + { + foreach (var stale in links.Where(l => l.IsPrimary && l.ContactId != contactId)) + { + stale.IsPrimary = false; + await contactLinkRepository.UpdateAsync(stale, autoSave: true); + } + } + + var linkChanged = false; + if (targetLink.IsPrimary != isPrimary) + { + targetLink.IsPrimary = isPrimary; + linkChanged = true; + } + if (role is not null && targetLink.Role != role) + { + targetLink.Role = role; + linkChanged = true; + } + if (linkChanged) + { + await contactLinkRepository.UpdateAsync(targetLink, autoSave: true); + } + + return (contact, targetLink); + } + + /// + public virtual async Task SetPrimaryAsync(string entityType, Guid entityId, Guid contactId) + { + ArgumentException.ThrowIfNullOrWhiteSpace(entityType); + + await ClearPrimaryAsync(entityType, entityId); + + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var link = await AsyncExecuter.FirstOrDefaultAsync(contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.ContactId == contactId + && l.IsActive)) + ?? throw new BusinessException("Contacts:ContactLinkNotFound") + .WithData("contactId", contactId) + .WithData("entityType", entityType) + .WithData("entityId", entityId); + + link.IsPrimary = true; + await contactLinkRepository.UpdateAsync(link, autoSave: true); + } + + /// + /// Clears the primary flag on all active contact links for the specified entity. + /// + private async Task ClearPrimaryAsync(string entityType, Guid entityId) + { + var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); + var currentPrimaryLinks = await AsyncExecuter.ToListAsync(contactLinksQuery + .Where(l => l.RelatedEntityType == entityType + && l.RelatedEntityId == entityId + && l.IsPrimary + && l.IsActive)); + + foreach (var existing in currentPrimaryLinks) + { + existing.IsPrimary = false; + await contactLinkRepository.UpdateAsync(existing, autoSave: true); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs new file mode 100644 index 0000000000..cf68d3360d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/IContactManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.GrantManager.Contacts; + +/// +/// Domain service for managing and writes. +/// Centralises primary-contact invariants so application services can stay thin and do not +/// need to call each other across module boundaries. +/// +public interface IContactManager +{ + /// + /// Creates a new contact and links it to the specified related entity. + /// When is true, any existing primary link on the entity is cleared first. + /// + Task<(Contact Contact, ContactLink Link)> CreateAsync( + string entityType, + Guid entityId, + ContactInput input, + string? role, + bool isPrimary); + + /// + /// Updates an existing contact's fields and its link to the specified related entity. + /// When is true, any other primary link on the entity is cleared first. + /// is only applied when non-null, matching legacy behaviour. + /// + Task<(Contact Contact, ContactLink Link)> UpdateAsync( + string entityType, + Guid entityId, + Guid contactId, + ContactInput input, + string? role, + bool isPrimary); + + /// + /// Marks the specified contact as the primary contact for the given related entity, + /// clearing the primary flag on any other active links. + /// + Task SetPrimaryAsync(string entityType, Guid entityId, Guid contactId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs index 127a2861ba..6fdf2c1c9f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.HttpApi/Controllers/ApplicantProfileController.cs @@ -10,7 +10,7 @@ namespace Unity.GrantManager.Controllers [ApiController] [Route("api/app/applicant-profiles")] [ServiceFilter(typeof(ApiKeyAuthorizationFilter))] - public class ApplicantProfileController(IApplicantProfileAppService applicantProfileAppService) : AbpControllerBase + public class ApplicantProfileController(IApplicantProfileQueryService applicantProfileService) : AbpControllerBase { /// @@ -29,7 +29,7 @@ public class ApplicantProfileController(IApplicantProfileAppService applicantPro [ProducesResponseType(typeof(ApplicantProfileDto), StatusCodes.Status200OK)] public async Task GetApplicantProfileAsync([FromQuery] ApplicantProfileInfoRequest applicantProfileRequest) { - var profile = await applicantProfileAppService.GetApplicantProfileAsync(applicantProfileRequest); + var profile = await applicantProfileService.GetApplicantProfileAsync(applicantProfileRequest); return Ok(profile); } @@ -37,7 +37,7 @@ public async Task GetApplicantProfileAsync([FromQuery] ApplicantP [Route("tenants")] public async Task GetApplicantProfileTenantsAsync([FromQuery] ApplicantProfileRequest applicantProfileRequest) { - var tenants = await applicantProfileAppService.GetApplicantTenantsAsync(applicantProfileRequest); + var tenants = await applicantProfileService.GetApplicantTenantsAsync(applicantProfileRequest); return Ok(tenants); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs new file mode 100644 index 0000000000..e38ed515e5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebAutoMapperProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; +using Unity.GrantManager.Web.Pages.ApplicantContact; + +namespace Unity.GrantManager.Web; + +public class GrantManagerWebAutoMapperProfile : Profile +{ + public GrantManagerWebAutoMapperProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.ContactId)); + + CreateMap(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs new file mode 100644 index 0000000000..4d9c66fada --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/ApplicantContactModalViewModel.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Unity.GrantManager.ApplicantProfile; + +namespace Unity.GrantManager.Web.Pages.ApplicantContact; + +public class ApplicantContactModalViewModel +{ + [HiddenInput] + public Guid ApplicantId { get; set; } + + [HiddenInput] + public Guid Id { get; set; } + + [HiddenInput] + public bool IsPrimaryInferred { get; set; } + + [DisplayName("ApplicationContact:Type")] + [Required] + [StringLength(100)] + public string Role { get; set; } = string.Empty; + + public List RoleOptions { get; set; } = CreateRoleOptions(); + + [DisplayName("ApplicationContact:FullName")] + [Required] + [MinLength(2)] + [StringLength(250)] + public string Name { get; set; } = string.Empty; + + [DisplayName("ApplicationContact:Title")] + [StringLength(200)] + public string? Title { get; set; } + + [DisplayName("ApplicationContact:Email")] + [EmailAddress] + [StringLength(200)] + public string? Email { get; set; } + + [DisplayName("ApplicationContact:MobilePhone")] + [StringLength(50)] + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$", ErrorMessage = "Enter a valid phone number")] + public string? MobilePhoneNumber { get; set; } + + [DisplayName("ApplicationContact:WorkPhone")] + [StringLength(50)] + [RegularExpression(@"^[\+]?[0-9\-\.\(\)\s]*$", ErrorMessage = "Enter a valid phone number")] + public string? WorkPhoneNumber { get; set; } + + [DisplayName("ApplicantContact:SetAsPrimary")] + public bool IsPrimary { get; set; } + + public void EnsureRoleOptions() + { + if (RoleOptions is null || RoleOptions.Count == 0) + { + RoleOptions = CreateRoleOptions(); + } + } + + public static List CreateRoleOptions() + { + return ApplicantContactRoleOptions.Options + .Select(option => new SelectListItem { Value = option.Value, Text = option.Label }) + .ToList(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml new file mode 100644 index 0000000000..feb60c2bde --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml @@ -0,0 +1,45 @@ +@page +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + +@model Unity.GrantManager.Web.Pages.ApplicantContact.EditModal + +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + +
+ + + + + + + + + + +
+
+ +
+
+ + @if (Model.ContactForm!.IsPrimary && Model.ContactForm.IsPrimaryInferred) + { +
@L["ApplicantContact:PrimaryHint"].Value
+ } +
+
+ + + + +
+ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs new file mode 100644 index 0000000000..a9622bf3b9 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantContact/EditModal.cshtml.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantContact; + +public class EditModal : AbpPageModel +{ + private readonly IApplicantContactAppService _applicantContactAppService; + + [BindProperty] + public ApplicantContactModalViewModel? ContactForm { get; set; } + + public EditModal(IApplicantContactAppService applicantContactAppService) + { + _applicantContactAppService = applicantContactAppService; + } + + public async Task OnGetAsync(Guid id, Guid applicantId) + { + var contactInfo = await _applicantContactAppService.GetByApplicantIdAsync(applicantId); + var contact = contactInfo.Contacts.FirstOrDefault(c => c.ContactId == id); + + if (contact is null || !contact.IsEditable) + { + return NotFound(); + } + + ContactForm = ObjectMapper.Map(contact); + ContactForm.ApplicantId = applicantId; + ContactForm.Id = contact.ContactId; + ContactForm.EnsureRoleOptions(); + + return Page(); + } + + public async Task OnPostAsync() + { + if (ContactForm is null) + { + return BadRequest(); + } + + ContactForm.EnsureRoleOptions(); + + if (!ModelState.IsValid) + { + return Page(); + } + + var updateDto = ObjectMapper.Map(ContactForm); + await _applicantContactAppService.UpdateAsync(ContactForm.ApplicantId, ContactForm.Id, updateDto); + + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsController.cs index da85ba2852..04ec4cd11d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsController.cs @@ -5,7 +5,6 @@ namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantContacts { - [ApiController] [Route("Widget/ApplicantContacts")] public class ApplicantContactsController : AbpController { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs index 6300519eca..5661af72e9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewComponent.cs @@ -3,15 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Contacts; +using Unity.GrantManager.ApplicantProfile; using Unity.Modules.Shared; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Widgets; using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Domain.Repositories; namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantContacts { @@ -20,30 +17,10 @@ namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantContacts ScriptTypes = new[] { typeof(ApplicantContactsScriptBundleContributor) }, StyleTypes = new[] { typeof(ApplicantContactsStyleBundleContributor) }, AutoInitialize = true)] - public class ApplicantContactsViewComponent : AbpViewComponent + public class ApplicantContactsViewComponent( + IApplicantContactQueryService applicantContactQueryService, + IPermissionChecker permissionChecker) : AbpViewComponent { - private const string ApplicantEntityType = "Applicant"; - - private readonly IApplicantAgentRepository _applicantAgentRepository; - private readonly IPermissionChecker _permissionChecker; - private readonly IRepository _applicationRepository; - private readonly IContactRepository _contactRepository; - private readonly IContactLinkRepository _contactLinkRepository; - - public ApplicantContactsViewComponent( - IApplicantAgentRepository applicantAgentRepository, - IPermissionChecker permissionChecker, - IRepository applicationRepository, - IContactRepository contactRepository, - IContactLinkRepository contactLinkRepository) - { - _applicantAgentRepository = applicantAgentRepository; - _permissionChecker = permissionChecker; - _applicationRepository = applicationRepository; - _contactRepository = contactRepository; - _contactLinkRepository = contactLinkRepository; - } - public async Task InvokeAsync(Guid applicantId) { if (applicantId == Guid.Empty) @@ -51,129 +28,36 @@ public async Task InvokeAsync(Guid applicantId) return View(new ApplicantContactsViewModel { ApplicantId = applicantId }); } - var agents = await _applicantAgentRepository.GetListByApplicantIdAsync(applicantId); - var orderedAgents = agents - .OrderByDescending(a => a.LastModificationTime ?? a.CreationTime) - .ToList(); - - var appRefMap = await BuildApplicationReferenceMapAsync(orderedAgents); - var linkedContacts = await GetLinkedContactsAsync(applicantId); - var agentContacts = MapAgentContacts(orderedAgents, appRefMap); + var aggregated = await applicantContactQueryService.GetByApplicantIdAsync(applicantId); - var allContacts = agentContacts.Concat(linkedContacts) - .OrderByDescending(c => c.CreationTime) + var contacts = aggregated.Contacts + .OrderByDescending(c => c.IsPrimary) + .ThenByDescending(c => c.CreationTime) .ToList(); - ResolvePrimaryContact(allContacts); - var viewModel = new ApplicantContactsViewModel { ApplicantId = applicantId, - CanEditContact = await _permissionChecker.IsGrantedAsync(UnitySelector.Applicant.Contact.Update), - Contacts = allContacts + CanEditContact = await permissionChecker.IsGrantedAsync(UnitySelector.Applicant.Contact.Update), + Contacts = contacts }; - var primaryContact = allContacts.FirstOrDefault(c => c.IsPrimary); - if (primaryContact != null) + var primary = contacts.FirstOrDefault(c => c.IsPrimary); + if (primary != null) { - viewModel.PrimaryContact = new ApplicantPrimaryContactViewModel + viewModel.PrimaryContact = new ApplicantPrimaryContactDisplayModel { - Id = primaryContact.Id, - Source = primaryContact.Source, - FullName = primaryContact.Name, - Title = primaryContact.Title, - Email = primaryContact.Email, - BusinessPhone = primaryContact.Phone, - CellPhone = string.Empty + ContactId = primary.ContactId, + FullName = primary.Name ?? string.Empty, + Title = primary.Title ?? string.Empty, + Email = primary.Email ?? string.Empty, + WorkPhone = primary.WorkPhoneNumber ?? string.Empty, + MobilePhone = primary.MobilePhoneNumber ?? string.Empty }; } return View(viewModel); } - - private async Task> BuildApplicationReferenceMapAsync(List agents) - { - var appIds = new HashSet( - agents.Where(a => a.ApplicationId.HasValue).Select(a => a.ApplicationId!.Value)); - - var appRefMap = new Dictionary(); - if (appIds.Count > 0) - { - var apps = await _applicationRepository.GetListAsync(a => appIds.Contains(a.Id)); - foreach (var app in apps) - { - appRefMap[app.Id] = app.ReferenceNo; - } - } - - return appRefMap; - } - - private static List MapAgentContacts( - List agents, - Dictionary appRefMap) - { - return agents - .Select(agent => new ApplicantContactItemDto - { - Id = agent.Id, - Name = agent.Name ?? string.Empty, - Email = agent.Email ?? string.Empty, - Phone = !string.IsNullOrWhiteSpace(agent.Phone) - ? agent.Phone! - : agent.Phone2 ?? string.Empty, - Title = agent.Title ?? string.Empty, - Type = string.Empty, - Source = "Agent", - IsPrimary = false, - CreationTime = agent.CreationTime, - ApplicationId = agent.ApplicationId, - ReferenceNo = agent.ApplicationId.HasValue - ? appRefMap.GetValueOrDefault(agent.ApplicationId.Value, string.Empty) - : string.Empty - }) - .ToList(); - } - - private static void ResolvePrimaryContact(List contacts) - { - var primaryContact = contacts.FirstOrDefault(c => c.IsPrimary) - ?? contacts.FirstOrDefault(); - - if (primaryContact != null) - { - primaryContact.IsPrimary = true; - } - } - - private async Task> GetLinkedContactsAsync(Guid applicantId) - { - var contactLinksQuery = await _contactLinkRepository.GetQueryableAsync(); - var contactsQuery = await _contactRepository.GetQueryableAsync(); - - return await ( - from link in contactLinksQuery - join contact in contactsQuery on link.ContactId equals contact.Id - where link.RelatedEntityType == ApplicantEntityType - && link.RelatedEntityId == applicantId - && link.IsActive - select new ApplicantContactItemDto - { - Id = contact.Id, - Name = contact.Name, - Email = contact.Email ?? string.Empty, - Phone = !string.IsNullOrWhiteSpace(contact.WorkPhoneNumber) - ? contact.WorkPhoneNumber! - : contact.MobilePhoneNumber ?? string.Empty, - Title = contact.Title ?? string.Empty, - Type = link.Role ?? string.Empty, - Source = "Contact", - IsPrimary = link.IsPrimary, - CreationTime = contact.CreationTime, - ApplicationId = null, - ReferenceNo = string.Empty - }).ToListAsync(); - } } public class ApplicantContactsStyleBundleContributor : BundleContributor diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs index 74bb3b2916..6276717f85 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/ApplicantContactsViewModel.cs @@ -1,47 +1,34 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using Unity.GrantManager.ApplicantProfile; +using Unity.GrantManager.ApplicantProfile.ProfileData; namespace Unity.GrantManager.Web.Views.Shared.Components.ApplicantContacts { + /// + /// View model for the Applicant Contacts widget on the internal Applicant Details page. + /// Aggregates three contact sources via + /// : + /// applicant-linked (), application contacts, and applicant agent contacts. + /// Only applicant-linked rows are editable; the primary contact is shown as read-only fields. + /// public class ApplicantContactsViewModel { public Guid ApplicantId { get; set; } public bool CanEditContact { get; set; } - public bool CanSave => CanEditContact && PrimaryContact.IsEditable; - public ApplicantPrimaryContactViewModel PrimaryContact { get; set; } = new(); - public List Contacts { get; set; } = new(); + public List Contacts { get; set; } = []; + public ApplicantPrimaryContactDisplayModel? PrimaryContact { get; set; } + public IReadOnlyList RoleOptions { get; set; } = ApplicantContactRoleOptions.Options; } - public class ApplicantPrimaryContactViewModel + /// Read-only display model for the primary contact summary shown above the grid. + public class ApplicantPrimaryContactDisplayModel { - public Guid Id { get; set; } - public string Source { get; set; } = string.Empty; - [Display(Name = "Full Name")] + public Guid ContactId { get; set; } public string FullName { get; set; } = string.Empty; - [Display(Name = "Title")] public string Title { get; set; } = string.Empty; - [Display(Name = "Business Phone")] - public string BusinessPhone { get; set; } = string.Empty; - [Display(Name = "Cell Phone")] - public string CellPhone { get; set; } = string.Empty; - [Display(Name = "Email")] public string Email { get; set; } = string.Empty; - public bool IsEditable => Id != Guid.Empty; - } - - public class ApplicantContactItemDto - { - public Guid Id { get; set; } - public string Name { get; set; } = string.Empty; - public string Email { get; set; } = string.Empty; - public string Phone { get; set; } = string.Empty; - public string Title { get; set; } = string.Empty; - public string Type { get; set; } = string.Empty; - public string Source { get; set; } = string.Empty; - public bool IsPrimary { get; set; } - public DateTime CreationTime { get; set; } - public string ReferenceNo { get; set; } = string.Empty; - public Guid? ApplicationId { get; set; } + public string WorkPhone { get; set; } = string.Empty; + public string MobilePhone { get; set; } = string.Empty; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml index f29c93bec3..c904e9d073 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.cshtml @@ -1,93 +1,112 @@ +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization @using Unity.GrantManager.Web.Views.Shared.Components.ApplicantContacts +@inject IStringLocalizer L @model ApplicantContactsViewModel @{ Layout = null; - var canEditPrimaryContact = Model.PrimaryContact.IsEditable && Model.CanEditContact; + var primary = Model.PrimaryContact; + const string placeholder = "—"; + var localizedTexts = new Dictionary + { + ["nullPlaceholder"] = placeholder, + ["primaryExplicitTooltip"] = L["ApplicantContacts:PrimaryExplicitTooltip"], + ["primaryInferredTooltip"] = L["ApplicantContacts:PrimaryInferredTooltip"], + ["sourceInfoApplication"] = L["ApplicantContacts:SourceInfoApplication"], + ["sourceInfoApplicantAgent"] = L["ApplicantContacts:SourceInfoApplicantAgent"], + ["sourceInfoGeneric"] = L["ApplicantContacts:SourceInfoGeneric"], + ["primaryContactVisuallyHidden"] = L["ApplicantContacts:PrimaryContact"], + ["edit"] = L["Common:Command:Edit"], + ["setAsPrimary"] = L["ApplicantContact:SetAsPrimary"], + ["view"] = L["ApplicantContacts:View"], + ["contactSaved"] = L["ApplicantContacts:ContactSaved"], + ["contactSetPrimary"] = L["ApplicantContacts:ContactSetPrimary"], + ["serviceUnavailable"] = L["ApplicantContacts:ServiceUnavailable"], + ["setPrimaryFailed"] = L["ApplicantContacts:SetPrimaryFailed"], + ["columnName"] = L["ApplicantContacts:ColumnName"], + ["columnType"] = L["ApplicationContact:Type"], + ["columnEmail"] = L["ApplicationContact:Email"], + ["columnPhone"] = L["ApplicantContacts:ColumnPhone"], + ["columnTitle"] = L["ApplicationContact:Title"], + ["columnSubmission"] = L["ApplicantContacts:ColumnSubmission"], + ["columnActions"] = string.Empty + }; } -
+
@if (Model.ApplicantId == Guid.Empty) {
- Applicant information not found. + @L["ApplicantContacts:ApplicantNotFound"]
} else { -
- - - - - @if (Model.CanSave) - { -
- -
- } - -
-
Primary Contact
-
-
+
+
+
+
@L["ApplicantContacts:PrimaryContact"]
+
+
+ @if (primary is null) + { +
@L["ApplicantContacts:NoPrimaryContact"]
+ } + else + { +
- + +
- + +
- + +
- + +
- + +
- @if (!Model.PrimaryContact.IsEditable) - { -
No primary contact on record.
- } -
-
+ } +
+
-
-
-
Contacts
-
-
- - -
+
+
+
@L["ApplicantContacts:ContactsTitle"]
+
+
+ +
- +
+
}
+ + + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.css index 844be9dbe5..3528115acc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.css @@ -4,6 +4,76 @@ position: relative; } +.applicant-contact-edit-form > .mb-3, +.applicant-contact-edit-form > .form-check, +.applicant-contact-edit-form > .abp-input, +.applicant-contact-edit-form > .abp-select { + width: 100%; +} + +.applicant-contacts-widget .applicant-contacts-info { + border-radius: 8px; + padding: 0 1rem 1rem 1rem; + margin-bottom: 0; +} + +.applicant-contacts-widget .applicant-contact-source-info { + display: inline-flex; + align-items: center; + cursor: help; + color: var(--bc-colors-grey-text-300); + font-size: 1rem; +} + +.applicant-contacts-widget .applicant-contact-source-info:hover { + color: var(--bc-colors-primary); +} + +.applicant-contacts-widget .applicant-contact-primary-badge { + display: inline-flex; + align-items: center; + margin-right: 0.35rem; + cursor: help; + font-size: 0.95rem; + opacity: 0.75; +} + +.applicant-contacts-widget .applicant-contact-primary-badge:hover { + opacity: 1; +} + +.applicant-contacts-widget .applicant-contact-edit-btn { + border: none; + box-shadow: none; + outline: none; + transition: none; +} + +.applicant-contacts-widget .applicant-contact-edit-btn:hover, +.applicant-contacts-widget .applicant-contact-edit-btn:focus { + border: none; + box-shadow: none; + outline: none; +} + +.applicant-contacts-widget .applicant-contact-menu-btn { + border: none; + box-shadow: none; + outline: none; + transition: none; + text-decoration: none; + color: var(--bc-colors-grey-text-300); +} + +.applicant-contacts-widget .applicant-contact-menu-btn:hover, +.applicant-contacts-widget .applicant-contact-menu-btn:focus { + border: none; + box-shadow: none; + outline: none; + text-decoration: none; + color: var(--bc-colors-primary); +} + .applicant-contacts-widget .applicant-organization-info { border-radius: 8px; padding: 0 1rem 1rem 1rem; @@ -37,6 +107,23 @@ border-bottom: 1px solid #dee2e6; } +.applicant-contacts-widget .primary-section__header { + padding: 0 0 0.5rem 0; +} + +.applicant-contacts-widget .primary-readonly dt { + font-size: var(--bc-font-size-sm); + color: var(--bc-colors-grey-text-300); + font-weight: 500; + margin-bottom: 0.125rem; +} + +.applicant-contacts-widget .primary-readonly dd { + font-size: var(--bc-font-size-base); + margin-bottom: 0; + word-break: break-word; +} + .applicant-contacts-widget .table-card { background-color: #fff; border: 1px solid #e0e6ed; @@ -58,6 +145,25 @@ width: 100%; } +.applicant-contacts-widget #ApplicantContactsTable td:last-child, +.applicant-contacts-widget #ApplicantContactsTable th:last-child { + text-align: center; + white-space: nowrap; +} + +.applicant-contacts-widget #ApplicantContactsTable td:last-child .applicant-contact-menu-btn, +.applicant-contacts-widget #ApplicantContactsTable td:last-child .applicant-contact-source-info, +.applicant-contacts-widget #ApplicantContactsTable td:last-child .applicant-contact-primary-badge { + display: inline-flex; + align-items: center; + vertical-align: middle; + margin-right: 0.25rem; +} + +.applicant-contacts-widget #ApplicantContactsTable td:last-child *:last-child { + margin-right: 0; +} + #ApplicantContactsTable_wrapper { width: 100%; overflow: visible !important; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js index c784527fee..813754da2d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js @@ -1,41 +1,259 @@ $(function () { const LAYOUT_NOTIFICATION_DELAYS = [0, 120, 300, 700]; - const contactsRaw = $('#ApplicantContacts_Data').val(); - const contactsData = safeParse(contactsRaw); - const nullPlaceholder = '—'; + let widgetRoot = $(); + let applicantId = null; + let canEdit = false; + let contactsData = []; + let roleLabelMap = {}; + let localizedTexts = {}; let contactsTable = null; - let zoneForm = null; + let savedOrder = null; + let editContactModal = null; - function renderTableLink(data, row) { - if (!data || !row.applicationId) { - return nullPlaceholder; + function t(key, fallback) { + return localizedTexts[key] || fallback; + } + + function format(template, value) { + return (template || '').replace('{0}', value); + } + + function ensureEditContactModal() { + if (editContactModal) { + return editContactModal; + } + + editContactModal = new abp.ModalManager(abp.appPath + 'ApplicantContact/EditModal'); + editContactModal.onResult(function () { + abp.notify.success(t('contactSaved', 'Contact saved.')); + refreshWidget(); + }); + + return editContactModal; + } + + function readWidgetState() { + widgetRoot = $('.applicant-contacts-widget'); + applicantId = widgetRoot.data('applicant-id'); + canEdit = widgetRoot.data('can-edit') === true || widgetRoot.data('can-edit') === 'true'; + + contactsData = safeParse($('#ApplicantContacts_Data').val()).map(toCamelCase); + localizedTexts = safeParse($('#ApplicantContacts_Texts').val()); + + roleLabelMap = {}; + safeParse($('#ApplicantContacts_RoleOptions').val()).forEach(function (option) { + const normalized = toCamelCase(option); + roleLabelMap[normalized.value] = normalized.label; + }); + } + + function pickCaseInsensitive(row, names) { // NOSONAR - intentionally scoped here; closure context is needed for widget encapsulation + if (!row) { return undefined; } + for (const name of names) { + if (row[name] !== undefined && row[name] !== null) { return row[name]; } + const lower = name.charAt(0).toLowerCase() + name.slice(1); + if (row[lower] !== undefined && row[lower] !== null) { return row[lower]; } + const upper = name.charAt(0).toUpperCase() + name.slice(1); + if (row[upper] !== undefined && row[upper] !== null) { return row[upper]; } + } + return undefined; + } + + function renderReferenceLink(data, type, row) { + const appId = pickCaseInsensitive(row, ['applicationId']); + const refNo = data || pickCaseInsensitive(row, ['referenceNo']); + const hasAppId = !!appId && appId !== '00000000-0000-0000-0000-000000000000'; + if (!hasAppId) { + return t('nullPlaceholder', '—'); } + const label = refNo || t('view', 'View'); + return `${label}`; + } - return `${data}`; + function renderPrimaryBadge(row) { // NOSONAR - intentionally scoped here; closure context is needed for widget encapsulation + if (!row.isPrimary) { + return ''; + } + const title = row.isPrimaryInferred + ? t('primaryInferredTooltip', 'Primary contact (auto-selected by most recent timestamp; not explicitly set).') + : t('primaryExplicitTooltip', 'Primary contact'); + return ` + + ${t('primaryContactVisuallyHidden', 'Primary contact')} + `; + } + + function renderActions(data, type, row) { + if (row.contactType !== 'Applicant') { + const message = row.contactType === 'Application' + ? t('sourceInfoApplication', 'Sourced from the Application submission. Managed on the Application Details form and cannot be edited here.') + : row.contactType === 'ApplicantAgent' + ? t('sourceInfoApplicantAgent', 'Sourced from the Applicant Agent on the CHEFS submission. Captured at intake and cannot be edited here.') + : format(t('sourceInfoGeneric', 'Sourced from {0} and cannot be edited here.'), row.contactType || 'another record'); + const escaped = $('
').text(message).html(); + return ` + + ${escaped} + `; + } + if (!canEdit) { + return ''; + } + const setPrimaryDisabled = row.isPrimary ? 'disabled' : ''; + return ``; + } + + function setAsPrimary(contact) { + const service = unity?.grantManager?.applicantProfile?.applicantContact; + if (!service) { + abp.notify.error(t('serviceUnavailable', 'Applicant contact service is not available.')); + return; + } + service.setPrimary(applicantId, contact.contactId) + .done(function () { + abp.notify.success(t('contactSetPrimary', 'Contact set as primary.')); + refreshWidget(); + }) + .fail(function () { + abp.notify.error(t('setPrimaryFailed', 'Failed to set contact as primary.')); + }); } - function initializeContactsTable(selector, data, columnDefs, extraConfig = {}) { - if (!$.fn.DataTable || !$(selector).length) { + function initializeContactsTable(order) { + if (!$.fn.DataTable || !$('#ApplicantContactsTable').length) { return null; } - return $(selector).DataTable( + if ($.fn.DataTable.isDataTable('#ApplicantContactsTable')) { + $('#ApplicantContactsTable').DataTable().destroy(); + } + + return $('#ApplicantContactsTable').DataTable( abp.libs.datatables.normalizeConfiguration({ - data: data, + data: contactsData, serverSide: false, - order: [[0, 'asc']], + order: order || [[0, 'asc']], searching: true, paging: true, pageLength: 10, select: false, info: true, + processing: true, scrollX: true, + stateSave: true, + stateDuration: 0, + stateSaveCallback: function (settings, data) { + try { + localStorage.setItem( + 'DataTables_ApplicantContactsTable_' + (applicantId || 'none'), + JSON.stringify(data)); + } catch (e) { console.error('Failed to save DataTables state to localStorage.', e); } + }, + stateLoadCallback: function () { + try { + const raw = localStorage.getItem( + 'DataTables_ApplicantContactsTable_' + (applicantId || 'none')); + return raw ? JSON.parse(raw) : null; + } catch (e) { console.error('Failed to load DataTables state from localStorage.', e); return null; } + }, drawCallback: function () { this.api().columns.adjust(); + $('#ApplicantContactsTable [data-bs-toggle="tooltip"]').each(function () { + const existing = bootstrap.Tooltip.getInstance(this); + if (existing) { existing.dispose(); } + bootstrap.Tooltip.getOrCreateInstance(this); + }); }, - ...extraConfig, - columnDefs: columnDefs + lengthMenu: [[10, 25, 50], [10, 25, 50]], + columnDefs: [ + { + title: t('columnName', 'Name'), + data: 'name', + width: '18%', + render: (data, type, row) => { + const name = data || t('nullPlaceholder', '—'); + if (type !== 'display') { + return name; + } + return renderPrimaryBadge(row) + name; + }, + targets: 0 + }, + { + title: t('columnType', 'Type'), + data: 'role', + width: '13%', + render: (data) => roleLabelMap[data] || data || t('nullPlaceholder', '—'), + targets: 1 + }, + { + title: t('columnEmail', 'Email'), + data: 'email', + width: '22%', + render: (data) => data || t('nullPlaceholder', '—'), + targets: 2 + }, + { + title: t('columnPhone', 'Phone'), + data: null, + width: '13%', + render: (data, type, row) => { + const phone = row.workPhoneNumber || row.mobilePhoneNumber; + return phone || t('nullPlaceholder', '—'); + }, + targets: 3 + }, + { + title: t('columnTitle', 'Title'), + data: 'title', + width: '18%', + render: (data) => data || t('nullPlaceholder', '—'), + targets: 4 + }, + { + title: t('columnSubmission', 'Submission #'), + data: 'referenceNo', + width: '10%', + render: renderReferenceLink, + targets: 5 + }, + { + title: t('columnActions', ''), + data: null, + orderable: false, + searchable: false, + width: '48px', + className: 'text-center', + render: renderActions, + targets: 6 + } + ] }) ); } @@ -46,121 +264,56 @@ $(function () { }); } - contactsTable = initializeContactsTable( - '#ApplicantContactsTable', - contactsData, - [ - { - title: 'Name', - data: 'name', - width: '18%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Email', - data: 'email', - width: '22%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Phone', - data: 'phone', - width: '13%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Title', - data: 'title', - width: '17%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Type', - data: 'type', - width: '10%', - render: (data) => data || nullPlaceholder - }, - { - title: 'Primary', - data: 'isPrimary', - width: '8%', - render: (data) => data ? 'Yes' : '' - }, - { - title: 'Submission #', - data: 'referenceNo', - width: '15%', - render: (data, type, row) => renderTableLink(data, row) - } - ], - { - lengthMenu: [[10, 25, 50], [10, 25, 50]] + function refreshWidget() { + if (contactsTable) { + try { + savedOrder = contactsTable.order(); + contactsTable.processing(true); + } catch (e) { console.error('Failed to enable DataTables processing indicator.', e); } } - ); - - scheduleLayoutNotifications(); - - const form = $('#ApplicantContactsForm'); - const saveButton = $('#saveApplicantContactsBtn'); - - if (form.length && saveButton.length && typeof UnityZoneForm === 'function') { - zoneForm = new UnityZoneForm(form, { - saveButtonSelector: '#saveApplicantContactsBtn' - }); - - zoneForm.init(); - - saveButton.on('click', function (event) { - event.preventDefault(); - if (!zoneForm || zoneForm.modifiedFields.size === 0) { - return; - } - - const applicantId = $('#ApplicantContacts_ApplicantId').val(); - if (!applicantId) { - abp.notify.warn('Applicant identifier is missing.'); - return; + $.ajax({ + url: abp.appPath + 'Widget/ApplicantContacts/Refresh', + type: 'GET', + dataType: 'html', + data: { applicantId: applicantId }, + success: function (html) { + const container = widgetRoot.parent(); + container.html(html); + bindWidget(savedOrder); + savedOrder = null; + abp.event.trigger('applicant-contacts-refreshed'); + }, + error: function () { + if (contactsTable) { + try { contactsTable.processing(false); } catch (e) { console.error('Failed to disable DataTables processing indicator.', e); } + } } + }); + } - const modifiedFields = Array.from(zoneForm.modifiedFields ?? []); - const contactDirty = modifiedFields.some((field) => field.startsWith('PrimaryContact.')); + function bindWidget(order) { + readWidgetState(); + contactsTable = initializeContactsTable(order); + scheduleLayoutNotifications(); + } - if (!contactDirty) { - return; - } + $(document).on('click', '.applicant-contact-edit-btn', function () { + const id = $(this).data('contact-id'); + ensureEditContactModal().open({ + id: id, + applicantId: applicantId + }); + }); - const contactId = $('#ApplicantContacts_PrimaryContactId').val(); - if (isGuidEmpty(contactId)) { - return; - } + $(document).on('click', '.applicant-contact-set-primary-btn', function () { + const id = $(this).data('contact-id'); + const contact = contactsData.find((c) => c.contactId === id); + if (!contact) return; + setAsPrimary(contact); + }); - const contactSource = $('#ApplicantContacts_PrimaryContactSource').val(); - - const payload = { - primaryContact: { - id: contactId, - source: contactSource, - fullName: form.find('[name="PrimaryContact.FullName"]').val(), - title: form.find('[name="PrimaryContact.Title"]').val(), - email: form.find('[name="PrimaryContact.Email"]').val(), - businessPhone: form.find('[name="PrimaryContact.BusinessPhone"]').val(), - cellPhone: form.find('[name="PrimaryContact.CellPhone"]').val() - } - }; - - unity.grantManager.applicants.applicant - .updateApplicantContactAddresses(applicantId, payload) - .done(function () { - abp.notify.success('Contact updated.'); - zoneForm.resetTracking(); - updateContactTableAfterSave(payload.primaryContact, contactsTable); - }) - .fail(function () { - abp.notify.error('Failed to update contact.'); - }); - }); - } + bindWidget(); }); function safeParse(value) { @@ -172,29 +325,20 @@ function safeParse(value) { } } -function notifyApplicantContactsLayoutChange() { - globalThis.dispatchEvent(new CustomEvent('applicant-contacts-layout-changed')); -} - -function isGuidEmpty(value) { - return !value || value === '00000000-0000-0000-0000-000000000000'; -} - -function updateContactTableAfterSave(contactPayload, contactsDt) { - if (!contactsDt || !contactPayload) { - return; +function toCamelCase(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; } - - contactsDt.rows().every(function () { - const rowData = this.data(); - if (rowData.id === contactPayload.id) { - rowData.name = contactPayload.fullName || ''; - rowData.email = contactPayload.email || ''; - rowData.phone = contactPayload.businessPhone || contactPayload.cellPhone || ''; - rowData.title = contactPayload.title || ''; - this.data(rowData); - } + const result = {}; + Object.keys(obj).forEach(function (key) { + const camelKey = key.length > 0 + ? key.charAt(0).toLowerCase() + key.slice(1) + : key; + result[camelKey] = obj[key]; }); + return result; +} - contactsDt.rows().invalidate().draw(false); +function notifyApplicantContactsLayoutChange() { + globalThis.dispatchEvent(new CustomEvent('applicant-contacts-layout-changed')); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs index 010123c805..5316101917 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.cs @@ -24,27 +24,37 @@ public class ChefsAttachments( IApplicationFormRepository applicationFormRepository) : AbpViewComponent { public async Task InvokeAsync(Guid applicationFormId) - { - var featureEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); + { + // Set safe defaults so the view is never left with null ViewBag values + // even if an exception is thrown partway through the checks below. + ViewBag.IsAIAttachmentSummariesEnabled = false; + ViewBag.IsAIAttachmentSummariesGenerateEnabled = false; - // View guard — for toggling visibility of existing summaries - ViewBag.IsAIAttachmentSummariesEnabled = - featureEnabled && - await permissionChecker.IsGrantedAsync(AIPermissions.Analysis.ViewAttachmentSummary); + var featureEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); - // Generate guard — full 3-level chain for the Generate Summary button - var settingProvider = LazyServiceProvider.LazyGetRequiredService(); - var tenantManualEnabled = await settingProvider.GetAsync(AISettings.ManualGenerationEnabled, defaultValue: false); - var applicationForm = await applicationFormRepository.GetAsync(applicationFormId); + // View guard — for toggling visibility of existing summaries + ViewBag.IsAIAttachmentSummariesEnabled = + featureEnabled && + await permissionChecker.IsGrantedAsync(AIPermissions.Analysis.ViewAttachmentSummary); - ViewBag.IsAIAttachmentSummariesGenerateEnabled = - featureEnabled && - tenantManualEnabled && - applicationForm.ManuallyInitiateAIAnalysis && - await permissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateAttachmentSummaries); + if (applicationFormId == Guid.Empty) + { + return View(); + } - return View(); - } + // Generate guard — full 3-level chain for the Generate Summary button + var settingProvider = LazyServiceProvider.LazyGetRequiredService(); + var tenantManualEnabled = await settingProvider.GetAsync(AISettings.ManualGenerationEnabled, defaultValue: false); + var applicationForm = await applicationFormRepository.GetAsync(applicationFormId); + + ViewBag.IsAIAttachmentSummariesGenerateEnabled = + featureEnabled && + tenantManualEnabled && + applicationForm.ManuallyInitiateAIAnalysis && + await permissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateAttachmentSummaries); + + return View(); + } } public class ChefsAttachmentsStyleBundleContributor : BundleContributor diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml index 3a2b2aa3a0..b2a1f3478c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml @@ -7,7 +7,7 @@
Submission Attachments
- @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled) + @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled == true) { } - @if (ViewBag.IsAIAttachmentSummariesEnabled) + @if (ViewBag.IsAIAttachmentSummariesEnabled == true) { -
-
+ @if (aiAttachmentSummariesEnabled && aiApplicationAnalysisEnabled && aiScoringEnabled) + { + + } +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index d0d87af7c8..53022382d6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -175,6 +175,33 @@ $(function () { $(selector).text(value ? `(${value})` : ''); } + function setAiGenerationStatus(value) { + $('#aiGenerationStatus').text(value ? `(${value})` : ''); + } + + function formatAiGenerationStatus(status) { + if (status === null || status === undefined || status === '') { + return ''; + } + + if (typeof status === 'string') { + return status; + } + + switch (status) { + case 0: + return 'Queued'; + case 1: + return 'Running'; + case 2: + return 'Completed'; + case 3: + return 'Failed'; + default: + return String(status); + } + } + function getScoresheetSchemaJson() { return $('#ApplicationScoresheetSchemaJson').val() || $('#AssessmentScoresheetSchemaJson').val() || @@ -351,6 +378,56 @@ $(function () { }); } + let aiGenerationPollTimeoutId = null; + + function stopAIGenerationPolling() { + if (aiGenerationPollTimeoutId) { + clearTimeout(aiGenerationPollTimeoutId); + aiGenerationPollTimeoutId = null; + } + } + + function pollAIGenerationStatus(applicationId, promptVersion, restoreButton, originalHtml) { + const poll = function() { + unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(applicationId, 'pipeline', promptVersion) + .done(function(request) { + const statusText = formatAiGenerationStatus(request?.status); + setAiGenerationStatus(statusText); + + if (statusText === 'Failed') { + stopAIGenerationPolling(); + setAiGenerationStatus('Failed'); + loadDevAiOutputs(); + restoreButton.html(originalHtml).prop('disabled', false); + abp.message.error(request?.failureReason || 'AI generate all failed.'); + return; + } + + if (!request || request.isActive === false || statusText === 'Completed') { + stopAIGenerationPolling(); + setAiGenerationStatus(''); + setDevAiOutputTimestamp('#analysisAiOutputTimestamp', request?.completedAt || request?.startedAt || null); + setDevAiOutputTimestamp('#scoringAiOutputTimestamp', request?.completedAt || request?.startedAt || null); + loadDevAiOutputs(); + restoreButton.html(originalHtml).prop('disabled', false); + if (statusText === 'Completed') { + abp.notify.success('AI generate all completed.'); + } + return; + } + + aiGenerationPollTimeoutId = setTimeout(poll, 2000); + }) + .fail(function() { + aiGenerationPollTimeoutId = setTimeout(poll, 3000); + }); + }; + + stopAIGenerationPolling(); + aiGenerationPollTimeoutId = setTimeout(poll, 500); + } + globalThis.refreshDevAiOutputs = loadDevAiOutputs; globalThis.generateAllAIDevOutputs = function(triggerButton = null) { @@ -366,18 +443,22 @@ $(function () { $button .html('Queueing...') .prop('disabled', true); - - unity.grantManager.grantApplications.applicationContent - .generateContent(applicationId, promptVersion) - .done(function() { - abp.notify.success('AI generate all queued. Refresh later to see updated results.'); + setAiGenerationStatus('Queueing'); + + unity.grantManager.grantApplications.grantApplication + .queueAIGeneration(applicationId, promptVersion) + .done(function(request) { + const statusText = formatAiGenerationStatus(request?.status); + setAiGenerationStatus(statusText || 'Queued'); + pollAIGenerationStatus(applicationId, promptVersion, $button, existingHtml); + abp.notify.success('AI generate all queued.'); }) .fail(function() { + setAiGenerationStatus(''); abp.message.error('Failed to queue AI generate all. Please try again.'); - }) - .always(function() { $button.html(existingHtml).prop('disabled', false); - }); + }) + ; }; $('#generateAllAiDevToolsBtn').on('click', function() { diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs new file mode 100644 index 0000000000..3d0e17d3e5 --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs @@ -0,0 +1,132 @@ +using Medallion.Threading; +using NSubstitute; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.GrantApplications.Automation; +using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.DistributedLocking; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.GrantApplications.Automation; + +public class AIGenerationQueueTests(ITestOutputHelper outputHelper) : GrantManagerApplicationTestBase(outputHelper) +{ + [Fact] + public void BuildPipelineRequestKey_Should_Normalize_Identity() + { + var key = ApplicationAIGenerationQueue.BuildPipelineRequestKey( + Guid.Parse("11111111-1111-1111-1111-111111111111"), + Guid.Parse("22222222-2222-2222-2222-222222222222"), + "v1"); + + key.ShouldBe("11111111-1111-1111-1111-111111111111:22222222-2222-2222-2222-222222222222:none:pipeline:v1"); + } + + [Fact] + public async Task QueueApplicationPipelineAsync_Should_Not_Enqueue_When_An_Active_Request_Already_Exists() + { + var tenantId = Guid.NewGuid(); + var applicationId = Guid.NewGuid(); + var promptVersion = "v1"; + var requestKey = ApplicationAIGenerationQueue.BuildPipelineRequestKey(tenantId, applicationId, promptVersion); + var request = new AIGenerationRequest( + Guid.NewGuid(), + tenantId, + AIGenerationRequestKeyHelper.PipelineOperationType, + applicationId, + null, + promptVersion, + requestKey); + + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var lockProvider = new TestDistributedLockProvider(); + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, lockProvider); + + await queue.QueueApplicationPipelineAsync(applicationId, tenantId, promptVersion); + + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any()); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueApplicationPipelineAsync_Should_Enqueue_New_Request_When_None_Exists() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + repository.InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + + RunApplicationAIPipelineJobArgs? capturedArgs = null; + var backgroundJobManager = Substitute.For(); + backgroundJobManager.EnqueueAsync(Arg.Any()) + .Returns(callInfo => + { + capturedArgs = callInfo.Arg(); + return Task.CompletedTask; + }); + + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueApplicationPipelineAsync(applicationId, tenantId, "v1"); + + capturedArgs.ShouldNotBeNull(); + capturedArgs!.ApplicationId.ShouldBe(applicationId); + capturedArgs.TenantId.ShouldBe(tenantId); + capturedArgs.PromptVersion.ShouldBe("v1"); + capturedArgs.RequestKey.ShouldBe(ApplicationAIGenerationQueue.BuildPipelineRequestKey(tenantId, applicationId, "v1")); + await repository.Received(1).InsertAsync(Arg.Is(r => + r.ApplicationId == applicationId && + r.TenantId == tenantId && + r.OperationType == AIGenerationRequestKeyHelper.PipelineOperationType && + r.RequestKey == capturedArgs.RequestKey && + r.Status == AIGenerationRequestStatus.Queued), Arg.Any(), Arg.Any()); + } + + private sealed class TestDistributedLockProvider : IDistributedLockProvider + { + public IDistributedLock CreateLock(string name) => new TestDistributedLock(name); + } + + private sealed class TestDistributedLock(string name) : IDistributedLock + { + public string Name => name; + + public IDistributedSynchronizationHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + new TestDistributedSynchronizationHandle(); + + public ValueTask AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + ValueTask.FromResult(new TestDistributedSynchronizationHandle()); + + public IDistributedSynchronizationHandle? TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + new TestDistributedSynchronizationHandle(); + + public ValueTask TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + ValueTask.FromResult(new TestDistributedSynchronizationHandle()); + } + + private sealed class TestDistributedSynchronizationHandle : IDistributedSynchronizationHandle + { + public void Dispose() + { + } + + public ValueTask DisposeAsync() + { + Dispose(); + return ValueTask.CompletedTask; + } + } +} diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs deleted file mode 100644 index 64884e5c65..0000000000 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using NSubstitute; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Unity.GrantManager.Applications; -using Unity.GrantManager.Attachments; -using Unity.GrantManager.GrantApplications; -using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -using Unity.GrantManager.GrantApplications.Automation.Events; -using Volo.Abp.EventBus.Local; -using Volo.Abp.Features; -using Volo.Abp.MultiTenancy; -using Xunit; -using Xunit.Abstractions; - -namespace Unity.GrantManager.GrantApplications.Automation; - -public class RunApplicationAIPipelineJobTests(ITestOutputHelper outputHelper) : GrantManagerApplicationTestBase(outputHelper) -{ - private static RunApplicationAIPipelineJob BuildJob( - IFeatureChecker featureChecker, - IApplicationScoringAppService? scoringService = null) - { - var attachmentService = Substitute.For(); - attachmentService.GenerateAttachmentSummariesForPipelineAsync(Arg.Any>(), Arg.Any()) - .Returns(Task.FromResult(new List())); - - var analysisService = Substitute.For(); - analysisService.GenerateApplicationAnalysisForPipelineAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new ApplicationAnalysisResultDto { Completed = true })); - - return new RunApplicationAIPipelineJob( - Substitute.For(), - attachmentService, - analysisService, - scoringService ?? Substitute.For(), - featureChecker, - Substitute.For(), - Substitute.For(), - NullLogger.Instance); - } - - [Fact] - public async Task ExecuteAsync_Should_Skip_Scoring_When_Feature_Disabled() - { - var featureChecker = Substitute.For(); - featureChecker.IsEnabledAsync("Unity.AI.Scoring").Returns(false); - featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries").Returns(false); - featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(false); - - var scoringService = Substitute.For(); - scoringService.GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new ApplicationScoringResultDto { Completed = true })); - - var job = BuildJob(featureChecker, scoringService); - - await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = Guid.NewGuid() }); - - await scoringService.DidNotReceive().GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task ExecuteAsync_Should_Run_Scoring_When_Feature_Enabled() - { - var featureChecker = Substitute.For(); - featureChecker.IsEnabledAsync("Unity.AI.Scoring").Returns(true); - featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries").Returns(false); - featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(false); - - var scoringService = Substitute.For(); - scoringService.GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new ApplicationScoringResultDto { Completed = true })); - - var job = BuildJob(featureChecker, scoringService); - - await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = Guid.NewGuid() }); - - await scoringService.Received(1).GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task ExecuteAsync_Should_Publish_Scoring_Event_When_Scoring_Completes() - { - var featureChecker = Substitute.For(); - featureChecker.IsEnabledAsync("Unity.AI.Scoring").Returns(true); - featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries").Returns(false); - featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(false); - - var scoringService = Substitute.For(); - scoringService.GenerateApplicationScoringForPipelineAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new ApplicationScoringResultDto { Completed = true })); - - var eventBus = Substitute.For(); - var job = new RunApplicationAIPipelineJob( - Substitute.For(), - Substitute.For(), - Substitute.For(), - scoringService, - featureChecker, - eventBus, - Substitute.For(), - NullLogger.Instance); - - await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs { ApplicationId = Guid.NewGuid() }); - - await eventBus.Received(1).PublishAsync(Arg.Is(e => e.ApplicationId != Guid.Empty)); - } -} From 9b3bb396c475b660d3405e499ed75cf710340aca Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 11:11:32 -0700 Subject: [PATCH 039/134] AB#32451 add AI request polling to generation flows --- .../AIGenerationRequestKeyHelper.cs | 3 + .../Pages/GrantApplications/Details.js | 94 ++++++++++++++++--- .../Pages/GrantApplications/ai-analysis.js | 49 +++++++++- .../Automation/AIGenerationQueueTests.cs | 9 +- 4 files changed, 139 insertions(+), 16 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs index e38b31aeeb..8b97e0574d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/GrantApplications/AIGenerationRequestKeyHelper.cs @@ -4,6 +4,9 @@ namespace Unity.GrantManager.GrantApplications; public static class AIGenerationRequestKeyHelper { + public const string AttachmentSummaryOperationType = "attachment-summary"; + public const string ApplicationAnalysisOperationType = "application-analysis"; + public const string ApplicationScoringOperationType = "application-scoring"; public const string PipelineOperationType = "pipeline"; public static string BuildRequestKey(Guid? tenantId, Guid applicationId, string operationType, string? promptVersion = null, Guid? attachmentId = null) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index 53022382d6..216f16bdfe 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -379,6 +379,7 @@ $(function () { } let aiGenerationPollTimeoutId = null; + const aiGenerationPollIntervalMs = 15000; function stopAIGenerationPolling() { if (aiGenerationPollTimeoutId) { @@ -387,10 +388,10 @@ $(function () { } } - function pollAIGenerationStatus(applicationId, promptVersion, restoreButton, originalHtml) { + function pollAIGenerationStatus(applicationId, operationType, promptVersion, restoreButton, originalHtml) { const poll = function() { unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(applicationId, 'pipeline', promptVersion) + .getAIGenerationStatus(applicationId, operationType, promptVersion) .done(function(request) { const statusText = formatAiGenerationStatus(request?.status); setAiGenerationStatus(statusText); @@ -417,10 +418,10 @@ $(function () { return; } - aiGenerationPollTimeoutId = setTimeout(poll, 2000); + aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); }) .fail(function() { - aiGenerationPollTimeoutId = setTimeout(poll, 3000); + aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); }); }; @@ -428,6 +429,77 @@ $(function () { aiGenerationPollTimeoutId = setTimeout(poll, 500); } + function queueAIGenerationOperation(queueAction, operationType, queuedMessage, failureMessage, restoreButton, originalHtml) { + queueAction() + .done(function(request) { + const statusText = formatAiGenerationStatus(request?.status); + setAiGenerationStatus(statusText || 'Queued'); + pollAIGenerationStatus( + $('#DetailsViewApplicationId').val(), + operationType, + globalThis.getSelectedPromptVersion?.() || null, + restoreButton, + originalHtml + ); + abp.notify.success(queuedMessage); + }) + .fail(function() { + setAiGenerationStatus(''); + abp.message.error(failureMessage); + restoreButton.html(originalHtml).prop('disabled', false); + }); + } + + globalThis.queueAttachmentSummary = function(triggerButton = null) { + const applicationId = $('#DetailsViewApplicationId').val(); + const $button = triggerButton ? $(triggerButton) : $('[onclick*="queueAttachmentSummary"]').first(); + const existingHtml = $button.html(); + const promptVersion = globalThis.getSelectedPromptVersion?.() || null; + + if (!applicationId || $button.prop('disabled')) { + return; + } + + $button + .html('Queueing...') + .prop('disabled', true); + setAiGenerationStatus('Queueing'); + + queueAIGenerationOperation( + () => unity.grantManager.attachments.attachmentSummary.generateAttachmentSummary(applicationId, promptVersion), + 'attachment-summary', + 'AI attachment summary queued.', + 'Failed to queue AI attachment summary. Please try again.', + $button, + existingHtml + ); + }; + + globalThis.queueApplicationScoring = function(triggerButton = null) { + const applicationId = $('#DetailsViewApplicationId').val(); + const $button = triggerButton ? $(triggerButton) : $('[onclick*="queueApplicationScoring"]').first(); + const existingHtml = $button.html(); + const promptVersion = globalThis.getSelectedPromptVersion?.() || null; + + if (!applicationId || $button.prop('disabled')) { + return; + } + + $button + .html('Queueing...') + .prop('disabled', true); + setAiGenerationStatus('Queueing'); + + queueAIGenerationOperation( + () => unity.grantManager.grantApplications.applicationScoring.generateApplicationScoring(applicationId, promptVersion), + 'application-scoring', + 'AI application scoring queued.', + 'Failed to queue AI application scoring. Please try again.', + $button, + existingHtml + ); + }; + globalThis.refreshDevAiOutputs = loadDevAiOutputs; globalThis.generateAllAIDevOutputs = function(triggerButton = null) { @@ -446,13 +518,13 @@ $(function () { setAiGenerationStatus('Queueing'); unity.grantManager.grantApplications.grantApplication - .queueAIGeneration(applicationId, promptVersion) - .done(function(request) { - const statusText = formatAiGenerationStatus(request?.status); - setAiGenerationStatus(statusText || 'Queued'); - pollAIGenerationStatus(applicationId, promptVersion, $button, existingHtml); - abp.notify.success('AI generate all queued.'); - }) + .queueAIGeneration(applicationId, promptVersion) + .done(function(request) { + const statusText = formatAiGenerationStatus(request?.status); + setAiGenerationStatus(statusText || 'Queued'); + pollAIGenerationStatus(applicationId, 'pipeline', promptVersion, $button, existingHtml); + abp.notify.success('AI generate all queued.'); + }) .fail(function() { setAiGenerationStatus(''); abp.message.error('Failed to queue AI generate all. Please try again.'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index 254595cbdd..2c8dfbcd8c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -416,6 +416,7 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { const $button = triggerButton ? $(triggerButton) : $('#regenerateApplicationAnalysis'); const existingHtml = $button.html(); const promptVersion = globalThis.getSelectedPromptVersion?.() || null; + const aiAnalysisPollIntervalMs = 15000; if (!applicationId || $button.prop('disabled')) { return; @@ -425,16 +426,58 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { .html('Queueing...') .prop('disabled', true); + let aiAnalysisPollTimeoutId = null; + const stopAIAnalysisPolling = function() { + if (aiAnalysisPollTimeoutId) { + clearTimeout(aiAnalysisPollTimeoutId); + aiAnalysisPollTimeoutId = null; + } + }; + + const poll = function() { + unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(applicationId, 'application-analysis', promptVersion) + .done(function(request) { + const statusText = request?.status ?? 'Queued'; + updateAnalysisTabStatus(statusText); + + if (statusText === 'Failed') { + stopAIAnalysisPolling(); + loadAIAnalysis(); + $button.html(existingHtml).prop('disabled', false); + abp.message.error(request?.failureReason || 'AI analysis failed.'); + return; + } + + if (!request || request.isActive === false || statusText === 'Completed') { + stopAIAnalysisPolling(); + loadAIAnalysis(); + $button.html(existingHtml).prop('disabled', false); + if (statusText === 'Completed') { + abp.notify.success('AI analysis completed.'); + } + return; + } + + aiAnalysisPollTimeoutId = setTimeout(poll, aiAnalysisPollIntervalMs); + }) + .fail(function() { + aiAnalysisPollTimeoutId = setTimeout(poll, aiAnalysisPollIntervalMs); + }); + }; + unity.grantManager.grantApplications.applicationAnalysis .generateApplicationAnalysis(applicationId, promptVersion) .then(function() { + updateAnalysisTabStatus('Queued'); abp.notify.success('AI analysis queued. Refresh later to see updated results.'); + stopAIAnalysisPolling(); + aiAnalysisPollTimeoutId = setTimeout(poll, 500); }) .catch(function() { - abp.message.error('Failed to queue AI analysis. Please try again.'); - }) - .always(function() { + stopAIAnalysisPolling(); $button.html(existingHtml).prop('disabled', false); + abp.message.error('Failed to queue AI analysis. Please try again.'); }); } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs index 3d0e17d3e5..14f0e18a4e 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs @@ -71,11 +71,14 @@ public async Task QueueApplicationPipelineAsync_Should_Enqueue_New_Request_When_ RunApplicationAIPipelineJobArgs? capturedArgs = null; var backgroundJobManager = Substitute.For(); - backgroundJobManager.EnqueueAsync(Arg.Any()) + backgroundJobManager.EnqueueAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) .Returns(callInfo => { capturedArgs = callInfo.Arg(); - return Task.CompletedTask; + return Task.FromResult(string.Empty); }); var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); @@ -119,6 +122,8 @@ public ValueTask AcquireAsync(TimeSpan? timeo private sealed class TestDistributedSynchronizationHandle : IDistributedSynchronizationHandle { + public CancellationToken HandleLostToken => CancellationToken.None; + public void Dispose() { } From c16d63e93fa20b40d3c6ed2e5f17a3b7b146202b Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 12:42:47 -0700 Subject: [PATCH 040/134] AB#32451 cover AI queue dedupe paths --- .../Automation/AIGenerationQueueTests.cs | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs index 14f0e18a4e..a9635b5bdc 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs @@ -98,6 +98,205 @@ await repository.Received(1).InsertAsync(Arg.Is(r => r.Status == AIGenerationRequestStatus.Queued), Arg.Any(), Arg.Any()); } + [Fact] + public async Task QueueApplicationAnalysisAsync_Should_Not_Enqueue_When_An_Active_Request_Already_Exists() + { + var tenantId = Guid.NewGuid(); + var applicationId = Guid.NewGuid(); + var promptVersion = "v1"; + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, promptVersion); + var request = new AIGenerationRequest( + Guid.NewGuid(), + tenantId, + AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, + applicationId, + null, + promptVersion, + requestKey); + + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueApplicationAnalysisAsync(applicationId, tenantId, promptVersion); + + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any()); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueApplicationAnalysisAsync_Should_Enqueue_New_Request_When_None_Exists() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var promptVersion = "v1"; + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + repository.InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + + GenerateApplicationAnalysisBackgroundJobArgs? capturedArgs = null; + var backgroundJobManager = Substitute.For(); + backgroundJobManager.EnqueueAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(callInfo => + { + capturedArgs = callInfo.Arg(); + return Task.FromResult(string.Empty); + }); + + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueApplicationAnalysisAsync(applicationId, tenantId, promptVersion); + + capturedArgs.ShouldNotBeNull(); + capturedArgs!.ApplicationId.ShouldBe(applicationId); + capturedArgs.TenantId.ShouldBe(tenantId); + capturedArgs.PromptVersion.ShouldBe(promptVersion); + capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, promptVersion)); + await repository.Received(1).InsertAsync(Arg.Is(r => + r.ApplicationId == applicationId && + r.TenantId == tenantId && + r.OperationType == AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType && + r.RequestKey == capturedArgs.RequestKey && + r.Status == AIGenerationRequestStatus.Queued), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueAttachmentSummariesAsync_Should_Not_Enqueue_When_An_Active_Request_Already_Exists() + { + var tenantId = Guid.NewGuid(); + var attachmentId = Guid.NewGuid(); + var promptVersion = "v1"; + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, attachmentId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, promptVersion); + var request = new AIGenerationRequest( + Guid.NewGuid(), + tenantId, + AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, + null, + attachmentId, + promptVersion, + requestKey); + + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueAttachmentSummariesAsync(new[] { attachmentId }, tenantId, promptVersion); + + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any()); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueAttachmentSummariesAsync_Should_Enqueue_New_Request_When_None_Exists() + { + var attachmentId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var promptVersion = "v1"; + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + repository.InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + + GenerateAttachmentSummaryBackgroundJobArgs? capturedArgs = null; + var backgroundJobManager = Substitute.For(); + backgroundJobManager.EnqueueAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(callInfo => + { + capturedArgs = callInfo.Arg(); + return Task.FromResult(string.Empty); + }); + + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueAttachmentSummariesAsync(new[] { attachmentId }, tenantId, promptVersion); + + capturedArgs.ShouldNotBeNull(); + capturedArgs!.AttachmentIds.ShouldContain(attachmentId); + capturedArgs.TenantId.ShouldBe(tenantId); + capturedArgs.PromptVersion.ShouldBe(promptVersion); + capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, attachmentId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, promptVersion)); + await repository.Received(1).InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueApplicationScoringAsync_Should_Not_Enqueue_When_An_Active_Request_Already_Exists() + { + var tenantId = Guid.NewGuid(); + var applicationId = Guid.NewGuid(); + var promptVersion = "v1"; + var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationScoringOperationType, promptVersion); + var request = new AIGenerationRequest( + Guid.NewGuid(), + tenantId, + AIGenerationRequestKeyHelper.ApplicationScoringOperationType, + applicationId, + null, + promptVersion, + requestKey); + + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); + + var backgroundJobManager = Substitute.For(); + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueApplicationScoringAsync(applicationId, tenantId, promptVersion); + + await backgroundJobManager.DidNotReceive().EnqueueAsync(Arg.Any()); + await repository.DidNotReceive().InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task QueueApplicationScoringAsync_Should_Enqueue_New_Request_When_None_Exists() + { + var applicationId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + var promptVersion = "v1"; + var repository = Substitute.For>(); + repository.GetQueryableAsync().Returns(Task.FromResult>(Array.Empty().AsQueryable())); + repository.InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + + GenerateApplicationScoringBackgroundJobArgs? capturedArgs = null; + var backgroundJobManager = Substitute.For(); + backgroundJobManager.EnqueueAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(callInfo => + { + capturedArgs = callInfo.Arg(); + return Task.FromResult(string.Empty); + }); + + var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + + await queue.QueueApplicationScoringAsync(applicationId, tenantId, promptVersion); + + capturedArgs.ShouldNotBeNull(); + capturedArgs!.ApplicationId.ShouldBe(applicationId); + capturedArgs.TenantId.ShouldBe(tenantId); + capturedArgs.PromptVersion.ShouldBe(promptVersion); + capturedArgs.RequestKey.ShouldBe(AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.ApplicationScoringOperationType, promptVersion)); + await repository.Received(1).InsertAsync(Arg.Is(r => + r.ApplicationId == applicationId && + r.TenantId == tenantId && + r.OperationType == AIGenerationRequestKeyHelper.ApplicationScoringOperationType && + r.RequestKey == capturedArgs.RequestKey && + r.Status == AIGenerationRequestStatus.Queued), Arg.Any(), Arg.Any()); + } + private sealed class TestDistributedLockProvider : IDistributedLockProvider { public IDistributedLock CreateLock(string name) => new TestDistributedLock(name); From a0535dd7f7e7757d8d2290dad55ea94cdbcf1da8 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 12:59:34 -0700 Subject: [PATCH 041/134] AB#32451 fix ai queue status predicate --- .../Automation/ApplicationAIGenerationQueue.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs index 969c5ba969..9d5d24d473 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs @@ -97,11 +97,9 @@ private async Task EnsureRequestAndEnqueueAsync( using (await requestLock.AcquireAsync()) { var query = await generationRequestRepository.GetQueryableAsync(); - var existingRequests = query.Where(x => - x.RequestKey == requestKey - && (x.Status == AIGenerationRequestStatus.Queued || x.Status == AIGenerationRequestStatus.Running)); - - var existing = existingRequests + var existing = query + .Where(x => x.RequestKey == requestKey + && (x.Status == AIGenerationRequestStatus.Queued || x.Status == AIGenerationRequestStatus.Running)) .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id) .FirstOrDefault(); From f112b2f3acfd58758a0bb4e93a805a431b2fac37 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 14:11:17 -0700 Subject: [PATCH 042/134] AB#32451 move AI requests into AI schema --- .../ApplicationAnalysisAppService.cs | 12 +- .../GenerateApplicationAnalysisJob.cs | 86 ++++++++ .../GenerateApplicationScoringJob.cs | 92 +++++++++ .../RunApplicationAIPipelineJob.cs | 185 ++++++++++++++++++ .../GrantManagerDbContext.cs | 2 +- .../20260415121500_AddAIGenerationRequests.cs | 18 +- .../GrantManagerDbContextModelSnapshot.cs | 2 +- .../ApplicationAnalysisAppServiceTests.cs | 13 +- .../RunApplicationAIPipelineJobTests.cs | 175 +++++++++++++++++ 9 files changed, 566 insertions(+), 19 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs index c5d8814534..6dc533506a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/GrantApplications/ApplicationAnalysisAppService.cs @@ -2,17 +2,19 @@ using System; using System.Threading.Tasks; using Unity.AI; -using Unity.AI.Operations; +using Unity.AI.Automation; using Unity.AI.Permissions; using Volo.Abp; using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; namespace Unity.GrantManager.GrantApplications; [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] public class ApplicationAnalysisAppService( - IApplicationAnalysisService applicationAnalysisService, - IFeatureChecker featureChecker) + IApplicationAIGenerationQueue aiGenerationQueue, + IFeatureChecker featureChecker, + ICurrentTenant currentTenant) : AIAppService, IApplicationAnalysisAppService { public virtual async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) @@ -22,8 +24,8 @@ public virtual async Task GenerateApplicationAnaly throw new UserFriendlyException("AI application analysis is not enabled."); } - await applicationAnalysisService.RegenerateAndSaveAsync(applicationId, promptVersion); - return new ApplicationAnalysisResultDto { Completed = true }; + await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, currentTenant.Id, promptVersion); + return new ApplicationAnalysisResultDto { Completed = false }; } // Internal-only: no HTTP endpoint, no auth check — safe for background job callers diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs new file mode 100644 index 0000000000..3ae554b632 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.AI.Operations; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class GenerateApplicationAnalysisJob( + IApplicationAnalysisService applicationAnalysisService, + IRepository generationRequestRepository, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + await MarkRunningAsync(args.RequestKey); + try + { + logger.LogInformation("Executing AI application analysis job for application {ApplicationId}.", args.ApplicationId); + await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); + + await MarkCompletedAsync(args.RequestKey); + } + catch (Exception ex) + { + await MarkFailedAsync(args.RequestKey, ex.Message); + throw; + } + } + } + + private async Task MarkRunningAsync(string requestKey) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkRunning(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkCompletedAsync(string requestKey) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkCompleted(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkFailedAsync(string requestKey, string? failureReason) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkFailed(DateTime.UtcNow, failureReason); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task GetRequestAsync(string requestKey) + { + var query = await generationRequestRepository.GetQueryableAsync(); + return query + .Where(x => x.RequestKey == requestKey) + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs new file mode 100644 index 0000000000..4f4722a35b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.AI.Operations; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.GrantApplications.Automation.Events; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus.Local; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class GenerateApplicationScoringJob( + IApplicationScoringService applicationScoringService, + ILocalEventBus localEventBus, + IRepository generationRequestRepository, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + await MarkRunningAsync(args.RequestKey); + try + { + logger.LogInformation("Executing AI application scoring job for application {ApplicationId}.", args.ApplicationId); + await applicationScoringService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent + { + ApplicationId = args.ApplicationId + }); + + await MarkCompletedAsync(args.RequestKey); + } + catch (Exception ex) + { + await MarkFailedAsync(args.RequestKey, ex.Message); + throw; + } + } + } + + private async Task MarkRunningAsync(string requestKey) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkRunning(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkCompletedAsync(string requestKey) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkCompleted(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkFailedAsync(string requestKey, string? failureReason) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkFailed(DateTime.UtcNow, failureReason); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task GetRequestAsync(string requestKey) + { + var query = await generationRequestRepository.GetQueryableAsync(); + return query + .Where(x => x.RequestKey == requestKey) + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs new file mode 100644 index 0000000000..df8a45ce39 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -0,0 +1,185 @@ +using Microsoft.Extensions.Logging; +using Medallion.Threading; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.AI; +using Unity.AI.Operations; +using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.GrantApplications.Automation.Events; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus.Local; +using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class RunApplicationAIPipelineJob( + IAIService aiService, + IAttachmentSummaryService attachmentSummaryService, + IApplicationAnalysisService applicationAnalysisService, + IApplicationScoringService applicationScoringService, + IFeatureChecker featureChecker, + ILocalEventBus localEventBus, + ICurrentTenant currentTenant, + IApplicationRepository applicationRepository, + IApplicationFormRepository applicationFormRepository, + IRepository generationRequestRepository, + IDistributedLockProvider distributedLockProvider, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + var requestKey = string.IsNullOrWhiteSpace(args.RequestKey) + ? ApplicationAIGenerationQueue.BuildPipelineRequestKey(args.TenantId, args.ApplicationId, args.PromptVersion) + : args.RequestKey; + var executionLock = distributedLockProvider.CreateLock($"ai-generation-run:{requestKey}"); + + using (await executionLock.AcquireAsync()) + { + try + { + var request = await GetRequestAsync(requestKey, args.ApplicationId); + + if (request != null && request.Status == AIGenerationRequestStatus.Completed) + { + logger.LogDebug("AI generation request {RequestKey} is already completed for application {ApplicationId}.", requestKey, args.ApplicationId); + return; + } + + if (request != null) + { + request.MarkRunning(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + var application = await applicationRepository.GetAsync(args.ApplicationId); + var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); + + if (!applicationForm.AutomaticallyGenerateAIAnalysis) + { + logger.LogDebug("Automatic AI analysis is disabled at form level for application {ApplicationId}, skipping intake pipeline.", args.ApplicationId); + await MarkCompletedAsync(requestKey, args.ApplicationId); + return; + } + + var attachmentSummariesEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); + var applicationAnalysisEnabled = await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"); + var scoringEnabled = await featureChecker.IsEnabledAsync("Unity.AI.Scoring"); + if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled) + { + logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); + await MarkCompletedAsync(requestKey, args.ApplicationId); + return; + } + + if (!await aiService.IsAvailableAsync()) + { + logger.LogWarning("AI service is not available, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); + await MarkFailedAsync(requestKey, args.ApplicationId, "AI service is not available."); + return; + } + + logger.LogInformation("Executing queued AI content pipeline for application {ApplicationId}.", args.ApplicationId); + if (attachmentSummariesEnabled) + { + await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion); + } + + Exception? analysisException = null; + Exception? scoringException = null; + if (applicationAnalysisEnabled) + { + try + { + await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + } + catch (Exception ex) + { + analysisException = ex; + logger.LogError(ex, "Error executing AI application analysis stage for application {ApplicationId}.", args.ApplicationId); + } + } + + if (scoringEnabled) + { + try + { + var result = await applicationScoringService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + if (!string.Equals(result, "{}", StringComparison.Ordinal)) + { + await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent + { + ApplicationId = args.ApplicationId + }); + } + } + catch (Exception ex) + { + scoringException = ex; + logger.LogError(ex, "Error executing AI application scoring stage for application {ApplicationId}.", args.ApplicationId); + } + } + + if (scoringException != null) + { + await MarkFailedAsync(requestKey, args.ApplicationId, scoringException.Message); + throw scoringException; + } + + if (analysisException != null) + { + await MarkFailedAsync(requestKey, args.ApplicationId, analysisException.Message); + throw analysisException; + } + + await MarkCompletedAsync(requestKey, args.ApplicationId); + } + catch (Exception ex) + { + await MarkFailedAsync(requestKey, args.ApplicationId, ex.Message); + throw; + } + } + } + } + + private async Task GetRequestAsync(string requestKey, Guid applicationId) + { + var query = await generationRequestRepository.GetQueryableAsync(); + return query + .Where(x => x.RequestKey == requestKey && x.ApplicationId == applicationId) + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } + + private async Task MarkCompletedAsync(string requestKey, Guid applicationId) + { + var request = await GetRequestAsync(requestKey, applicationId); + if (request == null) + { + return; + } + + request.MarkCompleted(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkFailedAsync(string requestKey, Guid applicationId, string? failureReason) + { + var request = await GetRequestAsync(requestKey, applicationId); + if (request == null) + { + return; + } + + request.MarkFailed(DateTime.UtcNow, failureReason); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs index 2cdb96eeba..a4df989e11 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -240,7 +240,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(b => { - b.ToTable(GrantManagerConsts.DbTablePrefix + "AIGenerationRequests", GrantManagerConsts.DbSchema); + b.ToTable(GrantManagerConsts.DbTablePrefix + "AIRequests", AIDbProperties.DbSchema); b.ConfigureByConvention(); b.Property(x => x.OperationType).IsRequired().HasMaxLength(64); b.Property(x => x.RequestKey).IsRequired().HasMaxLength(256); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.cs index f90a5d4523..1a118a5a47 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.cs @@ -10,7 +10,8 @@ public partial class AddAIGenerationRequests : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "AIGenerationRequests", + name: "AIRequests", + schema: "AI", columns: table => new { Id = table.Column(type: "uuid", nullable: false), @@ -36,23 +37,26 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_AIGenerationRequests", x => x.Id); + table.PrimaryKey("PK_AIRequests", x => x.Id); }); migrationBuilder.CreateIndex( - name: "IX_AIGenerationRequests_ApplicationId_OperationType_Status", - table: "AIGenerationRequests", + name: "IX_AIRequests_ApplicationId_OperationType_Status", + schema: "AI", + table: "AIRequests", columns: new[] { "ApplicationId", "OperationType", "Status" }); migrationBuilder.CreateIndex( - name: "IX_AIGenerationRequests_RequestKey", - table: "AIGenerationRequests", + name: "IX_AIRequests_RequestKey", + schema: "AI", + table: "AIRequests", column: "RequestKey"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "AIGenerationRequests"); + name: "AIRequests", + schema: "AI"); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index 495e324107..5dc0832bb0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -1362,7 +1362,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("RequestKey"); - b.ToTable("AIGenerationRequests", (string)null); + b.ToTable("AIRequests", "AI"); }); modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationAnalysisAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationAnalysisAppServiceTests.cs index 46c07f08ef..de59448fdf 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationAnalysisAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/ApplicationAnalysisAppServiceTests.cs @@ -2,9 +2,10 @@ using Shouldly; using System; using System.Threading.Tasks; -using Unity.AI.Operations; +using Unity.AI.Automation; using Unity.GrantManager.GrantApplications; using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; using Xunit; using Xunit.Abstractions; @@ -18,14 +19,16 @@ public async Task GenerateApplicationAnalysisAsync_Should_Return_Completed_Resul var featureChecker = Substitute.For(); featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis").Returns(true); - var analysisService = Substitute.For(); - analysisService.RegenerateAndSaveAsync(Arg.Any(), Arg.Any()).Returns("analysis"); + var queue = Substitute.For(); + var currentTenant = Substitute.For(); + currentTenant.Id.Returns(Guid.NewGuid()); - var service = new ApplicationAnalysisAppService(analysisService, featureChecker); + var service = new ApplicationAnalysisAppService(queue, featureChecker, currentTenant); var result = await service.GenerateApplicationAnalysisAsync(Guid.NewGuid()); result.ShouldNotBeNull(); - result.Completed.ShouldBeTrue(); + result.Completed.ShouldBeFalse(); + await queue.Received(1).QueueApplicationAnalysisAsync(Arg.Any(), currentTenant.Id, Arg.Any()); } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs new file mode 100644 index 0000000000..ff8618254c --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs @@ -0,0 +1,175 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Medallion.Threading; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Unity.AI; +using Unity.AI.Operations; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Attachments; +using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.DistributedLocking; +using Volo.Abp.EventBus.Local; +using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; +using Xunit; +using Xunit.Abstractions; + +namespace Unity.GrantManager.GrantApplications.Automation; + +public class RunApplicationAIPipelineJobTests(ITestOutputHelper outputHelper) : GrantManagerApplicationTestBase(outputHelper) +{ + [Fact] + public async Task ExecuteAsync_Should_Mark_Request_Completed_When_Features_Disabled() + { + var featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync(Arg.Any()).Returns(false); + + var repository = BuildRequestRepository(out var requests); + var applicationId = Guid.NewGuid(); + requests.Add(CreateRequest(applicationId)); + + var job = BuildJob(featureChecker, repository); + + await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs + { + ApplicationId = applicationId, + RequestKey = requests[0].RequestKey + }); + + Assert.Equal(AIGenerationRequestStatus.Completed, requests[0].Status); + } + + [Fact] + public async Task ExecuteAsync_Should_Not_Run_When_Request_Already_Completed() + { + var featureChecker = Substitute.For(); + featureChecker.IsEnabledAsync(Arg.Any()).Returns(true); + + var repository = BuildRequestRepository(out var requests); + var applicationId = Guid.NewGuid(); + var request = CreateRequest(applicationId); + request.MarkCompleted(DateTime.UtcNow); + requests.Add(request); + + var scoringService = Substitute.For(); + + var job = BuildJob(featureChecker, repository, scoringService: scoringService); + + await job.ExecuteAsync(new RunApplicationAIPipelineJobArgs + { + ApplicationId = applicationId, + RequestKey = request.RequestKey + }); + + await scoringService.DidNotReceive().RegenerateAndSaveAsync(Arg.Any(), Arg.Any()); + } + + private static RunApplicationAIPipelineJob BuildJob( + IFeatureChecker featureChecker, + IRepository generationRequestRepository, + IAttachmentSummaryService? attachmentSummaryService = null, + IApplicationAnalysisService? applicationAnalysisService = null, + IApplicationScoringService? scoringService = null, + IDistributedLockProvider? distributedLockProvider = null) + { + var application = (Application)RuntimeHelpers.GetUninitializedObject(typeof(Application)); + application.ApplicationFormId = Guid.NewGuid(); + + var applicationForm = (ApplicationForm)RuntimeHelpers.GetUninitializedObject(typeof(ApplicationForm)); + applicationForm.AutomaticallyGenerateAIAnalysis = true; + + var applicationRepository = Substitute.For(); + applicationRepository.GetAsync(Arg.Any(), Arg.Any()).Returns(application); + + var applicationFormRepository = Substitute.For(); + applicationFormRepository.GetAsync(Arg.Any(), Arg.Any()).Returns(applicationForm); + + return new RunApplicationAIPipelineJob( + Substitute.For(), + attachmentSummaryService ?? Substitute.For(), + applicationAnalysisService ?? Substitute.For(), + scoringService ?? Substitute.For(), + featureChecker, + Substitute.For(), + Substitute.For(), + applicationRepository, + applicationFormRepository, + generationRequestRepository, + distributedLockProvider ?? new TestDistributedLockProvider(), + NullLogger.Instance); + } + + private static IRepository BuildRequestRepository(out List requests) + { + var requestList = new List(); + requests = requestList; + var repository = Substitute.For>(); + + repository.GetQueryableAsync().Returns(Task.FromResult>(requestList.AsQueryable())); + repository.InsertAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => + { + var request = callInfo.Arg(); + requestList.Add(request); + return Task.FromResult(request); + }); + repository.UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(callInfo => Task.FromResult(callInfo.Arg())); + + return repository; + } + + private static AIGenerationRequest CreateRequest(Guid applicationId) + { + return new AIGenerationRequest( + Guid.NewGuid(), + Guid.NewGuid(), + AIGenerationRequestKeyHelper.PipelineOperationType, + applicationId, + null, + null, + $"tenant:{Guid.NewGuid():D}:application:{applicationId:D}:none:pipeline:default"); + } + + private sealed class TestDistributedLockProvider : IDistributedLockProvider + { + public IDistributedLock CreateLock(string name) => new TestDistributedLock(name); + } + + private sealed class TestDistributedLock(string name) : IDistributedLock + { + public string Name => name; + + public IDistributedSynchronizationHandle Acquire(TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default) => + new TestDistributedSynchronizationHandle(); + + public ValueTask AcquireAsync(TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default) => + ValueTask.FromResult(new TestDistributedSynchronizationHandle()); + + public IDistributedSynchronizationHandle? TryAcquire(TimeSpan timeout = default, System.Threading.CancellationToken cancellationToken = default) => + new TestDistributedSynchronizationHandle(); + + public ValueTask TryAcquireAsync(TimeSpan timeout = default, System.Threading.CancellationToken cancellationToken = default) => + ValueTask.FromResult(new TestDistributedSynchronizationHandle()); + } + + private sealed class TestDistributedSynchronizationHandle : IDistributedSynchronizationHandle + { + public System.Threading.CancellationToken HandleLostToken => System.Threading.CancellationToken.None; + + public void Dispose() + { + } + + public ValueTask DisposeAsync() + { + Dispose(); + return ValueTask.CompletedTask; + } + } +} From a30df8f3e6ce37088c05ec781a8ac172e3a7ee18 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 15:32:05 -0700 Subject: [PATCH 043/134] AB#32451 scaffold AI request migration --- ...121500_AddAIGenerationRequests.Designer.cs | 619 ++++++++++++++++++ .../GrantManagerDbContextModelSnapshot.cs | 214 +++--- 2 files changed, 725 insertions(+), 108 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.Designer.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.Designer.cs new file mode 100644 index 0000000000..6cc12bf009 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIGenerationRequests.Designer.cs @@ -0,0 +1,619 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Unity.GrantManager.Migrations.HostMigrations +{ + [DbContext(typeof(GrantManagerDbContext))] + [Migration("20260415121500_AddAIGenerationRequests")] + partial class AddAIGenerationRequests + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.PostgreSql) + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", (string)null); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", (string)null); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPrompt", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp1") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.HasKey("Id"); + + b.ToTable("AIPrompts", "AI"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPromptVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("Temperature") + .HasColumnType("double precision"); + + b.Property("PromptId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.Property("TargetModel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TargetProvider") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("VersionNumber") + .HasColumnType("integer"); + + b.Property("SystemPrompt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserPromptTemplate") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeveloperNotes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "VersionNumber") + .IsUnique(); + + b.ToTable("AIPromptVersions", "AI"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPromptVersion", b => + { + b.HasOne("Unity.AI.Domain.AIPrompt", "Prompt") + .WithMany("Versions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("Unity.AI.Domain.AIPrompt", b => + { + b.Navigation("Versions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index 5dc0832bb0..aac8eb62e4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -658,6 +658,101 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApplicantTenantMaps", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.GrantApplications.AIGenerationRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AttachmentId") + .HasColumnType("uuid"); + + b.Property("CompletedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uuid") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uuid") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("DeletionTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ExtraProperties"); + + b.Property("FailureReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uuid") + .HasColumnName("LastModifierId"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PromptVersion") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RequestKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("StartedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RequestKey"); + + b.HasIndex("ApplicationId", "OperationType", "Status"); + + b.ToTable("AIRequests", "AI"); + }); + modelBuilder.Entity("Unity.GrantManager.Integrations.CasClientCode", b => { b.Property("Id") @@ -1177,10 +1272,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("InboxMessages", (string)null); }); - modelBuilder.Entity("Unity.GrantManager.Messaging.OutboxMessage", b => - { - b.Property("Id") - .HasColumnType("uuid"); + modelBuilder.Entity("Unity.GrantManager.Messaging.OutboxMessage", b => + { + b.Property("Id") + .HasColumnType("uuid"); b.Property("AckStatus") .IsRequired() @@ -1265,110 +1360,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Source", "Status"); - b.ToTable("OutboxMessages", (string)null); - }); - - modelBuilder.Entity("Unity.GrantManager.GrantApplications.AIGenerationRequest", b => - { - b.Property("Id") - .HasColumnType("uuid"); - - b.Property("ApplicationId") - .HasColumnType("uuid") - .HasColumnName("ApplicationId"); - - b.Property("AttachmentId") - .HasColumnType("uuid") - .HasColumnName("AttachmentId"); - - b.Property("CreationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uuid") - .HasColumnName("CreatorId"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CompletedAt") - .HasColumnType("timestamp without time zone"); - - b.Property("DeleterId") - .HasColumnType("uuid") - .HasColumnName("DeleterId"); - - b.Property("DeletionTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("DeletionTime"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("text") - .HasColumnName("ExtraProperties"); - - b.Property("FailureReason") - .HasMaxLength(2000) - .HasColumnType("character varying(2000)"); - - b.Property("IsDeleted") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("IsDeleted"); - - b.Property("LastModificationTime") - .HasColumnType("timestamp without time zone") - .HasColumnName("LastModificationTime"); - - b.Property("LastModifierId") - .HasColumnType("uuid") - .HasColumnName("LastModifierId"); - - b.Property("OperationType") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("PromptVersion") - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("RequestKey") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("StartedAt") - .HasColumnType("timestamp without time zone"); - - b.Property("Status") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("character varying(20)"); - - b.Property("TenantId") - .HasColumnType("uuid") - .HasColumnName("TenantId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId", "OperationType", "Status"); - - b.HasIndex("RequestKey"); - - b.ToTable("AIRequests", "AI"); - }); - - modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => - { - b.Property("Id") - .HasColumnType("uuid"); + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Unity.GrantManager.Tokens.TenantToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); b.Property("CreationTime") .HasColumnType("timestamp without time zone") From 798cfc92186aae9bdeda738a78199fddabe9cac8 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 16:59:05 -0700 Subject: [PATCH 044/134] AB#32451 consolidate AI queue entrypoints --- .../IApplicationAIGenerationQueue.cs | 6 + .../IGrantApplicationAppService.cs | 4 + .../ApplicationAIGenerationQueue.cs | 13 +- .../GenerateApplicationAnalysisJob.cs | 5 +- .../GenerateApplicationScoringJob.cs | 5 +- .../GenerateAttachmentSummaryJob.cs | 85 ++++++++++++ .../RunApplicationAIPipelineJob.cs | 8 +- .../GrantApplicationAppService.cs | 108 ++++++++++++++- .../Pages/GrantApplications/Details.js | 33 ++--- .../Pages/GrantApplications/ai-analysis.js | 43 ++++-- .../AssessmentScoresWidget/Default.js | 6 +- .../ChefsAttachments/ChefsAttachments.js | 49 +++---- .../Components/ReviewList/ReviewList.js | 128 +++++++++++++----- 13 files changed, 372 insertions(+), 121 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs index 3db64559f0..b8f059a463 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs @@ -5,5 +5,11 @@ namespace Unity.AI.Automation; public interface IApplicationAIGenerationQueue { +<<<<<<< HEAD +======= + Task QueueAttachmentSummariesAsync(Guid applicationId, IReadOnlyList attachmentIds, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); +>>>>>>> 64123200c (AB#32451 consolidate AI queue entrypoints) Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs index 2d44ac5f64..c4fbe82eb0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs @@ -19,6 +19,10 @@ public interface IGrantApplicationAppService Task GetAsync(Guid id); Task TriggerAction(Guid applicationId, GrantApplicationAction triggerAction); Task QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null); + Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null); + Task QueueAttachmentSummariesAsync(Guid applicationId, List attachmentIds, string? promptVersion = null); + Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null); Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null); Task QueueAIPipelineAsync(Guid applicationId, string? promptVersion = null); Task GetAccountCodingIdFromFormIdAsync(Guid formId); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs index 9d5d24d473..50d6a32be7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs @@ -1,14 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using Medallion.Threading; using Unity.AI.Automation; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; +using Medallion.Threading; +using Volo.Abp.Domain.Repositories; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; -using Volo.Abp.DistributedLocking; -using Volo.Abp.Domain.Repositories; namespace Unity.GrantManager.GrantApplications.Automation; @@ -97,9 +96,11 @@ private async Task EnsureRequestAndEnqueueAsync( using (await requestLock.AcquireAsync()) { var query = await generationRequestRepository.GetQueryableAsync(); - var existing = query - .Where(x => x.RequestKey == requestKey - && (x.Status == AIGenerationRequestStatus.Queued || x.Status == AIGenerationRequestStatus.Running)) + var existingRequests = query.Where(x => + x.RequestKey == requestKey + && (x.Status == AIGenerationRequestStatus.Queued || x.Status == AIGenerationRequestStatus.Running)); + + var existing = existingRequests .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id) .FirstOrDefault(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs index 3ae554b632..4daaef6fa3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -76,9 +76,8 @@ private async Task MarkFailedAsync(string requestKey, string? failureReason) private async Task GetRequestAsync(string requestKey) { - var query = await generationRequestRepository.GetQueryableAsync(); - return query - .Where(x => x.RequestKey == requestKey) + var requests = await generationRequestRepository.GetListAsync(x => x.RequestKey == requestKey); + return requests .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id) .FirstOrDefault(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs index 4f4722a35b..ed168268c2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -82,9 +82,8 @@ private async Task MarkFailedAsync(string requestKey, string? failureReason) private async Task GetRequestAsync(string requestKey) { - var query = await generationRequestRepository.GetQueryableAsync(); - return query - .Where(x => x.RequestKey == requestKey) + var requests = await generationRequestRepository.GetListAsync(x => x.RequestKey == requestKey); + return requests .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id) .FirstOrDefault(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs new file mode 100644 index 0000000000..51adae95ba --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.AI.Operations; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public class GenerateAttachmentSummaryJob( + IAttachmentSummaryService attachmentSummaryService, + IRepository generationRequestRepository, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + await MarkRunningAsync(args.RequestKey); + try + { + logger.LogInformation( + "Executing AI attachment summary job for {AttachmentCount} attachment(s).", + args.AttachmentIds.Count); + await attachmentSummaryService.GenerateAndSaveAsync(args.AttachmentIds, args.PromptVersion); + await MarkCompletedAsync(args.RequestKey); + } + catch (Exception ex) + { + await MarkFailedAsync(args.RequestKey, ex.Message); + throw; + } + } + } + + private async Task MarkRunningAsync(string requestKey) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkRunning(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkCompletedAsync(string requestKey) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkCompleted(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task MarkFailedAsync(string requestKey, string? failureReason) + { + var request = await GetRequestAsync(requestKey); + if (request == null) + { + return; + } + + request.MarkFailed(DateTime.UtcNow, failureReason); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + private async Task GetRequestAsync(string requestKey) + { + var requests = await generationRequestRepository.GetListAsync(x => x.RequestKey == requestKey); + return requests + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs index df8a45ce39..cea059bf2d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -151,9 +151,11 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent private async Task GetRequestAsync(string requestKey, Guid applicationId) { - var query = await generationRequestRepository.GetQueryableAsync(); - return query - .Where(x => x.RequestKey == requestKey && x.ApplicationId == applicationId) + var requests = await generationRequestRepository.GetListAsync(x => + x.RequestKey == requestKey && + x.ApplicationId == applicationId); + + return requests .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id) .FirstOrDefault(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 1f2ae9ea88..9d3a3ba4fc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -14,15 +14,15 @@ using System.Threading.Tasks; using Unity.Flex.WorksheetInstances; using Unity.Flex.Worksheets; -using Unity.AI.Models; -using Unity.AI.Responses; -using Unity.GrantManager.Applicants; +using Unity.AI.Models; +using Unity.AI.Responses; +using Unity.GrantManager.Applicants; using Unity.GrantManager.ApplicationForms; using Unity.GrantManager.Applications; using Unity.GrantManager.Events; using Unity.GrantManager.Flex; using Unity.GrantManager.Identity; -using Unity.GrantManager.Payments; +using Unity.GrantManager.Payments; using Unity.Modules.Shared; using Unity.Modules.Shared.Correlation; using Unity.Payments.PaymentRequests; @@ -32,7 +32,8 @@ using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; -using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Features; namespace Unity.GrantManager.GrantApplications; @@ -46,12 +47,14 @@ public class GrantApplicationAppService( IApplicationFormSubmissionRepository applicationFormSubmissionRepository, IApplicantRepository applicantRepository, IApplicationFormRepository applicationFormRepository, + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IApplicantAgentRepository applicantAgentRepository, IApplicantAddressRepository applicantAddressRepository, IApplicantSupplierAppService applicantSupplierService, IPaymentRequestAppService paymentRequestService, IApplicationAIGenerationQueue aiGenerationQueue, - IAIGenerationStatusAppService aiGenerationStatusAppService) + IAIGenerationStatusAppService aiGenerationStatusAppService, + IFeatureChecker featureChecker) : GrantManagerAppService, IGrantApplicationAppService { private static readonly JsonSerializerOptions AiAnalysisReadOptions = new() @@ -1035,7 +1038,71 @@ await LocalEventBus.PublishAsync( public async Task QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null) { - return await QueueAIPipelineAsync(applicationId, promptVersion); + await EnsureAIAnalysisEnabledAsync(); + return await QueueApplicationAnalysisAsync(applicationId, promptVersion); + } + + public async Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureAIAnalysisEnabledAsync(); + await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, + promptVersion); + + return request ?? throw new UserFriendlyException("Unable to queue AI analysis request."); + } + + public async Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureAttachmentSummariesEnabledAsync(); + var attachmentIds = await applicationChefsFileAttachmentRepository.GetListAsync(applicationId); + if (attachmentIds.Count == 0) + { + throw new UserFriendlyException("No attachments are available to summarize for this application."); + } + + await aiGenerationQueue.QueueAttachmentSummariesAsync(applicationId, attachmentIds.Select(a => a.Id).ToList(), CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, + promptVersion); + + return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request."); + } + + public async Task QueueAttachmentSummariesAsync(Guid applicationId, List attachmentIds, string? promptVersion = null) + { + await EnsureAttachmentSummariesEnabledAsync(); + if (attachmentIds.Count == 0) + { + throw new UserFriendlyException("No attachments are available to summarize for this application."); + } + + await aiGenerationQueue.QueueAttachmentSummariesAsync(applicationId, attachmentIds, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, + promptVersion); + + return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request."); + } + + public async Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null) + { + await EnsureScoringEnabledAsync(); + await aiGenerationQueue.QueueApplicationScoringAsync(applicationId, CurrentTenant.Id, promptVersion); + + var request = await aiGenerationStatusAppService.GetLatestAsync( + applicationId, + AIGenerationRequestKeyHelper.ApplicationScoringOperationType, + promptVersion); + + return request ?? throw new UserFriendlyException("Unable to queue AI scoring request."); } public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null) @@ -1045,6 +1112,9 @@ public async Task QueueAIGenerationAsync(Guid applicatio public async Task QueueAIPipelineAsync(Guid applicationId, string? promptVersion = null) { + await EnsureAttachmentSummariesEnabledAsync(); + await EnsureAIAnalysisEnabledAsync(); + await EnsureScoringEnabledAsync(); await aiGenerationQueue.QueueApplicationPipelineAsync(applicationId, CurrentTenant.Id, promptVersion); var request = await aiGenerationStatusAppService.GetLatestAsync( @@ -1054,6 +1124,30 @@ public async Task QueueAIPipelineAsync(Guid applicationI return request ?? throw new UserFriendlyException("Unable to queue AI generation request."); } + + private async Task EnsureAttachmentSummariesEnabledAsync() + { + if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries")) + { + throw new UserFriendlyException("AI attachment summaries are not enabled."); + } + } + + private async Task EnsureAIAnalysisEnabledAsync() + { + if (!await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis")) + { + throw new UserFriendlyException("AI application analysis is not enabled."); + } + } + + private async Task EnsureScoringEnabledAsync() + { + if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) + { + throw new UserFriendlyException("AI scoring is not enabled."); + } + } #endregion APPLICATION WORKFLOW public async Task> GetAllApplicationsAsync() diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index 216f16bdfe..314c82fb52 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -400,23 +400,20 @@ $(function () { stopAIGenerationPolling(); setAiGenerationStatus('Failed'); loadDevAiOutputs(); - restoreButton.html(originalHtml).prop('disabled', false); + restoreButton.html('Completed').prop('disabled', true); abp.message.error(request?.failureReason || 'AI generate all failed.'); return; } - if (!request || request.isActive === false || statusText === 'Completed') { - stopAIGenerationPolling(); - setAiGenerationStatus(''); - setDevAiOutputTimestamp('#analysisAiOutputTimestamp', request?.completedAt || request?.startedAt || null); - setDevAiOutputTimestamp('#scoringAiOutputTimestamp', request?.completedAt || request?.startedAt || null); - loadDevAiOutputs(); - restoreButton.html(originalHtml).prop('disabled', false); - if (statusText === 'Completed') { - abp.notify.success('AI generate all completed.'); - } - return; - } + if (!request || request.isActive === false || statusText === 'Completed') { + stopAIGenerationPolling(); + setAiGenerationStatus(''); + setDevAiOutputTimestamp('#analysisAiOutputTimestamp', request?.completedAt || request?.startedAt || null); + setDevAiOutputTimestamp('#scoringAiOutputTimestamp', request?.completedAt || request?.startedAt || null); + loadDevAiOutputs(); + restoreButton.html('Completed').prop('disabled', true); + return; + } aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); }) @@ -441,12 +438,11 @@ $(function () { restoreButton, originalHtml ); - abp.notify.success(queuedMessage); }) .fail(function() { setAiGenerationStatus(''); abp.message.error(failureMessage); - restoreButton.html(originalHtml).prop('disabled', false); + restoreButton.html('Completed').prop('disabled', true); }); } @@ -466,7 +462,7 @@ $(function () { setAiGenerationStatus('Queueing'); queueAIGenerationOperation( - () => unity.grantManager.attachments.attachmentSummary.generateAttachmentSummary(applicationId, promptVersion), + () => unity.grantManager.grantApplications.grantApplication.queueAttachmentSummary(applicationId, promptVersion), 'attachment-summary', 'AI attachment summary queued.', 'Failed to queue AI attachment summary. Please try again.', @@ -491,7 +487,7 @@ $(function () { setAiGenerationStatus('Queueing'); queueAIGenerationOperation( - () => unity.grantManager.grantApplications.applicationScoring.generateApplicationScoring(applicationId, promptVersion), + () => unity.grantManager.grantApplications.grantApplication.queueApplicationScoring(applicationId, promptVersion), 'application-scoring', 'AI application scoring queued.', 'Failed to queue AI application scoring. Please try again.', @@ -518,12 +514,11 @@ $(function () { setAiGenerationStatus('Queueing'); unity.grantManager.grantApplications.grantApplication - .queueAIGeneration(applicationId, promptVersion) + .queueAIPipeline(applicationId, promptVersion) .done(function(request) { const statusText = formatAiGenerationStatus(request?.status); setAiGenerationStatus(statusText || 'Queued'); pollAIGenerationStatus(applicationId, 'pipeline', promptVersion, $button, existingHtml); - abp.notify.success('AI generate all queued.'); }) .fail(function() { setAiGenerationStatus(''); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index 2c8dfbcd8c..418c3d3ed0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -417,6 +417,8 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { const existingHtml = $button.html(); const promptVersion = globalThis.getSelectedPromptVersion?.() || null; const aiAnalysisPollIntervalMs = 15000; + const aiAnalysisMaxPollFailures = 3; + const aiAnalysisMaxQueueWaitMs = 120000; if (!applicationId || $button.prop('disabled')) { return; @@ -427,6 +429,8 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { .prop('disabled', true); let aiAnalysisPollTimeoutId = null; + let aiAnalysisPollFailures = 0; + let aiAnalysisQueuedAt = Date.now(); const stopAIAnalysisPolling = function() { if (aiAnalysisPollTimeoutId) { clearTimeout(aiAnalysisPollTimeoutId); @@ -438,8 +442,8 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { unity.grantManager.grantApplications.grantApplication .getAIGenerationStatus(applicationId, 'application-analysis', promptVersion) .done(function(request) { + aiAnalysisPollFailures = 0; const statusText = request?.status ?? 'Queued'; - updateAnalysisTabStatus(statusText); if (statusText === 'Failed') { stopAIAnalysisPolling(); @@ -449,32 +453,47 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { return; } + if (Date.now() - aiAnalysisQueuedAt > aiAnalysisMaxQueueWaitMs) { + stopAIAnalysisPolling(); + $button.html(existingHtml).prop('disabled', false); + abp.message.error('AI analysis is still queued. Please try again later.'); + return; + } + if (!request || request.isActive === false || statusText === 'Completed') { stopAIAnalysisPolling(); loadAIAnalysis(); - $button.html(existingHtml).prop('disabled', false); - if (statusText === 'Completed') { - abp.notify.success('AI analysis completed.'); - } + $button.html('Completed').prop('disabled', true); return; } aiAnalysisPollTimeoutId = setTimeout(poll, aiAnalysisPollIntervalMs); }) - .fail(function() { + .fail(function(error) { + console.warn('Failed to poll AI analysis status.', error); + aiAnalysisPollFailures += 1; + + if (aiAnalysisPollFailures > aiAnalysisMaxPollFailures) { + stopAIAnalysisPolling(); + $button.html(existingHtml).prop('disabled', false); + abp.message.error('Unable to load AI analysis status. Please try again.'); + return; + } + aiAnalysisPollTimeoutId = setTimeout(poll, aiAnalysisPollIntervalMs); }); }; - unity.grantManager.grantApplications.applicationAnalysis - .generateApplicationAnalysis(applicationId, promptVersion) - .then(function() { - updateAnalysisTabStatus('Queued'); - abp.notify.success('AI analysis queued. Refresh later to see updated results.'); + unity.grantManager.grantApplications.grantApplication + .queueApplicationAnalysis(applicationId, promptVersion) + .done(function(request) { + aiAnalysisPollFailures = 0; + setAiGenerationStatus(formatAiGenerationStatus(request?.status) || 'Queued'); stopAIAnalysisPolling(); aiAnalysisPollTimeoutId = setTimeout(poll, 500); }) - .catch(function() { + .fail(function(error) { + console.error('Failed to queue AI analysis.', error); stopAIAnalysisPolling(); $button.html(existingHtml).prop('disabled', false); abp.message.error('Failed to queue AI analysis. Please try again.'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index fb6bcdc61c..79bcdced25 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -597,16 +597,14 @@ function queueApplicationScoring(triggerButton = null) { .prop('disabled', true); unity.grantManager.grantApplications.applicationScoring - .generateApplicationScoring(applicationId, promptVersion) + .queueApplicationScoring(applicationId, promptVersion) .done(function () { - abp.notify.success('AI scoring queued. Refresh later to see updated results.'); + $button.html('Completed').prop('disabled', true); }) .fail(function () { abp.message.error( 'Failed to queue AI scoring. Please try again.' ); - }) - .always(function () { $button.html(existingHtml).prop('disabled', false); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 5466a20147..7d80f14484 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -187,41 +187,24 @@ $(function () { const existingHTML = $activeButton.html(); - // Call the backend API - $.ajax({ - url: - '/api/app/attachment-summary/generate-attachment-summaries' + - '?promptVersion=' + - encodeURIComponent(promptVersion || ''), - data: JSON.stringify(attachmentIds), - contentType: 'application/json', - type: 'POST', - beforeSend: function () { - $activeButton - .html( - 'Queueing...' - ) - .prop('disabled', true); - }, - success: function (summaries) { - abp.notify.success( - 'AI summaries queued for ' + - summaries.length + - ' attachment(s). Refresh later to see updated results.' - ); - + $activeButton + .html( + 'Queueing...' + ) + .prop('disabled', true); + + unity.grantManager.grantApplications.grantApplication + .queueAttachmentSummaries(applicationId, attachmentIds, promptVersion) + .done(function () { + $activeButton.html('Completed').prop('disabled', true); resetAttachmentSelectionState(); - + }) + .fail(function (error) { + console.error('Error queueing AI summaries:', error); + abp.message.error('An error occurred while queueing AI summaries. Please try again.'); $activeButton.html(existingHTML).prop('disabled', false); - }, - error: function (error) { - console.error('Error generating AI summaries:', error); - abp.notify.error( - 'An error occurred while queueing AI summaries. Please try again.' - ); - $activeButton.html(existingHTML).prop('disabled', false); - }, - }); + }) + ; }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 7727bbedeb..0f378e12d1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -1,6 +1,6 @@ const l = abp.localization.getResource('GrantManager'); const pageApplicationId = decodeURIComponent(document.querySelector("#DetailsViewApplicationId").value); -const isAiScoringEnabled = document.querySelector("#ReviewListAIScoringEnabled")?.value === 'True'; +const isAiScoringEnabled = document.querySelector("#ReviewListAIScoringEnabled")?.value === 'True'; const canUseAiScoring = isAiScoringEnabled; const actionButtonConfigMap = { @@ -28,7 +28,7 @@ const finalApplicationStates = [ ]; $(function () { - const nullPlaceholder = '—'; + const nullPlaceholder = '—'; let inputAction = function (requestData, dataTableSettings) { const applicationId = pageApplicationId @@ -76,7 +76,7 @@ $(function () { } }); - $.fn.dataTable.Api.register('row().selectWithParams()', function (params) { + $.fn.dataTable.Api.register('row().selectWithParams()', function (params) { this.params = params; return this.select(); }); @@ -255,7 +255,7 @@ $(function () { $("#AdjudicationTeamLeadActionBar .dt-buttons").contents().unwrap(); updateAiActionButtonsVisibility(reviewListTable); - reviewListTable.on('select', function (e, dt, type, indexes) { + reviewListTable.on('select', function (e, dt, type, indexes) { handleRowSelection(e, dt, type, indexes, reviewListTable); }); @@ -263,9 +263,9 @@ $(function () { handleRowDeselection(e, dt, type, indexes, reviewListTable); }); - PubSub.subscribe('refresh_review_list', (msg, data) => { - refreshReviewList(data, reviewListTable); - }); + PubSub.subscribe('refresh_review_list', (msg, data) => { + refreshReviewList(data, reviewListTable); + }); PubSub.subscribe('refresh_review_list_without_sidepanel', (msg, data) => { refreshReviewList(data, reviewListTable, false); @@ -294,7 +294,7 @@ function handleRowSelection(e, dt, type, indexes, reviewListTable) { if (type === 'row') { let selectedData = reviewListTable.row(indexes).data(); document.getElementById("AssessmentId").value = selectedData.id; - if (refreshSidePanel) { + if (refreshSidePanel) { PubSub.publish('select_application_review', selectedData); PubSub.publish('refresh_assessment_attachment_list', selectedData.id); } @@ -455,29 +455,95 @@ function unityWorkflowButtonAction(e, dt, button, config) { } } -function generateAiButtonAction(e, dt, button, config) { - const $btn = $(this.node()); - const promptVersion = globalThis.getSelectedPromptVersion?.() || null; - - this.disable(); - $btn.html('Queueing...'); - - unity.grantManager.grantApplications.applicationScoring.generateApplicationScoring(pageApplicationId, promptVersion) - .done(function () { - abp.notify.success('AI scoring queued. Refresh later to see updated results.'); - }) - .fail(function () { - abp.message.error('Failed to queue AI scoring. Please try again.'); - }) - .always(() => { - this.enable(); - $btn.html(generateAiButtonText(null, null, null)); - }); -} - -function executeAssessmentAction(assessmentId, triggerAction) { - unity.grantManager.assessments.assessment.executeAssessmentAction(assessmentId, triggerAction, {}) - .then(function (result) { +function generateAiButtonAction(e, dt, button, config) { + const $button = button?.node ? $(button.node) : null; + const promptVersion = globalThis.getSelectedPromptVersion?.() || null; + const aiGenerationPollIntervalMs = 15000; + let aiGenerationPollTimeoutId = null; + + if ($button?.length) { + $button.prop('disabled', true); + $button.html('Generating...'); + globalThis.AIGenerationButtonState?.setGenerating($button); + } + + const stopPolling = function () { + if (aiGenerationPollTimeoutId) { + clearTimeout(aiGenerationPollTimeoutId); + aiGenerationPollTimeoutId = null; + } + }; + + const poll = function () { + unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(pageApplicationId, 'application-scoring', promptVersion) + .done(function (request) { + const status = request?.status; + + if (status === 'Failed') { + stopPolling(); + abp.message.error(request?.failureReason || 'AI scoring failed.'); + if ($button?.length) { + globalThis.AIGenerationButtonState?.restore($button); + $button.prop('disabled', false); + $button.html(generateAiButtonText(null, null, null)); + } + return; + } + + if (!request || request.isActive === false || status === 'Completed') { + stopPolling(); + setReviewListAiButtonCompleted($button); + refreshReviewListAfterAiScoring(); + return; + } + + aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); + }) + .fail(function () { + aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); + }); + }; + + unity.grantManager.grantApplications.grantApplication.queueApplicationScoring(pageApplicationId, promptVersion) + .done(function (request) { + if (request?.status === 'Completed') { + setReviewListAiButtonCompleted($button); + refreshReviewListAfterAiScoring(); + return; + } + + aiGenerationPollTimeoutId = setTimeout(poll, 500); + }) + .fail(function () { + stopPolling(); + abp.message.error('Failed to queue AI scoring. Please try again.'); + if ($button?.length) { + globalThis.AIGenerationButtonState?.restore($button); + $button.prop('disabled', false); + $button.html(generateAiButtonText(null, null, null)); + } + }) + ; +} + +function setReviewListAiButtonCompleted($button) { + if (!$button?.length) { + return; + } + + globalThis.AIGenerationButtonState?.setCompleted($button); + $button.html('Completed').prop('disabled', true); +} + +function refreshReviewListAfterAiScoring() { + PubSub.publish('refresh_review_list', pageApplicationId); + PubSub.publish('refresh_assessment_scores', null); +} + +function executeAssessmentAction(assessmentId, triggerAction) { + unity.grantManager.assessments.assessment.executeAssessmentAction(assessmentId, triggerAction, {}) + .then(function (result) { PubSub.publish('assessment_action_completed'); PubSub.publish('refresh_review_list', assessmentId); abp.notify.success( From 7cf627998f380fbc257c36e7b493e67872f39197 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 17 Apr 2026 22:16:33 -0700 Subject: [PATCH 045/134] AB#32451 tighten AI generation flow --- .../IApplicationAIGenerationQueue.cs | 7 +- .../IAIGenerationStatusAppService.cs | 2 +- .../IGrantApplicationAppService.cs | 1 - .../AIGenerationStatusAppService.cs | 8 +- .../AIGenerationRequestJobBase.cs | 62 ++++ .../GenerateApplicationAnalysisJob.cs | 53 +--- .../GenerateApplicationScoringJob.cs | 53 +--- .../GenerateAttachmentSummaryJob.cs | 59 +--- .../RunApplicationAIPipelineJob.cs | 63 +--- ...teAIAssessmentOnScoringGeneratedHandler.cs | 9 +- .../GrantApplicationAppService.cs | 40 +-- .../Pages/GrantApplications/ai-analysis.js | 6 +- .../ChefsAttachments/ChefsAttachments.js | 290 +++++++----------- .../ChefsAttachments/Default.cshtml | 34 +- .../Automation/AIGenerationQueueTests.cs | 28 +- .../AIGenerationStatusAppServiceTests.cs | 44 +++ ...ssessmentOnScoringGeneratedHandlerTests.cs | 36 +-- 17 files changed, 304 insertions(+), 491 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobBase.cs create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationStatusAppServiceTests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs index b8f059a463..f7837b6c5a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Automation/IApplicationAIGenerationQueue.cs @@ -5,11 +5,8 @@ namespace Unity.AI.Automation; public interface IApplicationAIGenerationQueue { -<<<<<<< HEAD -======= - Task QueueAttachmentSummariesAsync(Guid applicationId, IReadOnlyList attachmentIds, Guid? tenantId, string? promptVersion = null); + Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); ->>>>>>> 64123200c (AB#32451 consolidate AI queue entrypoints) - Task QueueApplicationPipelineAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); + Task QueueAllAIStagesAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs index 836d2e8ec7..7bc6594040 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IAIGenerationStatusAppService.cs @@ -6,5 +6,5 @@ namespace Unity.GrantManager.GrantApplications; public interface IAIGenerationStatusAppService : IApplicationService { - Task GetLatestAsync(Guid applicationId, string operationType, string? promptVersion = null); + Task GetLatestAsync(Guid applicationId, string operationType, string? promptVersion = null, Guid? tenantId = null); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs index c4fbe82eb0..0d1dc61543 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs @@ -21,7 +21,6 @@ public interface IGrantApplicationAppService Task QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null); Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null); - Task QueueAttachmentSummariesAsync(Guid applicationId, List attachmentIds, string? promptVersion = null); Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null); Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null); Task QueueAIPipelineAsync(Guid applicationId, string? promptVersion = null); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs index 22749f0c55..60781d99c1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/AIGenerationStatusAppService.cs @@ -4,22 +4,26 @@ using Unity.GrantManager.GrantApplications; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; namespace Unity.GrantManager.GrantApplications; public class AIGenerationStatusAppService( - IRepository generationRequestRepository) + IRepository generationRequestRepository, + ICurrentTenant currentTenant) : ApplicationService, IAIGenerationStatusAppService { - public virtual async Task GetLatestAsync(Guid applicationId, string operationType, string? promptVersion = null) + public virtual async Task GetLatestAsync(Guid applicationId, string operationType, string? promptVersion = null, Guid? tenantId = null) { var query = await generationRequestRepository.GetQueryableAsync(); + var resolvedTenantId = tenantId ?? currentTenant.Id; var item = await AsyncExecuter.FirstOrDefaultAsync( query.Where(x => x.ApplicationId == applicationId && x.OperationType == operationType && + x.TenantId == resolvedTenantId && (promptVersion == null || x.PromptVersion == promptVersion)) .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id)); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobBase.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobBase.cs new file mode 100644 index 0000000000..a3d063c002 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobBase.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; + +public static class AIGenerationRequestJobBase +{ + public static async Task MarkRunningAsync( + IRepository generationRequestRepository, + AIGenerationRequest? request) + { + if (request == null) + { + return; + } + + request.MarkRunning(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + public static async Task MarkCompletedAsync( + IRepository generationRequestRepository, + AIGenerationRequest? request) + { + if (request == null) + { + return; + } + + request.MarkCompleted(DateTime.UtcNow); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + public static async Task MarkFailedAsync( + IRepository generationRequestRepository, + AIGenerationRequest? request, + string? failureReason) + { + if (request == null) + { + return; + } + + request.MarkFailed(DateTime.UtcNow, failureReason); + await generationRequestRepository.UpdateAsync(request, autoSave: true); + } + + public static async Task GetLatestRequestAsync( + IRepository generationRequestRepository, + Expression> predicate) + { + var requests = await generationRequestRepository.GetListAsync(predicate); + return requests + .OrderByDescending(x => x.CreationTime) + .ThenByDescending(x => x.Id) + .FirstOrDefault(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs index 4daaef6fa3..5103244300 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationAnalysisJob.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Linq; using System.Threading.Tasks; using Unity.AI.Operations; using Unity.GrantManager.GrantApplications; @@ -21,65 +20,21 @@ public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJob { using (currentTenant.Change(args.TenantId)) { - await MarkRunningAsync(args.RequestKey); + var request = await AIGenerationRequestJobBase.GetLatestRequestAsync(generationRequestRepository, x => x.RequestKey == args.RequestKey); + await AIGenerationRequestJobBase.MarkRunningAsync(generationRequestRepository, request); try { logger.LogInformation("Executing AI application analysis job for application {ApplicationId}.", args.ApplicationId); await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId); - await MarkCompletedAsync(args.RequestKey); + await AIGenerationRequestJobBase.MarkCompletedAsync(generationRequestRepository, request); } catch (Exception ex) { - await MarkFailedAsync(args.RequestKey, ex.Message); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, ex.Message); throw; } } } - - private async Task MarkRunningAsync(string requestKey) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkRunning(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkCompletedAsync(string requestKey) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkCompleted(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkFailedAsync(string requestKey, string? failureReason) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkFailed(DateTime.UtcNow, failureReason); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task GetRequestAsync(string requestKey) - { - var requests = await generationRequestRepository.GetListAsync(x => x.RequestKey == requestKey); - return requests - .OrderByDescending(x => x.CreationTime) - .ThenByDescending(x => x.Id) - .FirstOrDefault(); - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs index ed168268c2..a0cd1736c4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateApplicationScoringJob.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Linq; using System.Threading.Tasks; using Unity.AI.Operations; using Unity.GrantManager.GrantApplications; @@ -24,7 +23,8 @@ public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobA { using (currentTenant.Change(args.TenantId)) { - await MarkRunningAsync(args.RequestKey); + var request = await AIGenerationRequestJobBase.GetLatestRequestAsync(generationRequestRepository, x => x.RequestKey == args.RequestKey); + await AIGenerationRequestJobBase.MarkRunningAsync(generationRequestRepository, request); try { logger.LogInformation("Executing AI application scoring job for application {ApplicationId}.", args.ApplicationId); @@ -34,58 +34,13 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent ApplicationId = args.ApplicationId }); - await MarkCompletedAsync(args.RequestKey); + await AIGenerationRequestJobBase.MarkCompletedAsync(generationRequestRepository, request); } catch (Exception ex) { - await MarkFailedAsync(args.RequestKey, ex.Message); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, ex.Message); throw; } } } - - private async Task MarkRunningAsync(string requestKey) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkRunning(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkCompletedAsync(string requestKey) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkCompleted(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkFailedAsync(string requestKey, string? failureReason) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkFailed(DateTime.UtcNow, failureReason); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task GetRequestAsync(string requestKey) - { - var requests = await generationRequestRepository.GetListAsync(x => x.RequestKey == requestKey); - return requests - .OrderByDescending(x => x.CreationTime) - .ThenByDescending(x => x.Id) - .FirstOrDefault(); - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs index 51adae95ba..a3cbf6dacd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/GenerateAttachmentSummaryJob.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Linq; using System.Threading.Tasks; using Unity.AI.Operations; using Unity.GrantManager.GrantApplications; @@ -21,65 +20,21 @@ public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobAr { using (currentTenant.Change(args.TenantId)) { - await MarkRunningAsync(args.RequestKey); + var request = await AIGenerationRequestJobBase.GetLatestRequestAsync(generationRequestRepository, x => x.RequestKey == args.RequestKey); + await AIGenerationRequestJobBase.MarkRunningAsync(generationRequestRepository, request); try { logger.LogInformation( - "Executing AI attachment summary job for {AttachmentCount} attachment(s).", - args.AttachmentIds.Count); - await attachmentSummaryService.GenerateAndSaveAsync(args.AttachmentIds, args.PromptVersion); - await MarkCompletedAsync(args.RequestKey); + "Executing AI attachment summary job for application {ApplicationId}.", + args.ApplicationId); + await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion); + await AIGenerationRequestJobBase.MarkCompletedAsync(generationRequestRepository, request); } catch (Exception ex) { - await MarkFailedAsync(args.RequestKey, ex.Message); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, ex.Message); throw; } } } - - private async Task MarkRunningAsync(string requestKey) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkRunning(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkCompletedAsync(string requestKey) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkCompleted(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkFailedAsync(string requestKey, string? failureReason) - { - var request = await GetRequestAsync(requestKey); - if (request == null) - { - return; - } - - request.MarkFailed(DateTime.UtcNow, failureReason); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task GetRequestAsync(string requestKey) - { - var requests = await generationRequestRepository.GetListAsync(x => x.RequestKey == requestKey); - return requests - .OrderByDescending(x => x.CreationTime) - .ThenByDescending(x => x.Id) - .FirstOrDefault(); - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs index cea059bf2d..68863446d7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -44,7 +44,9 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) { try { - var request = await GetRequestAsync(requestKey, args.ApplicationId); + var request = await AIGenerationRequestJobBase.GetLatestRequestAsync(generationRequestRepository, x => + x.RequestKey == requestKey && + x.ApplicationId == args.ApplicationId); if (request != null && request.Status == AIGenerationRequestStatus.Completed) { @@ -52,11 +54,7 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) return; } - if (request != null) - { - request.MarkRunning(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } + await AIGenerationRequestJobBase.MarkRunningAsync(generationRequestRepository, request); var application = await applicationRepository.GetAsync(args.ApplicationId); var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); @@ -64,7 +62,7 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) if (!applicationForm.AutomaticallyGenerateAIAnalysis) { logger.LogDebug("Automatic AI analysis is disabled at form level for application {ApplicationId}, skipping intake pipeline.", args.ApplicationId); - await MarkCompletedAsync(requestKey, args.ApplicationId); + await AIGenerationRequestJobBase.MarkCompletedAsync(generationRequestRepository, request); return; } @@ -74,14 +72,14 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled) { logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); - await MarkCompletedAsync(requestKey, args.ApplicationId); + await AIGenerationRequestJobBase.MarkCompletedAsync(generationRequestRepository, request); return; } if (!await aiService.IsAvailableAsync()) { logger.LogWarning("AI service is not available, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); - await MarkFailedAsync(requestKey, args.ApplicationId, "AI service is not available."); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, "AI service is not available."); return; } @@ -128,60 +126,27 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent if (scoringException != null) { - await MarkFailedAsync(requestKey, args.ApplicationId, scoringException.Message); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, scoringException.Message); throw scoringException; } if (analysisException != null) { - await MarkFailedAsync(requestKey, args.ApplicationId, analysisException.Message); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, analysisException.Message); throw analysisException; } - await MarkCompletedAsync(requestKey, args.ApplicationId); + await AIGenerationRequestJobBase.MarkCompletedAsync(generationRequestRepository, request); } catch (Exception ex) { - await MarkFailedAsync(requestKey, args.ApplicationId, ex.Message); + var request = await AIGenerationRequestJobBase.GetLatestRequestAsync(generationRequestRepository, x => + x.RequestKey == requestKey && + x.ApplicationId == args.ApplicationId); + await AIGenerationRequestJobBase.MarkFailedAsync(generationRequestRepository, request, ex.Message); throw; } } } } - - private async Task GetRequestAsync(string requestKey, Guid applicationId) - { - var requests = await generationRequestRepository.GetListAsync(x => - x.RequestKey == requestKey && - x.ApplicationId == applicationId); - - return requests - .OrderByDescending(x => x.CreationTime) - .ThenByDescending(x => x.Id) - .FirstOrDefault(); - } - - private async Task MarkCompletedAsync(string requestKey, Guid applicationId) - { - var request = await GetRequestAsync(requestKey, applicationId); - if (request == null) - { - return; - } - - request.MarkCompleted(DateTime.UtcNow); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } - - private async Task MarkFailedAsync(string requestKey, Guid applicationId, string? failureReason) - { - var request = await GetRequestAsync(requestKey, applicationId); - if (request == null) - { - return; - } - - request.MarkFailed(DateTime.UtcNow, failureReason); - await generationRequestRepository.UpdateAsync(request, autoSave: true); - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs index c93eff4685..4f8da39ef8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/Handlers/CreateAIAssessmentOnScoringGeneratedHandler.cs @@ -1,21 +1,18 @@ using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; -using Unity.AI.Settings; using Unity.GrantManager.Applications; using Unity.GrantManager.Assessments; using Unity.GrantManager.GrantApplications.Automation.Events; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus; using Volo.Abp.Features; -using Volo.Abp.Settings; using Volo.Abp.Uow; namespace Unity.GrantManager.GrantApplications.Automation.Handlers; public class CreateAIAssessmentOnScoringGeneratedHandler( AssessmentManager assessmentManager, IApplicationRepository applicationRepository, IFeatureChecker featureChecker, - ISettingProvider settingProvider, IUnitOfWorkManager unitOfWorkManager, ILogger logger) : ILocalEventHandler, ITransientDependency { @@ -26,10 +23,6 @@ public async Task HandleEventAsync(ApplicationAIScoringGeneratedEvent eventData) logger.LogWarning("Event data or application ID is null in CreateAIAssessmentOnScoringGeneratedHandler."); return; } - if (!await settingProvider.GetAsync(AISettings.AutomaticGenerationEnabled, defaultValue: false)) - { - return; - } if (!await featureChecker.IsEnabledAsync("Unity.AI.Scoring")) { return; @@ -47,4 +40,4 @@ public async Task HandleEventAsync(ApplicationAIScoringGeneratedEvent eventData) logger.LogError(ex, "Error creating AI assessment for application {ApplicationId}.", eventData.ApplicationId); } } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 9d3a3ba4fc..e5af5a2805 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -1050,7 +1050,8 @@ public async Task QueueApplicationAnalysisAsync(Guid app var request = await aiGenerationStatusAppService.GetLatestAsync( applicationId, AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType, - promptVersion); + promptVersion, + CurrentTenant.Id); return request ?? throw new UserFriendlyException("Unable to queue AI analysis request."); } @@ -1058,36 +1059,13 @@ public async Task QueueApplicationAnalysisAsync(Guid app public async Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null) { await EnsureAttachmentSummariesEnabledAsync(); - var attachmentIds = await applicationChefsFileAttachmentRepository.GetListAsync(applicationId); - if (attachmentIds.Count == 0) - { - throw new UserFriendlyException("No attachments are available to summarize for this application."); - } - - await aiGenerationQueue.QueueAttachmentSummariesAsync(applicationId, attachmentIds.Select(a => a.Id).ToList(), CurrentTenant.Id, promptVersion); - - var request = await aiGenerationStatusAppService.GetLatestAsync( - applicationId, - AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, - promptVersion); - - return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request."); - } - - public async Task QueueAttachmentSummariesAsync(Guid applicationId, List attachmentIds, string? promptVersion = null) - { - await EnsureAttachmentSummariesEnabledAsync(); - if (attachmentIds.Count == 0) - { - throw new UserFriendlyException("No attachments are available to summarize for this application."); - } - - await aiGenerationQueue.QueueAttachmentSummariesAsync(applicationId, attachmentIds, CurrentTenant.Id, promptVersion); + await aiGenerationQueue.QueueAttachmentSummaryAsync(applicationId, CurrentTenant.Id, promptVersion); var request = await aiGenerationStatusAppService.GetLatestAsync( applicationId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType, - promptVersion); + promptVersion, + CurrentTenant.Id); return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request."); } @@ -1100,14 +1078,15 @@ public async Task QueueApplicationScoringAsync(Guid appl var request = await aiGenerationStatusAppService.GetLatestAsync( applicationId, AIGenerationRequestKeyHelper.ApplicationScoringOperationType, - promptVersion); + promptVersion, + CurrentTenant.Id); return request ?? throw new UserFriendlyException("Unable to queue AI scoring request."); } public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null) { - return await aiGenerationStatusAppService.GetLatestAsync(applicationId, operationType, promptVersion); + return await aiGenerationStatusAppService.GetLatestAsync(applicationId, operationType, promptVersion, CurrentTenant.Id); } public async Task QueueAIPipelineAsync(Guid applicationId, string? promptVersion = null) @@ -1120,7 +1099,8 @@ public async Task QueueAIPipelineAsync(Guid applicationI var request = await aiGenerationStatusAppService.GetLatestAsync( applicationId, AIGenerationRequestKeyHelper.PipelineOperationType, - promptVersion); + promptVersion, + CurrentTenant.Id); return request ?? throw new UserFriendlyException("Unable to queue AI generation request."); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index 418c3d3ed0..8f9f56b79b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -488,7 +488,7 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { .queueApplicationAnalysis(applicationId, promptVersion) .done(function(request) { aiAnalysisPollFailures = 0; - setAiGenerationStatus(formatAiGenerationStatus(request?.status) || 'Queued'); + setAIGenerationStatus(formatAiGenerationStatus(request?.status) || 'Queued'); stopAIAnalysisPolling(); aiAnalysisPollTimeoutId = setTimeout(poll, 500); }) @@ -500,6 +500,10 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { }); } +function setAIGenerationStatus(value) { + $('#aiGenerationStatus').text(value ? `(${value})` : ''); +} + function loadAIAnalysis() { if ($('#AIAnalysisFeatureEnabled').val() === 'False') { return; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 7d80f14484..d07b5a61a6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -1,20 +1,20 @@ // Note: File depends on Unity.GrantManager.Web\Views\Shared\Components\_Shared\Attachments.js $(function () { - globalThis.queueAttachmentSummary = function(triggerButton = null) { + globalThis.queueAttachmentSummary = function (triggerButton = null) { $('#generateAiSummaries') .data('trigger-button', triggerButton || null) .trigger('click'); }; - const downloadAll = $('#downloadSelected'); + const downloadAll = $('#downloadAll'); const dt = $('#ChefsAttachmentsTable'); let chefsDataTable; - let selectedAtttachments = []; + const aiSummaryPollIntervalMs = 15000; + let aiSummaryPollTimeoutId = null; const nullPlaceholder = '—'; let inputAction = function (requestData, dataTableSettings) { - const urlParams = new URL(window.location.toLocaleString()) - .searchParams; + const urlParams = new URL(window.location.toLocaleString()).searchParams; const applicationId = urlParams.get('ApplicationId'); return applicationId; }; @@ -31,15 +31,8 @@ $(function () { }); }, 10); - if (result.length === 0 || selectedAtttachments.length === 0) { - $(downloadAll).prop('disabled', true); + $(downloadAll).prop('disabled', result.length === 0); - if (document.getElementById('generateAiSummaries')) { - $generateAISummariesButton.prop('disabled', true); - } - } - - // Check if any attachments have AI summaries and enable/disable toggle button const hasAISummaries = result.some( (item) => item.aiSummary && item.aiSummary.trim() !== '' ); @@ -49,7 +42,7 @@ $(function () { if (!hasAISummaries) { $toggleButton.attr('title', 'No AI summaries available'); } else { - $toggleButton.attr('title', 'Toggle AI Summaries'); + $toggleButton.attr('title', 'Show AI Summaries'); } } } @@ -60,13 +53,25 @@ $(function () { function getColumns() { return [ - getSelectColumn('Select Attachment', 'rowCount', 'chefs-files'), + getChefsIconColumn(), getChefsFileNameColumn(), getChefsLabelColumn(), getChefsFileDownloadColumn(), ]; } + function getChefsIconColumn() { + return { + title: '', + width: '40px', + className: 'text-center', + render: function () { + return ''; + }, + orderable: false, + }; + } + function getChefsFileNameColumn() { return { title: 'Document Name', @@ -91,8 +96,6 @@ $(function () { }; } - - let formatItems = function (items) { const newData = items.map((item, index) => { return { @@ -115,10 +118,6 @@ $(function () { scrollCollapse: false, processing: true, autoWidth: true, - select: { - style: 'multiple', - selector: 'td:not(:nth-child(4))', - }, ajax: abp.libs.datatables.createAjax( unity.grantManager.attachments.attachment .getApplicationChefsFileAttachments, @@ -152,59 +151,87 @@ $(function () { chefsDataTable.ajax.reload(); }); - //Generate AI summaries for attachments + // Generate AI summaries for the current application attachments. const $generateAISummariesButton = $('#generateAiSummaries'); if ($generateAISummariesButton.length > 0) { - function resetAttachmentSelectionState() { - selectedAtttachments = []; - $('.select-all-chefs-files').prop('checked', false); - chefsDataTable.$('.chkbox').prop('checked', false); - $(downloadAll).prop('disabled', true); - $generateAISummariesButton.prop('disabled', true); - } - $generateAISummariesButton.on('click', function () { const $button = $(this); const triggerButton = $button.data('trigger-button'); const $activeButton = triggerButton ? $(triggerButton) : $button; - const rowsToProcess = triggerButton - ? chefsDataTable.rows().data() - : chefsDataTable.rows({ selected: true }).data(); + const applicationId = new URL(window.location.toLocaleString()).searchParams.get('ApplicationId'); const promptVersion = globalThis.getSelectedPromptVersion?.() || null; $button.removeData('trigger-button'); - if (rowsToProcess.length === 0) { - abp.message.warn( - triggerButton - ? 'No attachments were found to generate summaries for.' - : 'Please select at least one attachment to generate summaries.' - ); + if (!applicationId) { + abp.message.warn('No application was found for attachment summary generation.'); return; } - const attachmentIds = rowsToProcess.toArray().map((row) => row.id); - const existingHTML = $activeButton.html(); + const stopPolling = function () { + if (aiSummaryPollTimeoutId) { + clearTimeout(aiSummaryPollTimeoutId); + aiSummaryPollTimeoutId = null; + } + }; + const poll = function () { + unity.grantManager.grantApplications.grantApplication + .getAIGenerationStatus(applicationId, 'attachment-summary', promptVersion) + .done(function (request) { + const status = request?.status; + + if (status === 'Failed') { + stopPolling(); + abp.message.error(request?.failureReason || 'AI attachment summary generation failed.'); + globalThis.AIGenerationButtonState?.restore($activeButton); + $activeButton.html(existingHTML).prop('disabled', false); + return; + } + + if (!request || request.isActive === false || status === 'Completed') { + stopPolling(); + globalThis.AIGenerationButtonState?.setCompleted($activeButton); + $activeButton.html('Completed').prop('disabled', true); + chefsDataTable.ajax.reload(); + return; + } + + aiSummaryPollTimeoutId = setTimeout(poll, aiSummaryPollIntervalMs); + }) + .fail(function () { + aiSummaryPollTimeoutId = setTimeout(poll, aiSummaryPollIntervalMs); + }); + }; $activeButton .html( - 'Queueing...' + 'Generating...' ) .prop('disabled', true); + globalThis.AIGenerationButtonState?.setGenerating($activeButton); unity.grantManager.grantApplications.grantApplication - .queueAttachmentSummaries(applicationId, attachmentIds, promptVersion) - .done(function () { - $activeButton.html('Completed').prop('disabled', true); - resetAttachmentSelectionState(); + .queueAttachmentSummary(applicationId, promptVersion) + .done(function (request) { + if (request?.status === 'Completed') { + $activeButton.html('Completed').prop('disabled', true); + chefsDataTable.ajax.reload(); + return; + } + + aiSummaryPollTimeoutId = setTimeout(poll, 500); }) .fail(function (error) { + if (aiSummaryPollTimeoutId) { + clearTimeout(aiSummaryPollTimeoutId); + aiSummaryPollTimeoutId = null; + } console.error('Error queueing AI summaries:', error); abp.message.error('An error occurred while queueing AI summaries. Please try again.'); + globalThis.AIGenerationButtonState?.restore($activeButton); $activeButton.html(existingHTML).prop('disabled', false); - }) - ; + }); }); } @@ -218,28 +245,24 @@ $(function () { const $icon = $button.find('i'); const $label = $button.find('.toggle-ai-summaries-label'); - // Don't do anything if button is disabled if ($button.prop('disabled')) { return; } if (allAISummariesExpanded) { - // Collapse all chefsDataTable.rows().every(function () { const row = this; if (row.child.isShown()) { const $childRow = $(row.child()); const $summaryRow = $childRow.find('.ai-summary-row'); - // Add fade-out class to the summary row $summaryRow.removeClass('fade-in').addClass('fade-out'); - // Wait for animation to complete before hiding setTimeout(function () { row.child.hide(); $(row.node()).removeClass('shown'); $summaryRow.removeClass('fade-out'); - }, 500); // Match animation duration + }, 500); } }); $icon.removeClass('fa-chevron-up').addClass('fa-chevron-down'); @@ -247,26 +270,19 @@ $(function () { $button.attr('title', 'Show AI Summaries'); allAISummariesExpanded = false; } else { - // Expand all chefsDataTable.rows().every(function () { const row = this; const rowData = row.data(); - // Only expand if there's an AI summary if (rowData.aiSummary && rowData.aiSummary.trim() !== '') { - // Create the summary HTML const summaryHtml = formatAISummary(rowData); - // Show the child row row.child(summaryHtml).show(); $(row.node()).addClass('shown'); - // Add fade-in animation after DOM is ready setTimeout(function () { const $childRow = $(row.child()); - $childRow - .find('.ai-summary-row') - .addClass('fade-in'); + $childRow.find('.ai-summary-row').addClass('fade-in'); }, 10); } }); @@ -278,7 +294,6 @@ $(function () { }); } - // Reset AI summary expansion state when table is reloaded chefsDataTable.on('draw.dt', function () { if (allAISummariesExpanded) { const $button = $('#toggleAllAISummaries'); @@ -305,78 +320,6 @@ $(function () { ); } - chefsDataTable.on('select', function (e, dt, type, indexes) { - if (indexes?.length) { - indexes.forEach((index) => { - $(chefsDataTable.row(index).node()).find('.chkbox').prop('checked', true); - if (chefsDataTable.$('.chkbox:checked').length == chefsDataTable.$('.chkbox').length) { - $('.select-all-chefs-files').prop('checked', true); - } - selectAttachment(type, index, 'select_chefs_file'); - }); - } - }); - - chefsDataTable.on('deselect', function (e, dt, type, indexes) { - if (indexes?.length) { - indexes.forEach((index) => { - $(chefsDataTable.row(index).node()).find('.chkbox').prop('checked', false); - if (chefsDataTable.$('.chkbox:checked').length != chefsDataTable.$('.chkbox').length) { - $('.select-all-chefs-files').prop('checked', false); - } - selectAttachment(type, index, 'deselect_chefs_file'); - }); - } - }); - - function selectAttachment(type, indexes, action) { - if (type === 'row') { - let data = chefsDataTable.row(indexes).data(); - PubSub.publish(action, data); - - if (action == 'select_chefs_file') { - const found = selectedAtttachments.some( - (item) => item.chefsFileId === data.chefsFileId - ); - if (!found) { - selectedAtttachments.push({ - FormSubmissionId: data.chefsSubmissionId, - ChefsFileId: data.chefsFileId, - Filename: data.fileName, - }); - } - } else if (action == 'deselect_chefs_file') { - const filtedItems = selectedAtttachments.filter( - (item) => item.ChefsFileId !== data.chefsFileId - ); - selectedAtttachments = filtedItems; - } - - if (selectedAtttachments.length > 0) { - $(downloadAll).prop('disabled', false); - } else { - $(downloadAll).prop('disabled', true); - } - - if ( - document.getElementById('generateAiSummaries') && - selectedAtttachments.length > 0 - ) { - $generateAISummariesButton.prop('disabled', false); - } else { - $generateAISummariesButton.prop('disabled', true); - } - } - } - - $('.select-all-chefs-files').on('click', function () { - if ($(this).is(':checked')) { - chefsDataTable.rows({ page: 'current' }).select(); - } else { - chefsDataTable.rows({ page: 'current' }).deselect(); - } - }); - $('#resyncSubmissionAttachments').on('click', function () { let applicationId = document.getElementById( 'AssessmentResultViewApplicationId' @@ -385,9 +328,7 @@ $(function () { unity.grantManager.attachments.attachment .resyncSubmissionAttachments(applicationId) .done(function () { - abp.notify.success( - 'Submission Attachment/s has been resynced.' - ); + abp.notify.success('Submission Attachment/s has been resynced.'); chefsDataTable.ajax.reload(); chefsDataTable.columns.adjust(); }); @@ -406,17 +347,19 @@ $(function () { const _this = $(this); const existingHTML = _this.html(); const zip = new JSZip(); - const tempFiles = selectedAtttachments; + const tempFiles = chefsDataTable.rows({ search: 'applied' }).data().toArray().map((row) => ({ + FormSubmissionId: row.chefsSubmissionId, + ChefsFileId: row.chefsFileId, + Filename: row.fileName, + })); if (tempFiles.length > 0) { - //Calls an endpoint $.ajax({ url: '/api/app/attachment/chefs/download-all', data: JSON.stringify(tempFiles), contentType: 'application/json', type: 'POST', beforeSend: function () { - //Add loading spinner $(_this) .html( '
Downloading...
' @@ -430,9 +373,7 @@ $(function () { }); }); - zip.generateAsync({ type: 'blob' }).then(function ( - content - ) { + zip.generateAsync({ type: 'blob' }).then(function (content) { const link = document.createElement('a'); link.href = URL.createObjectURL(content); link.download = `${refNo}-All_Attachments.zip`; @@ -443,7 +384,6 @@ $(function () { '', 'The files have been downloaded successfully.' ); - //show original HTML and enable $(_this).html(existingHTML).prop('disabled', false); }, error: function (error) { @@ -472,35 +412,35 @@ function getChefsFileDownloadColumn() { className: 'text-nowrap', render: function (data, type, full, meta) { let submissionId = encodeURIComponent(full.chefsSubmissionId); - let fileId = encodeURIComponent(data); - let fileName = full.fileName; - let displayName = full.displayName || full.fileName; + let fileId = encodeURIComponent(data); + let fileName = full.fileName; + let displayName = full.displayName || full.fileName; let html = ''; return html; }, @@ -526,7 +466,6 @@ function downloadChefsFile(event) { chefsFileName, }); - //Calls an endpoint $.ajax({ url: '/api/app/attachment/chefs/' + @@ -537,7 +476,6 @@ function downloadChefsFile(event) { chefsFileName, type: 'GET', success: function (data) { - // Download file by navigating to the endpoint const downloadUrl = '/api/app/attachment/chefs/' + encodeURIComponent(chefsSubmissionId) + @@ -546,7 +484,6 @@ function downloadChefsFile(event) { '/' + encodeURIComponent(chefsFileName); - // Create a temporary link and trigger download const link = document.createElement('a'); link.href = downloadUrl; link.download = chefsFileName; @@ -554,10 +491,7 @@ function downloadChefsFile(event) { document.body.appendChild(link); link.click(); document.body.removeChild(link); - abp.notify.success( - '', - 'The file has been downloaded successfully.' - ); + abp.notify.success('', 'The file has been downloaded successfully.'); }, error: function (error) { console.log('Error downloading CHEFS file:', error); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml index e59115427a..7f4b892de0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/Default.cshtml @@ -3,29 +3,23 @@ @inject IStringLocalizer L
-
-
-
Submission Attachments
-
-
- - - -
+
+
Submission Attachments
-
+
+ + + @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled) { - - - - @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled) - { - - } - @if (ViewBag.IsAIAttachmentSummariesEnabled) - { - - } + +
+ @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled || ViewBag.IsAIAttachmentSummariesEnabled) + { +
+ @if (ViewBag.IsAIAttachmentSummariesGenerateEnabled) + { + + } + @if (ViewBag.IsAIAttachmentSummariesEnabled) + { + + } +
+ }
From d54f0536bef51cec9ba774651252a8b4bee1e651 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 27 Apr 2026 13:03:27 -0700 Subject: [PATCH 065/134] feature/AB#32632-WorksheetMerge Co-authored-by: Copilot --- .../Pages/WorksheetConfiguration/Index.cshtml | 76 +------------------ .../WorksheetConfiguration/Default.cshtml | 66 ++++++++++++++++ .../WorksheetConfigurationViewComponent.cs | 45 +++++++++++ .../WorksheetListWidget/WorksheetList.js | 44 ----------- .../ConfigurationManagement/Index.cshtml | 22 +----- .../Pages/ConfigurationManagement/Index.js | 1 - 6 files changed, 115 insertions(+), 139 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/Default.cshtml create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/WorksheetConfigurationViewComponent.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml index e765faa9d3..06e56218b5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml @@ -1,78 +1,6 @@ @page -@using Microsoft.Extensions.Localization; -@using Unity.Flex.Localization; @using Unity.Flex.Web.Pages.WorksheetConfiguration; -@using Unity.Flex.Web.Views.Shared.Components.WorksheetList -@using Volo.Abp.Features; +@using Unity.Flex.Web.Views.Shared.Components.WorksheetConfiguration @model Unity.Flex.Web.Pages.WorksheetConfiguration.IndexModel; -@inject IStringLocalizer L -@inject IFeatureChecker FeatureChecker - -@section styles -{ - -} -@section scripts -{ - - -} - -@if (await FeatureChecker.IsEnabledAsync("Unity.Flex")) -{ - -
- - -
-
- -
-
-

Worksheets

-
- - -
-
-
- -
- Published filter - - - - -
-
-
-
-
- @await Component.InvokeAsync(typeof(WorksheetListWidget)) -
- -
-
-
- - - -

Preview

-
-

No sections to display.

-
-
-
-
-
-
-} \ No newline at end of file +@await Component.InvokeAsync(typeof(WorksheetConfigurationViewComponent)) \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/Default.cshtml new file mode 100644 index 0000000000..bde3e6c6f3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/Default.cshtml @@ -0,0 +1,66 @@ +@using Microsoft.Extensions.Localization +@using Unity.Flex.Localization +@using Unity.Flex.Web.Views.Shared.Components.WorksheetList +@using Volo.Abp.Features +@model Unity.Flex.Web.Views.Shared.Components.WorksheetConfiguration.WorksheetConfigurationViewModel + +@inject IStringLocalizer L +@inject IFeatureChecker FeatureChecker + +@if (await FeatureChecker.IsEnabledAsync("Unity.Flex")) +{ + +
+ + +
+
+ +
+
+

Worksheets

+
+ + +
+
+
+ +
+ Published filter + + + + +
+
+
+
+
+ @await Component.InvokeAsync(typeof(WorksheetListWidget)) +
+ +
+
+
+ + + +

Preview

+
+

No sections to display.

+
+
+
+
+
+
+} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/WorksheetConfigurationViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/WorksheetConfigurationViewComponent.cs new file mode 100644 index 0000000000..c0b45438c7 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetConfiguration/WorksheetConfigurationViewComponent.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Unity.Flex.Web.Views.Shared.Components.WorksheetConfiguration; + +[Widget( + ScriptTypes = [typeof(WorksheetConfigurationScriptBundleContributor)], + StyleTypes = [typeof(WorksheetConfigurationStyleBundleContributor)], + AutoInitialize = true)] +public class WorksheetConfigurationViewComponent(IConfiguration configuration) : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View(new WorksheetConfigurationViewModel + { + MaxFileSize = configuration["S3:MaxFileSize"] ?? "" + }); + } +} + +public class WorksheetConfigurationViewModel +{ + public string MaxFileSize { get; set; } = ""; +} + +public class WorksheetConfigurationStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/Pages/WorksheetConfiguration/Index.css"); + } +} + +public class WorksheetConfigurationScriptBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/sortablejs/Sortable.js"); + context.Files.AddIfNotContains("/Pages/WorksheetConfiguration/Index.js"); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 5f2d61fa12..afbf0ddfbe 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -61,20 +61,6 @@ $(function () { PubSub.publish('refresh_worksheet_list'); }); - function refreshWorksheetListWidget() { - const url = `../Flex/Widgets/WorksheetList/Refresh`; - fetch(url) - .then(response => response.text()) - .then(data => { - document.getElementById('worksheet-info-widget-list').innerHTML = data; - setTimeout(() => { - PubSub.publish('worksheet_list_refreshed'); - }, 100); - }) - .catch(error => { - console.error('Error refreshing worksheet-info-list-widget:', error); - }); - } function makeSectionsAndFieldsSortable() { makeCustomFieldsSortable(); @@ -111,36 +97,6 @@ $(function () { }); } - function updateCustomFieldsSequence(evt) { - let sectionId = evt.target.dataset.sectionId; - let oldIndex = evt.oldIndex; - let newIndex = evt.newIndex; - - unity.flex.worksheets.worksheetSection - .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) - .done(function () { - updatePreview(); - abp.notify.success( - 'Custom fields order updated.' - ); - }); - } - - function updateSectionSequence(evt) { - let worksheetId = evt.target.dataset.worksheetId; - let oldIndex = evt.oldIndex; - let newIndex = evt.newIndex; - - unity.flex.worksheets.worksheet - .resequenceSections(worksheetId, oldIndex, newIndex, {}) - .done(function () { - updatePreview(); - abp.notify.success( - 'Sections fields order updated.' - ); - }); - } - function updatePreview() { let worksheets = $('button.accordion-button[aria-expanded=true]'); const previewPane = $('#worksheet-preview').length ? $('#worksheet-preview') : $('#preview'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml index 08ab554893..59dfcfa43e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml @@ -1,6 +1,6 @@ @page @using Microsoft.AspNetCore.Mvc.Localization -@using Unity.Flex.Web.Views.Shared.Components.WorksheetList +@using Unity.Flex.Web.Views.Shared.Components.WorksheetConfiguration @using Unity.GrantManager.Localization @using Unity.GrantManager.Web.Pages.ConfigurationManagement @using Volo.Abp.AspNetCore.Mvc.UI.Layout @@ -208,25 +208,7 @@ @if (Model.ShowCustomFields) {
- -
-
-
- @await Component.InvokeAsync(typeof(WorksheetListWidget)) -
-
-
-
- - -

Preview

-
-

No sections to display.

-
-
-
-
-
+ @await Component.InvokeAsync(typeof(WorksheetConfigurationViewComponent))
} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js index 45bd4463e4..44624dc76a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js @@ -32,7 +32,6 @@ } const splitRestoreMap = { - 'custom-fields-div': initResizableSplit('worksheet-split-container', 'worksheet-left', 'worksheet-divider', 'worksheet-right', 'worksheetSplitWidth'), 'scoresheets-div': initResizableSplit('scoresheet-split-container', 'scoresheet-left', 'scoresheet-divider', 'scoresheet-right', 'scoresheetSplitWidth') }; From 7eb689ef938afcf7a4d22e467ea7a678e00fb4df Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 27 Apr 2026 13:50:57 -0700 Subject: [PATCH 066/134] feature/AB#32632-WorksheetMerge-Duplicates --- .../WorksheetListWidget/WorksheetList.js | 69 +------------------ 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index afbf0ddfbe..0086016885 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -59,74 +59,7 @@ $(function () { cloneWorksheetModal.onResult(function (result, response) { PubSub.publish('refresh_worksheet_list'); - }); - - - function makeSectionsAndFieldsSortable() { - makeCustomFieldsSortable(); - makeSectionsSortable(); - } - - function makeCustomFieldsSortable() { - document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { - _ = new Sortable(div, { - animation: 150, - onEnd: function (evt) { - updateCustomFieldsSequence(evt); - }, - ghostClass: 'blue-background', - onMove: function (_) { - return true; - } - }); - }); - } - - function makeSectionsSortable() { - document.querySelectorAll('.sections-wrapper-outer').forEach(function (div) { - _ = new Sortable(div, { - animation: 150, - onEnd: function (evt) { - updateSectionSequence(evt); - }, - ghostClass: 'blue-background', - onMove: function (_) { - return true; - } - }); - }); - } - - function updatePreview() { - let worksheets = $('button.accordion-button[aria-expanded=true]'); - const previewPane = $('#worksheet-preview').length ? $('#worksheet-preview') : $('#preview'); - - if (worksheets?.length > 0) { - let worksheetId = worksheets[0].dataset.worksheetId; - const url = `../Flex/Widgets/WorksheetInstance/Refresh?` - + `instanceCorrelationId=00000000-0000-0000-0000-000000000000&` - + `instanceCorrelationProvider=Preview&` - + `sheetCorrelationId=00000000-0000-0000-0000-000000000000&` - + `sheetCorrelationProvider=Preview&` - + `uiAnchor=Preview&` - + `worksheetId=${worksheetId}`; - fetch(url) - .then(response => response.text()) - .then(data => { - previewPane.html(data); - previewPane.find(':input').prop('readonly', true); - PubSub.publish('worksheet_preview_datagrid_refresh'); - }) - .catch(error => { - console.error('Error generating preview:', error); - }); - - } else { - previewPane?.html('

No sections to display.

'); - } - - $('.preview-scrollable').first().scrollTop(0); - } + }); PubSub.subscribe( 'refresh_worksheet_list', From 2583945fd04ebc9dae89928eb43c7c5fb92cf2d6 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 27 Apr 2026 13:57:16 -0700 Subject: [PATCH 067/134] feature/AB#32632-WorksheetMerge-CoPilotSuggestions Co-authored-by: Copilot --- .../UpsertCustomFieldModal.cshtml.cs | 3 +-- .../WorksheetInstanceWidget/_WorksheetSections.cshtml | 6 ++---- .../Components/WorksheetListWidget/WorksheetList.js | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs index 7a8486b53b..e2b96b74f3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using System.Text.Json; using Unity.Flex; using Unity.Flex.Web.Views.Shared.Components.CustomFieldDefinitionWidget; using Unity.Flex.Worksheets; @@ -111,7 +110,7 @@ public async Task OnGetAsync(Guid worksheetId, Guid sectionId, Guid fieldId, str if (customField.Definition != null) { - var existingDef = JsonSerializer.Deserialize(customField.Definition); + var existingDef = customField.Definition.ConvertDefinition(customField.Type); IsHidden = existingDef?.IsHidden ?? false; HideLabel = existingDef?.HideLabel ?? false; IsDisabled = existingDef?.IsDisabled ?? false; diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/_WorksheetSections.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/_WorksheetSections.cshtml index 21d8ecc9c4..dab14d8c6d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/_WorksheetSections.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/_WorksheetSections.cshtml @@ -46,18 +46,16 @@ var fieldDef = field.Definition?.ConvertDefinition(field.Type); var fieldInputId = $"{field.Name}.{Model.ModelName}.{field.Id}.{Model.WorksheetId}"; var labelPositionClass = fieldDef?.LabelPosition == "Left" ? "label-left" : "label-top"; - var fieldStyleAttr = !string.IsNullOrEmpty(fieldDef?.Style) ? $" style=\"{fieldDef.Style}\"" : ""; var fieldExtraCssClass = !string.IsNullOrEmpty(fieldDef?.CssClass) ? $" {fieldDef.CssClass}" : ""; - var labelStyleAttr = !string.IsNullOrEmpty(fieldDef?.LabelStyle) ? $" style=\"{fieldDef.LabelStyle}\"" : "";
+ style="@fieldDef?.Style"> @if (fieldDef?.HideLabel != true) { - + }
diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 0086016885..83f5875709 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -99,14 +99,14 @@ function makeCustomFieldsSortable() { document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { const wrapper = div.closest('.sections-wrapper-outer'); const isArchived = wrapper?.dataset.isArchived === 'true'; - _ = new Sortable(div, { + new Sortable(div, { animation: 150, disabled: isArchived, onEnd: function (evt) { updateCustomFieldsSequence(evt); }, ghostClass: 'blue-background', - onMove: function (_) { + onMove: function () { return true; } }); @@ -116,14 +116,14 @@ function makeCustomFieldsSortable() { function makeSectionsSortable() { document.querySelectorAll('.sections-wrapper-outer').forEach(function (div) { const isArchived = div.dataset.isArchived === 'true'; - _ = new Sortable(div, { + new Sortable(div, { animation: 150, disabled: isArchived, onEnd: function (evt) { updateSectionSequence(evt); }, ghostClass: 'blue-background', - onMove: function (_) { + onMove: function () { return true; } }); From 55006869e7b398045581be1f6bfdc59c973a799d Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Mon, 27 Apr 2026 14:26:30 -0700 Subject: [PATCH 068/134] AB#32451 clean up chefs attachments polling helpers --- .../ChefsAttachments/ChefsAttachments.js | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 9fd948d6d0..1453988905 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -14,7 +14,7 @@ $(function () { const nullPlaceholder = '—'; let inputAction = function (requestData, dataTableSettings) { - const urlParams = new URL(window.location.toLocaleString()).searchParams; + const urlParams = new URL(globalThis.location.toLocaleString()).searchParams; const applicationId = urlParams.get('ApplicationId'); return applicationId; }; @@ -60,18 +60,6 @@ $(function () { ]; } - function getChefsIconColumn() { - return { - title: '', - width: '40px', - className: 'text-center', - render: function () { - return ''; - }, - orderable: false, - }; - } - function getChefsFileNameColumn() { return { title: 'Document Name', @@ -153,12 +141,18 @@ $(function () { // Generate AI summaries for the current application attachments. const $generateAISummariesButton = $('#generateAiSummaries'); + const stopPolling = function () { + if (aiSummaryPollTimeoutId) { + clearTimeout(aiSummaryPollTimeoutId); + aiSummaryPollTimeoutId = null; + } + }; if ($generateAISummariesButton.length > 0) { $generateAISummariesButton.on('click', function () { const $button = $(this); const triggerButton = $button.data('trigger-button'); const $activeButton = triggerButton ? $(triggerButton) : $button; - const applicationId = new URL(window.location.toLocaleString()).searchParams.get('ApplicationId'); + const applicationId = new URL(globalThis.location.toLocaleString()).searchParams.get('ApplicationId'); const promptVersion = globalThis.getSelectedPromptVersion?.() || null; $button.removeData('trigger-button'); @@ -169,12 +163,6 @@ $(function () { } const existingHTML = $activeButton.html(); - const stopPolling = function () { - if (aiSummaryPollTimeoutId) { - clearTimeout(aiSummaryPollTimeoutId); - aiSummaryPollTimeoutId = null; - } - }; const poll = function () { unity.grantManager.grantApplications.grantApplication .getAIGenerationStatus(applicationId, 'attachment-summary', promptVersion) @@ -403,6 +391,18 @@ $(function () { }); }); +function getChefsIconColumn() { + return { + title: '', + width: '40px', + className: 'text-center', + render: function () { + return ''; + }, + orderable: false, + }; +} + function getChefsFileDownloadColumn() { return { title: '', From d128c42458c39557928a2569f1684b8f49c006cd Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 27 Apr 2026 14:28:15 -0700 Subject: [PATCH 069/134] feature/AB#32632-WorksheetMerge-CoPilotSuggestions Co-authored-by: Copilot --- .../WorksheetListWidget/WorksheetList.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 83f5875709..1f4a10c4e7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -95,11 +95,16 @@ function makeSectionsAndFieldsSortable() { makeSectionsSortable(); } +let customFieldSortables = []; +let sectionSortables = []; + function makeCustomFieldsSortable() { + customFieldSortables.forEach(s => s.destroy()); + customFieldSortables = []; document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { const wrapper = div.closest('.sections-wrapper-outer'); const isArchived = wrapper?.dataset.isArchived === 'true'; - new Sortable(div, { + customFieldSortables.push(new Sortable(div, { animation: 150, disabled: isArchived, onEnd: function (evt) { @@ -109,14 +114,16 @@ function makeCustomFieldsSortable() { onMove: function () { return true; } - }); + })); }); } function makeSectionsSortable() { + sectionSortables.forEach(s => s.destroy()); + sectionSortables = []; document.querySelectorAll('.sections-wrapper-outer').forEach(function (div) { const isArchived = div.dataset.isArchived === 'true'; - new Sortable(div, { + sectionSortables.push(new Sortable(div, { animation: 150, disabled: isArchived, onEnd: function (evt) { @@ -126,7 +133,7 @@ function makeSectionsSortable() { onMove: function () { return true; } - }); + })); }); } From f8a6b7c0bfd15fb8fc9322c52239edd5b5f19780 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Mon, 27 Apr 2026 14:41:48 -0700 Subject: [PATCH 070/134] AB#32451 apply Copilot AI queue fixes --- .../RunApplicationAIPipelineJobArgs.cs | 2 +- .../ApplicationAIGenerationQueue.cs | 30 +++++++++++++++++-- .../AIGenerationRequestJobHelper.cs | 5 ++-- .../RunApplicationAIPipelineJob.cs | 7 +++-- .../GrantApplicationAppService.cs | 30 +++++++++++++++++++ .../GrantManagerDbContext.cs | 2 +- .../20260415121500_AddAIRequestTable.cs | 4 +-- .../GrantManagerDbContextModelSnapshot.cs | 2 +- .../Pages/GrantApplications/Details.js | 6 ++-- .../AssessmentScoresWidget/Default.js | 6 ++-- .../Components/ReviewList/ReviewList.js | 6 ++-- 11 files changed, 82 insertions(+), 18 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs index 418a5dc665..3f5fee5594 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJobArgs.cs @@ -7,5 +7,5 @@ public class RunApplicationAIPipelineJobArgs public Guid ApplicationId { get; set; } public Guid? TenantId { get; set; } public string? PromptVersion { get; set; } - public string? RequestKey { get; set; } + public string RequestKey { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs index 81e57b11a8..2d787933ab 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/ApplicationAIGenerationQueue.cs @@ -5,6 +5,7 @@ using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; using Medallion.Threading; +using Microsoft.Extensions.Logging; using Volo.Abp.Domain.Repositories; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -14,7 +15,8 @@ namespace Unity.GrantManager.GrantApplications.Automation; public class ApplicationAIGenerationQueue( IBackgroundJobManager backgroundJobManager, IRepository generationRequestRepository, - IDistributedLockProvider distributedLockProvider) + IDistributedLockProvider distributedLockProvider, + ILogger logger) : IApplicationAIGenerationQueue, ITransientDependency { public async Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null) @@ -131,7 +133,31 @@ private async Task EnsureRequestAndEnqueueAsync( requestKey); await generationRequestRepository.InsertAsync(request, autoSave: true); - await enqueue(); + + try + { + await enqueue(); + } + catch (Exception ex) + { + await MarkFailedBestEffortAsync(request, ex); + throw; + } + } + } + + private async Task MarkFailedBestEffortAsync(AIGenerationRequest request, Exception exception) + { + try + { + await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, exception.Message); + } + catch (Exception markException) + { + logger.LogError( + markException, + "Failed to mark AI generation request {RequestId} as failed after enqueue failure.", + request.Id); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs index 5e898fea1b..843b091203 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/AIGenerationRequestJobHelper.cs @@ -53,8 +53,9 @@ public static async Task MarkFailedAsync( IRepository generationRequestRepository, Expression> predicate) { - var requests = await generationRequestRepository.GetListAsync(predicate); - return requests + var query = await generationRequestRepository.GetQueryableAsync(); + return query + .Where(predicate) .OrderByDescending(x => x.CreationTime) .ThenByDescending(x => x.Id) .FirstOrDefault(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs index 427ce252ee..def126a49e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs @@ -29,6 +29,11 @@ public class RunApplicationAIPipelineJob( { public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) { + if (string.IsNullOrWhiteSpace(args.RequestKey)) + { + throw new ArgumentException("RequestKey is required.", nameof(args.RequestKey)); + } + using (currentTenant.Change(args.TenantId)) { var request = await AIGenerationRequestJobHelper.GetLatestRequestAsync( @@ -101,13 +106,11 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent if (scoringException != null) { - await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, scoringException.Message); throw scoringException; } if (analysisException != null) { - await AIGenerationRequestJobHelper.MarkFailedAsync(generationRequestRepository, request, analysisException.Message); throw analysisException; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index eb3ff058d3..b27fc4c051 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -1041,6 +1041,7 @@ public async Task QueueAIGenerationAsync(Guid applicatio return await QueueApplicationAnalysisAsync(applicationId, promptVersion); } + [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] public async Task QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) { await EnsureAIAnalysisEnabledAsync(); @@ -1054,6 +1055,7 @@ public async Task QueueApplicationAnalysisAsync(Guid app return request ?? throw new UserFriendlyException("Unable to queue AI analysis request."); } + [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] public async Task QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null) { await EnsureAttachmentSummariesEnabledAsync(); @@ -1067,6 +1069,7 @@ public async Task QueueAttachmentSummaryAsync(Guid appli return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request."); } + [Authorize(AIPermissions.Analysis.GenerateScoring)] public async Task QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null) { await EnsureScoringEnabledAsync(); @@ -1082,9 +1085,13 @@ public async Task QueueApplicationScoringAsync(Guid appl public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null) { + await EnsureAIGenerationStatusAccessAsync(operationType); return await aiGenerationStatusAppService.GetLatestAsync(applicationId, operationType, CurrentTenant.Id); } + [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] + [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] + [Authorize(AIPermissions.Analysis.GenerateScoring)] public async Task QueueAllAIStagesAsync(Guid applicationId, string? promptVersion = null) { await EnsureAttachmentSummariesEnabledAsync(); @@ -1100,6 +1107,29 @@ public async Task QueueAllAIStagesAsync(Guid application return request ?? throw new UserFriendlyException("Unable to queue AI generation request."); } + private async Task EnsureAIGenerationStatusAccessAsync(string operationType) + { + switch (operationType) + { + case AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewApplicationAnalysis); + return; + case AIGenerationRequestKeyHelper.AttachmentSummaryOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewAttachmentSummary); + return; + case AIGenerationRequestKeyHelper.ApplicationScoringOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewScoringResult); + return; + case AIGenerationRequestKeyHelper.PipelineOperationType: + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewApplicationAnalysis); + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewAttachmentSummary); + await AuthorizationService.CheckAsync(AIPermissions.Analysis.ViewScoringResult); + return; + default: + throw new UserFriendlyException("Unknown AI generation operation type."); + } + } + private async Task EnsureAttachmentSummariesEnabledAsync() { if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries")) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs index 61e8b3b289..8e81c33850 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerDbContext.cs @@ -247,7 +247,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.Property(x => x.FailureReason).HasMaxLength(2000); b.Property(x => x.Status).IsRequired(); b.HasIndex(x => x.RequestKey); - b.HasIndex(x => new { x.ApplicationId, x.OperationType, x.Status }); + b.HasIndex(x => new { x.TenantId, x.ApplicationId, x.OperationType, x.Status }); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs index 9539a61e78..f2f4954571 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/20260415121500_AddAIRequestTable.cs @@ -39,10 +39,10 @@ protected override void Up(MigrationBuilder migrationBuilder) }); migrationBuilder.CreateIndex( - name: "IX_AIRequests_ApplicationId_OperationType_Status", + name: "IX_AIRequests_TenantId_ApplicationId_OperationType_Status", schema: "AI", table: "AIRequests", - columns: new[] { "ApplicationId", "OperationType", "Status" }); + columns: new[] { "TenantId", "ApplicationId", "OperationType", "Status" }); migrationBuilder.CreateIndex( name: "IX_AIRequests_RequestKey", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs index 40ae752ad2..8cb9d112f7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/HostMigrations/GrantManagerDbContextModelSnapshot.cs @@ -739,7 +739,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("RequestKey"); - b.HasIndex("ApplicationId", "OperationType", "Status"); + b.HasIndex("TenantId", "ApplicationId", "OperationType", "Status"); b.ToTable("AIRequests", "AI"); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index 4519cf9c1a..1e7cabeb86 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -369,9 +369,9 @@ $(function () { const statusText = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (statusText === 'Failed') { stopAIGenerationPolling(); - loadDevAiOutputs(); globalThis.AIGenerationButtonState?.restore(restoreButton); - restoreButton.html('Completed').prop('disabled', true); + restoreButton.html(originalHtml).prop('disabled', false); + loadDevAiOutputs(); abp.message.error(request?.failureReason || 'AI generate all failed.'); return; } @@ -411,7 +411,7 @@ $(function () { .fail(function() { abp.message.error(failureMessage); globalThis.AIGenerationButtonState?.restore(restoreButton); - restoreButton.html('Completed').prop('disabled', true); + restoreButton.html(originalHtml).prop('disabled', false); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 9ee58297ab..1e7dd26748 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -610,7 +610,7 @@ function queueApplicationScoring(triggerButton = null) { unity.grantManager.grantApplications.grantApplication .getAIGenerationStatus(applicationId, 'application-scoring', promptVersion) .done(function (request) { - const status = request?.status; + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Failed') { stopPolling(); @@ -638,7 +638,9 @@ function queueApplicationScoring(triggerButton = null) { unity.grantManager.grantApplications.applicationScoring .queueApplicationScoring(applicationId, promptVersion) .done(function (request) { - if (request?.status === 'Completed') { + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; + + if (status === 'Completed') { $button.html('Completed').prop('disabled', true); PubSub.publish('refresh_assessment_scores', null); return; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 0f378e12d1..e5368d36ff 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -478,7 +478,7 @@ function generateAiButtonAction(e, dt, button, config) { unity.grantManager.grantApplications.grantApplication .getAIGenerationStatus(pageApplicationId, 'application-scoring', promptVersion) .done(function (request) { - const status = request?.status; + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; if (status === 'Failed') { stopPolling(); @@ -507,7 +507,9 @@ function generateAiButtonAction(e, dt, button, config) { unity.grantManager.grantApplications.grantApplication.queueApplicationScoring(pageApplicationId, promptVersion) .done(function (request) { - if (request?.status === 'Completed') { + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; + + if (status === 'Completed') { setReviewListAiButtonCompleted($button); refreshReviewListAfterAiScoring(); return; From 2f2433d3f8df92d96d9d5c08cfdb357f7c5cda55 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Mon, 27 Apr 2026 15:04:47 -0700 Subject: [PATCH 071/134] AB#32451 fix AI permissions namespace import --- .../GrantApplications/GrantApplicationAppService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 6e65ca15a2..34726393ad 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -27,6 +27,7 @@ using Unity.Modules.Shared.Correlation; using Unity.Payments.PaymentRequests; using Unity.AI.Automation; +using Unity.AI.Permissions; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Authorization; From 7e520c7ad309295ddc9fa744f3dcd2162138b451 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Mon, 27 Apr 2026 15:54:11 -0700 Subject: [PATCH 072/134] AB#32451 fix AI queue tests for logger and queryable helper --- .../Automation/AIGenerationQueueTests.cs | 26 ++++++++++++++----- .../RunApplicationAIPipelineJobTests.cs | 8 ++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs index 888a6e90c2..45dcdf556d 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationQueueTests.cs @@ -1,4 +1,5 @@ using Medallion.Threading; +using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; using System; @@ -41,7 +42,7 @@ public async Task QueueAllAIStagesAsync_Should_Enqueue_Pipeline_Job_When_None_Ex return Task.FromResult(string.Empty); }); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueAllAIStagesAsync(applicationId, tenantId, "v1"); @@ -77,7 +78,7 @@ public async Task QueueApplicationAnalysisAsync_Should_Not_Enqueue_When_An_Activ repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); var backgroundJobManager = Substitute.For(); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueApplicationAnalysisAsync(applicationId, tenantId, promptVersion); @@ -108,7 +109,7 @@ public async Task QueueApplicationAnalysisAsync_Should_Enqueue_New_Request_When_ return Task.FromResult(string.Empty); }); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueApplicationAnalysisAsync(applicationId, tenantId, promptVersion); @@ -143,7 +144,7 @@ public async Task QueueAttachmentSummaryAsync_Should_Not_Enqueue_When_An_Active_ repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); var backgroundJobManager = Substitute.For(); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueAttachmentSummaryAsync(applicationId, tenantId, promptVersion); @@ -174,7 +175,7 @@ public async Task QueueAttachmentSummaryAsync_Should_Enqueue_New_Request_When_No return Task.FromResult(string.Empty); }); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueAttachmentSummaryAsync(applicationId, tenantId, promptVersion); @@ -209,7 +210,7 @@ public async Task QueueApplicationScoringAsync_Should_Not_Enqueue_When_An_Active repository.GetQueryableAsync().Returns(Task.FromResult>(new[] { request }.AsQueryable())); var backgroundJobManager = Substitute.For(); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueApplicationScoringAsync(applicationId, tenantId, promptVersion); @@ -240,7 +241,7 @@ public async Task QueueApplicationScoringAsync_Should_Enqueue_New_Request_When_N return Task.FromResult(string.Empty); }); - var queue = new ApplicationAIGenerationQueue(backgroundJobManager, repository, new TestDistributedLockProvider()); + var queue = CreateQueue(backgroundJobManager, repository); await queue.QueueApplicationScoringAsync(applicationId, tenantId, promptVersion); @@ -293,4 +294,15 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } } + + private static ApplicationAIGenerationQueue CreateQueue( + IBackgroundJobManager backgroundJobManager, + IRepository repository) + { + return new ApplicationAIGenerationQueue( + backgroundJobManager, + repository, + new TestDistributedLockProvider(), + Substitute.For>()); + } } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs index 2ee20bfebe..9bcde4f0f8 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/RunApplicationAIPipelineJobTests.cs @@ -104,12 +104,8 @@ private static IRepository BuildRequestRepository(out requests = requestList; var repository = Substitute.For>(); - repository.GetListAsync(Arg.Any>>()) - .Returns(callInfo => - { - var predicate = callInfo.Arg>>().Compile(); - return Task.FromResult(requestList.Where(predicate).ToList()); - }); + repository.GetQueryableAsync() + .Returns(Task.FromResult>(requestList.AsQueryable())); repository.UpdateAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(callInfo => Task.FromResult(callInfo.Arg())); From 7108393905231cc105ddcaae9add100c420ea3c4 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 27 Apr 2026 15:57:32 -0700 Subject: [PATCH 073/134] AB#32694 copilot feedback --- .../ApplicantTenantMapReconciler.cs | 8 ++-- .../Contacts/ContactManager.cs | 28 +++++++------- .../Components/ApplicantContacts/Default.js | 37 ++++++++++++++----- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs index 8d43fe61ec..0c43076a10 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/ApplicantTenantMapReconciler.cs @@ -51,9 +51,11 @@ public class ApplicantTenantMapReconciler( foreach (var oidcSub in distinctOidcSubs) { - var subUsername = oidcSub.Contains('@') - ? oidcSub[..oidcSub.IndexOf('@')].ToUpperInvariant() - : oidcSub.ToUpperInvariant(); + var subUsername = SubjectNormalizer.Normalize(oidcSub); + if (subUsername is null) + { + continue; + } desiredMappings.Add((subUsername, tenant.Id, tenant.Name)); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs index 5ab378ac8b..8fb041ecfa 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Contacts/ContactManager.cs @@ -67,18 +67,6 @@ public class ContactManager( ArgumentException.ThrowIfNullOrWhiteSpace(entityType); ArgumentNullException.ThrowIfNull(input); - var contact = await contactRepository.GetAsync(contactId); - - contact.Name = input.Name; - contact.Title = input.Title; - contact.Email = input.Email; - contact.HomePhoneNumber = input.HomePhoneNumber; - contact.MobilePhoneNumber = input.MobilePhoneNumber; - contact.WorkPhoneNumber = input.WorkPhoneNumber; - contact.WorkPhoneExtension = input.WorkPhoneExtension; - - await contactRepository.UpdateAsync(contact, autoSave: true); - var contactLinksQuery = await contactLinkRepository.GetQueryableAsync(); var links = await AsyncExecuter.ToListAsync(contactLinksQuery .Where(l => l.RelatedEntityType == entityType @@ -91,12 +79,24 @@ public class ContactManager( .WithData("entityType", entityType) .WithData("entityId", entityId); + var contact = await contactRepository.GetAsync(contactId); + + contact.Name = input.Name; + contact.Title = input.Title; + contact.Email = input.Email; + contact.HomePhoneNumber = input.HomePhoneNumber; + contact.MobilePhoneNumber = input.MobilePhoneNumber; + contact.WorkPhoneNumber = input.WorkPhoneNumber; + contact.WorkPhoneExtension = input.WorkPhoneExtension; + + await contactRepository.UpdateAsync(contact); + if (isPrimary) { foreach (var stale in links.Where(l => l.IsPrimary && l.ContactId != contactId)) { stale.IsPrimary = false; - await contactLinkRepository.UpdateAsync(stale, autoSave: true); + await contactLinkRepository.UpdateAsync(stale); } } @@ -113,7 +113,7 @@ public class ContactManager( } if (linkChanged) { - await contactLinkRepository.UpdateAsync(targetLink, autoSave: true); + await contactLinkRepository.UpdateAsync(targetLink); } return (contact, targetLink); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js index c7c5dbacd9..be59456a2f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js @@ -19,6 +19,18 @@ $(function () { return (template || '').replace('{0}', value); } + function escapeHtml(value) { + return $('
').text(value || '').html(); + } + + function renderEscapedText(value, type) { + if (type !== 'display') { + return value; + } + + return escapeHtml(value); + } + function ensureEditContactModal() { if (editContactModal) { return editContactModal; @@ -64,11 +76,18 @@ $(function () { const appId = pickCaseInsensitive(row, ['applicationId']); const refNo = data || pickCaseInsensitive(row, ['referenceNo']); const hasAppId = !!appId && appId !== '00000000-0000-0000-0000-000000000000'; + const plainTextLabel = refNo || t('nullPlaceholder', '—'); + + if (type !== 'display') { + return plainTextLabel; + } + if (!hasAppId) { - return t('nullPlaceholder', '—'); + return plainTextLabel; } - const label = refNo || t('view', 'View'); - return `${label}`; + + const label = escapeHtml(refNo || t('view', 'View')); + return `${label}`; } function renderPrimaryBadge(row) { // NOSONAR - intentionally scoped here; closure context is needed for widget encapsulation @@ -206,7 +225,7 @@ $(function () { if (type !== 'display') { return name; } - return renderPrimaryBadge(row) + name; + return renderPrimaryBadge(row) + escapeHtml(name); }, targets: 0 }, @@ -214,14 +233,14 @@ $(function () { title: t('columnType', 'Type'), data: 'role', width: '13%', - render: (data) => roleLabelMap[data] || data || t('nullPlaceholder', '—'), + render: (data, type) => renderEscapedText(roleLabelMap[data] || data || t('nullPlaceholder', '—'), type), targets: 1 }, { title: t('columnEmail', 'Email'), data: 'email', width: '22%', - render: (data) => data || t('nullPlaceholder', '—'), + render: (data, type) => renderEscapedText(data || t('nullPlaceholder', '—'), type), targets: 2 }, { @@ -229,8 +248,8 @@ $(function () { data: null, width: '13%', render: (data, type, row) => { - const phone = row.workPhoneNumber || row.mobilePhoneNumber; - return phone || t('nullPlaceholder', '—'); + const phone = row.workPhoneNumber || row.mobilePhoneNumber || t('nullPlaceholder', '—'); + return renderEscapedText(phone, type); }, targets: 3 }, @@ -238,7 +257,7 @@ $(function () { title: t('columnTitle', 'Title'), data: 'title', width: '18%', - render: (data) => data || t('nullPlaceholder', '—'), + render: (data, type) => renderEscapedText(data || t('nullPlaceholder', '—'), type), targets: 4 }, { From d68bb68a7f07d1504db04839711c55c7dfd8799a Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Mon, 27 Apr 2026 16:14:33 -0700 Subject: [PATCH 074/134] AB#32451 fix Details.cshtml script section merge --- .../Pages/GrantApplications/Details.cshtml | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index cce1c46858..294997e161 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -60,27 +60,20 @@ && formManualEnabled && await PermissionChecker.IsGrantedAsync(AIPermissions.Analysis.GenerateScoring); } -@section styles -{ - - -} -<<<<<<< feature/AB#32451-ai-generation-polling-dedupe-and-locking -@section scripts -{ - - - - - -======= -@section scripts -{ - - ->>>>>>> dev - -} +@section styles +{ + + +} +@section scripts +{ + + + + + + +}
@await Component.InvokeAsync("ApplicationBreadcrumbWidget", new { applicationId = @Model.ApplicationId }) From 50249672186f35bfdb0d217dd7c5be898be8ff9c Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Mon, 27 Apr 2026 16:20:36 -0700 Subject: [PATCH 075/134] AB#32694 - sonarqube fixes --- .../IApplicantTenantMapReconciler.cs | 1 - .../Applicants/ApplicantAppService.cs | 15 ++++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs index 4d8475bb1e..f398d3de5a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/TenantMappings/IApplicantTenantMapReconciler.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; namespace Unity.GrantManager.ApplicantProfile; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index e28d4c961b..b55e659a79 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -35,7 +35,9 @@ public class ApplicantAppService(IApplicantRepository applicantRepository, IOrgBookService orgBookService, IApplicantAgentRepository applicantAgentRepository, IApplicationRepository applicationRepository) : GrantManagerAppService, IApplicantAppService -{ +{ + private const string ApplicantIdDataKey = "ApplicantId"; + protected new ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); [RemoteService(false)] @@ -276,8 +278,7 @@ private async Task UpdatePrimaryContactAsync(Guid applicantId, UpdatePrimaryCont { case "Contact": await UpdateLinkedContactAsync(applicantId, input); - break; - case "Agent": + break; default: await UpdateAgentContactAsync(applicantId, input); break; @@ -290,7 +291,7 @@ private async Task UpdateAgentContactAsync(Guid applicantId, UpdatePrimaryContac if (applicantAgent.ApplicantId != applicantId) { throw new BusinessException("Unity:Applicant:ContactNotFound") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("ContactId", input.Id); } @@ -318,7 +319,7 @@ private async Task UpdateLinkedContactAsync(Guid applicantId, UpdatePrimaryConta if (link == null) { throw new BusinessException("Unity:Applicant:ContactNotFound") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("ContactId", input.Id); } @@ -345,14 +346,14 @@ private async Task UpdatePrimaryAddressAsync(Guid applicantId, UpdatePrimaryAppl if (applicantAddress.ApplicantId != applicantId) { throw new BusinessException("Unity:Applicant:AddressNotFound") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("AddressId", input.Id); } if (applicantAddress.AddressType != expectedType) { throw new BusinessException("Unity:Applicant:AddressTypeMismatch") - .WithData("ApplicantId", applicantId) + .WithData(ApplicantIdDataKey, applicantId) .WithData("AddressId", input.Id) .WithData("ExpectedType", expectedType.ToString()); } From a651b8b826380429c600fc667d63e7b93efc4139 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Mon, 27 Apr 2026 17:20:58 -0700 Subject: [PATCH 076/134] feature/AB#32632-WorksheetMerge-FixdragDropquestionInSections Co-authored-by: Copilot --- .../Worksheets/ICustomFieldAppService.cs | 1 + .../Domain/Worksheets/CustomField.cs | 2 +- .../Worksheets/CustomFieldAppService.cs | 27 ++++++++++++ .../WorksheetListWidget/WorksheetList.js | 44 ++++++++++++++----- .../Pages/ConfigurationManagement/Index.js | 16 ++++--- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs index 0715d4458c..81955ba310 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/ICustomFieldAppService.cs @@ -8,5 +8,6 @@ public interface ICustomFieldAppService Task GetAsync(Guid id); Task EditAsync(Guid id, EditCustomFieldDto dto); Task DeleteAsync(Guid id); + Task MoveToSectionAsync(Guid fieldId, Guid targetSectionId, uint newIndex); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs index 1eca950ceb..aebf75ba85 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/CustomField.cs @@ -32,7 +32,7 @@ public virtual WorksheetSection Section get => _section ?? throw new InvalidOperationException("Uninitialized property: " + nameof(Section)); } - public virtual Guid SectionId { get; } + public virtual Guid SectionId { get; internal set; } private WorksheetSection? _section; protected CustomField() diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs index cb8ba2e617..8b9c696cca 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/CustomFieldAppService.cs @@ -5,6 +5,7 @@ using Volo.Abp.Domain.Entities; using System.Linq; using Unity.Flex.Worksheets.Definitions; +using Volo.Abp; namespace Unity.Flex.Worksheets { @@ -39,5 +40,31 @@ public async Task DeleteAsync(Guid id) section.RemoveField(field); } + + public async Task MoveToSectionAsync(Guid fieldId, Guid targetSectionId, uint newIndex) + { + var field = await customFieldRepostitory.GetAsync(fieldId) ?? throw new EntityNotFoundException(); + if (field.SectionId == targetSectionId) return; + + var worksheet = await worksheetRepository.GetBySectionAsync(field.SectionId, true) ?? throw new EntityNotFoundException(); + if (worksheet.Published) throw new UserFriendlyException("Cannot move fields in a published worksheet."); + + var sourceSection = worksheet.Sections.FirstOrDefault(s => s.Id == field.SectionId) ?? throw new EntityNotFoundException(); + var targetSection = worksheet.Sections.FirstOrDefault(s => s.Id == targetSectionId) ?? throw new EntityNotFoundException(); + + // Renumber source section, excluding the moving field + var sourceFields = sourceSection.Fields.Where(f => f.Id != fieldId).OrderBy(f => f.Order).ToList(); + for (int i = 0; i < sourceFields.Count; i++) + sourceFields[i].SetOrder((uint)(i + 1)); + + // Make room in target section at the insertion point + uint insertAt = Math.Min(newIndex + 1, (uint)(targetSection.Fields.Count + 1)); + foreach (var f in targetSection.Fields.Where(f => f.Order >= insertAt)) + f.SetOrder(f.Order + 1); + + // Update FK directly — EF Core change tracker issues a single UPDATE, avoids orphan-deletion + field.SetOrder(insertAt); + field.SectionId = targetSectionId; + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 1f4a10c4e7..c92a195602 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -104,7 +104,9 @@ function makeCustomFieldsSortable() { document.querySelectorAll('.custom-fields-wrapper').forEach(function (div) { const wrapper = div.closest('.sections-wrapper-outer'); const isArchived = wrapper?.dataset.isArchived === 'true'; + const worksheetId = wrapper?.dataset.worksheetId; customFieldSortables.push(new Sortable(div, { + group: `custom-fields-${worksheetId}`, animation: 150, disabled: isArchived, onEnd: function (evt) { @@ -138,18 +140,36 @@ function makeSectionsSortable() { } function updateCustomFieldsSequence(evt) { - let sectionId = evt.target.dataset.sectionId; - let oldIndex = evt.oldIndex; - let newIndex = evt.newIndex; - - unity.flex.worksheets.worksheetSection - .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) - .done(function () { - updatePreview(); - abp.notify.success( - 'Custom fields order updated.' - ); - }); + if (evt.from === evt.to) { + // Reorder within the same section + const sectionId = evt.from.dataset.sectionId; + const oldIndex = evt.oldIndex; + const newIndex = evt.newIndex; + + unity.flex.worksheets.worksheetSection + .resequenceCustomFields(sectionId, oldIndex, newIndex, {}) + .done(function () { + updatePreview(); + abp.notify.success('Custom fields order updated.'); + }); + } else { + // Move to a different section + const fieldId = evt.item.dataset.id; + const targetSectionId = evt.to.dataset.sectionId; + const newIndex = evt.newIndex; + + unity.flex.worksheets.customField + .moveToSection(fieldId, targetSectionId, newIndex, {}) + .done(function () { + updatePreview(); + abp.notify.success('Field moved to new section.'); + }) + .fail(function () { + // Revert the DOM move on failure + evt.from.insertBefore(evt.item, evt.from.children[evt.oldIndex] || null); + abp.notify.error('Failed to move field.'); + }); + } } function updateSectionSequence(evt) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js index 44624dc76a..f17597dc1a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js @@ -1,6 +1,7 @@ $(function () { const menuItems = $('#ConfigurationManagementSideMenu .nav-item'); const configSections = $('.config-section'); + const ACTIVE_MENU_KEY = 'ConfigurationManagement_ActiveMenu'; init(); @@ -12,11 +13,13 @@ adjustDataTables(); }); - // Auto-select the first visible menu item - const firstMenuItem = menuItems.first(); - if (firstMenuItem.length) { - firstMenuItem.addClass('active'); - const targetId = firstMenuItem.data('target'); + // Restore the last active menu item from localStorage, fallback to first + const savedMenuId = localStorage.getItem(ACTIVE_MENU_KEY); + const savedMenuItem = savedMenuId ? menuItems.filter('#' + savedMenuId) : $(); + const activeMenuItem = (savedMenuItem.length ? savedMenuItem : menuItems.first()); + if (activeMenuItem.length) { + activeMenuItem.addClass('active'); + const targetId = activeMenuItem.data('target'); $('#' + targetId).removeClass('hide'); } @@ -39,6 +42,9 @@ const clickedItem = $(e.currentTarget); const targetId = clickedItem.data('target'); + // Persist selection + localStorage.setItem(ACTIVE_MENU_KEY, clickedItem.attr('id')); + // Update active menu item menuItems.removeClass('active'); clickedItem.addClass('active'); From 1722df2de5afdd04b169c884ed951cceb0746581 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Mon, 27 Apr 2026 20:49:07 -0700 Subject: [PATCH 077/134] AB#31305: Supplier Handling on Applicant Merge - Initial Draft --- .../Applicants/ApplicantListDto.cs | 3 + .../Applicants/ApplicantAppService.cs | 83 ++++++++++++------- .../Applicants/ApplicantSupplierAppService.cs | 53 ++++++++++++ .../Applicants/HandleSupplierAfterMergeDto.cs | 10 +++ .../IApplicantSupplierAppService.cs | 15 ++++ .../Repositories/ApplicantRepository.cs | 40 +++++++-- .../Components/ApplicantInfo/Default.cshtml | 17 ++++ .../Components/ApplicantInfo/Default.js | 60 +++++++++++++- .../ApplicantsActionBar/ListMerge.cshtml | 2 + .../ApplicantsActionBar/ListMerge.js | 54 ++++++++++-- 10 files changed, 290 insertions(+), 47 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs index ee8b71bd15..117abbfcdb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Applicants/ApplicantListDto.cs @@ -29,6 +29,9 @@ public class ApplicantListDto : AuditedEntityDto public int? FiscalDay { get; set; } public DateTime? StartedOperatingDate { get; set; } public string? SupplierId { get; set; } + public string? SupplierNumber { get; set; } + public string? SupplierName { get; set; } + public string? SupplierStatus { get; set; } public decimal? MatchPercentage { get; set; } public bool IsDuplicated { get; set; } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs index 0cdd0897f1..f42e42418a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantAppService.cs @@ -22,6 +22,7 @@ using Unity.Payments.Suppliers; using Volo.Abp.DependencyInjection; using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Repositories; namespace Unity.GrantManager.Applicants; @@ -34,7 +35,8 @@ public class ApplicantAppService(IApplicantRepository applicantRepository, IApplicantAddressRepository addressRepository, IOrgBookService orgBookService, IApplicantAgentRepository applicantAgentRepository, - IApplicationRepository applicationRepository) : GrantManagerAppService, IApplicantAppService + IApplicationRepository applicationRepository, + IRepository supplierRepository) : GrantManagerAppService, IApplicantAppService { protected new ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); @@ -678,37 +680,58 @@ public async Task> GetListAsync(ApplicantListRe // Execute query var applicants = await query.ToListAsync(); + // Batch-load supplier data for all applicants in one query + var supplierIds = applicants + .Where(a => a.SupplierId.HasValue) + .Select(a => a.SupplierId!.Value) + .Distinct() + .ToList(); + + Dictionary supplierMap = new(); + if (supplierIds.Count > 0) + { + var suppliers = await supplierRepository.GetListAsync(s => supplierIds.Contains(s.Id)); + supplierMap = suppliers.ToDictionary(s => s.Id); + } + // Map to DTOs - var items = applicants.Select(applicant => new ApplicantListDto + var items = applicants.Select(applicant => { - Id = applicant.Id, - ApplicantName = applicant.ApplicantName, - UnityApplicantId = applicant.UnityApplicantId, - OrgName = applicant.OrgName, - OrgNumber = applicant.OrgNumber, - OrgStatus = applicant.OrgStatus, - OrganizationType = applicant.OrganizationType, - Status = applicant.Status, - RedStop = applicant.RedStop, - NonRegisteredBusinessName = applicant.NonRegisteredBusinessName, - NonRegOrgName = applicant.NonRegOrgName, - OrganizationSize = applicant.OrganizationSize, - Sector = applicant.Sector, - SubSector = applicant.SubSector, - ApproxNumberOfEmployees = applicant.ApproxNumberOfEmployees, - IndigenousOrgInd = applicant.IndigenousOrgInd, - SectorSubSectorIndustryDesc = applicant.SectorSubSectorIndustryDesc, - FiscalMonth = applicant.FiscalMonth, - BusinessNumber = applicant.BusinessNumber, - FiscalDay = applicant.FiscalDay, - StartedOperatingDate = applicant.StartedOperatingDate.HasValue - ? applicant.StartedOperatingDate.Value.ToDateTime(TimeOnly.MinValue) - : null, - SupplierId = applicant.SupplierId?.ToString(), - MatchPercentage = applicant.MatchPercentage, - IsDuplicated = applicant.IsDuplicated, - CreationTime = applicant.CreationTime, - LastModificationTime = applicant.LastModificationTime + supplierMap.TryGetValue(applicant.SupplierId ?? Guid.Empty, out var supplier); + return new ApplicantListDto + { + Id = applicant.Id, + ApplicantName = applicant.ApplicantName, + UnityApplicantId = applicant.UnityApplicantId, + OrgName = applicant.OrgName, + OrgNumber = applicant.OrgNumber, + OrgStatus = applicant.OrgStatus, + OrganizationType = applicant.OrganizationType, + Status = applicant.Status, + RedStop = applicant.RedStop, + NonRegisteredBusinessName = applicant.NonRegisteredBusinessName, + NonRegOrgName = applicant.NonRegOrgName, + OrganizationSize = applicant.OrganizationSize, + Sector = applicant.Sector, + SubSector = applicant.SubSector, + ApproxNumberOfEmployees = applicant.ApproxNumberOfEmployees, + IndigenousOrgInd = applicant.IndigenousOrgInd, + SectorSubSectorIndustryDesc = applicant.SectorSubSectorIndustryDesc, + FiscalMonth = applicant.FiscalMonth, + BusinessNumber = applicant.BusinessNumber, + FiscalDay = applicant.FiscalDay, + StartedOperatingDate = applicant.StartedOperatingDate.HasValue + ? applicant.StartedOperatingDate.Value.ToDateTime(TimeOnly.MinValue) + : null, + SupplierId = applicant.SupplierId?.ToString(), + SupplierNumber = supplier?.Number, + SupplierName = supplier?.Name, + SupplierStatus = supplier?.Status, + MatchPercentage = applicant.MatchPercentage, + IsDuplicated = applicant.IsDuplicated, + CreationTime = applicant.CreationTime, + LastModificationTime = applicant.LastModificationTime + }; }).ToList(); return new PagedResultDto(totalCount, items); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs index 1a7ee7bb1c..b49bfd1371 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/ApplicantSupplierAppService.cs @@ -148,4 +148,57 @@ public async Task EnsureNoPendingPaymentsForApplicantAsync(Guid applicantId) if (!applicant.SupplierId.HasValue) return null; return await supplierAppService.GetAsync(applicant.SupplierId.Value); } + + [HttpGet("api/app/applicant-supplier/has-pending-payments-for-merge")] + public async Task HasPendingPaymentsForMergeAsync(Guid principalId, Guid nonPrincipalId) + { + if (!await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature)) + { + return false; + } + + var principalAppIds = (await applicationRepository + .GetListAsync(a => a.ApplicantId == principalId)) + .Select(a => a.Id) + .ToList(); + + var nonPrincipalAppIds = (await applicationRepository + .GetListAsync(a => a.ApplicantId == nonPrincipalId)) + .Select(a => a.Id) + .ToList(); + + var allAppIds = principalAppIds.Concat(nonPrincipalAppIds).ToList(); + if (allAppIds.Count == 0) return false; + + var pendingPayments = await paymentRequestService + .GetPaymentPendingListByCorrelationIdsAsync(allAppIds); + + return pendingPayments != null && pendingPayments.Count > 0; + } + + [HttpPost("api/app/applicant-supplier/handle-supplier-after-merge")] + [Authorize(UnitySelector.Payment.Supplier.Update)] + public async Task HandleSupplierAfterMergeAsync(HandleSupplierAfterMergeDto dto) + { + await EnsureNoPendingPaymentsForApplicantAsync(dto.PrincipalId); + await EnsureNoPendingPaymentsForApplicantAsync(dto.NonPrincipalId); + + var principal = await applicantRepository.GetAsync(dto.PrincipalId); + principal.SupplierId = dto.SelectedSupplierId; + await applicantRepository.UpdateAsync(principal); + + var nonPrincipal = await applicantRepository.GetAsync(dto.NonPrincipalId); + nonPrincipal.SupplierId = dto.SelectedSupplierId; + await applicantRepository.UpdateAsync(nonPrincipal); + + // Null DefaultSiteId on all applications now belonging to the principal + // (both transferred and pre-existing). Staff re-set per application. + var applications = await applicationRepository + .GetListAsync(a => a.ApplicantId == dto.PrincipalId); + foreach (var application in applications) + { + application.DefaultSiteId = null; + await applicationRepository.UpdateAsync(application); + } + } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs new file mode 100644 index 0000000000..58f2f6ba97 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/HandleSupplierAfterMergeDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Unity.GrantManager.Applicants; + +public class HandleSupplierAfterMergeDto +{ + public Guid PrincipalId { get; set; } + public Guid NonPrincipalId { get; set; } + public Guid? SelectedSupplierId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs index 5b144cdf88..992a6bae87 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Applicants/IApplicantSupplierAppService.cs @@ -16,4 +16,19 @@ public interface IApplicantSupplierAppService : IApplicationService Task UpdateApplicantSupplierNumberAsync(Guid applicantId, string supplierNumber, Guid? applicationId = null); Task UpdateAplicantSupplierByBn9Async(Guid applicantId, string bn9); Task EnsureNoPendingPaymentsForApplicantAsync(Guid applicantId); + + /// + /// Returns true if either the principal or non-principal applicant has any + /// in-progress payments (L1Pending / L2Pending / L3Pending). Used by the + /// Merge UI to show a warning before the merge is executed. + /// + Task HasPendingPaymentsForMergeAsync(Guid principalId, Guid nonPrincipalId); + + /// + /// Applies supplier and DefaultSiteId changes after a merge: + /// sets SupplierId = selectedSupplierId on both the principal and non-principal + /// (keeping the duplicate record in sync), and nulls DefaultSiteId on every + /// application that now belongs to the principal. + /// + Task HandleSupplierAfterMergeAsync(HandleSupplierAfterMergeDto dto); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs index 886f68310a..102136373c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ApplicantRepository.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using Unity.GrantManager.Applications; using Unity.GrantManager.EntityFrameworkCore; +using Unity.Payments.Domain.Suppliers; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -73,12 +74,36 @@ public async Task GetApplicantAutocompleteQueryAsync(string? appli .AsNoTracking() .ToListAsync(); - var filteredApplicants = applicants + var filtered = applicants .Where(a => (!string.IsNullOrEmpty(a.ApplicantName) && a.ApplicantName.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)) || (!string.IsNullOrEmpty(a.UnityApplicantId) && a.UnityApplicantId.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)) ) - .Select(a => new + .Take(10) + .ToList(); + + // Batch-fetch supplier data for the matched applicants + var supplierIds = filtered + .Where(a => a.SupplierId.HasValue) + .Select(a => a.SupplierId!.Value) + .Distinct() + .ToList(); + + Dictionary supplierMap = new(); + if (supplierIds.Count > 0) + { + var suppliers = await dbContext.Set() + .AsNoTracking() + .Where(s => supplierIds.Contains(s.Id)) + .Select(s => new { s.Id, s.Number, s.Name, s.Status }) + .ToListAsync(); + supplierMap = suppliers.ToDictionary(s => s.Id, s => (s.Number, s.Name, s.Status)); + } + + var filteredApplicants = filtered.Select(a => + { + supplierMap.TryGetValue(a.SupplierId ?? Guid.Empty, out var sup); + return new { a.Id, a.ApplicantName, @@ -97,10 +122,13 @@ public async Task GetApplicantAutocompleteQueryAsync(string? appli a.FiscalDay, a.FiscalMonth, a.UnityApplicantId, - a.IsDuplicated - }) - .Take(10) - .ToList(); + a.IsDuplicated, + SupplierId = a.SupplierId?.ToString(), + SupplierNumber = sup.Number, + SupplierName = sup.Name, + SupplierStatus = sup.Status + }; + }); var json = JsonSerializer.Serialize(filteredApplicants); return JsonDocument.Parse(json); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml index 960f806cc0..2560aed15c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml @@ -629,6 +629,21 @@ + + Supplier + + + + + + +
@@ -646,6 +661,8 @@ The other record will not be deleted; instead, it will be flagged as a duplicate and can be removed from the Applicant list in a separate process.

Note: The address and contact information for any affected applications will be preserved and remain untouched.

+

Note: All default sites associated with submissions linked to the merged applicant will be removed and must be re‑set after the merge is completed.

+
Warning: One or more affected submissions have in‑progress payments. These payments will be impacted by the merge. Please complete final approval of all payments before proceeding.

Are you sure?

diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css index b2d06369a4..15038d6b8d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.css @@ -53,7 +53,8 @@ /* Prevent wrapper and dt-container from generating their own scrollbars */ #FundingHistoryTable_wrapper, #IssueTrackingTable_wrapper, -#AuditHistoryTable_wrapper { +#AuditHistoryTable_wrapper, +#ReportsHistoryTable_wrapper { overflow: visible !important; } @@ -64,7 +65,8 @@ /* Scroll body — vertical scrollbar when many records */ #FundingHistoryTable_wrapper .dt-scroll-body, #IssueTrackingTable_wrapper .dt-scroll-body, -#AuditHistoryTable_wrapper .dt-scroll-body { +#AuditHistoryTable_wrapper .dt-scroll-body, +#ReportsHistoryTable_wrapper .dt-scroll-body { max-height: clamp(180px, calc(100vh - 540px), 350px) !important; overflow-y: auto !important; overflow-x: auto !important; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js index c9688e999e..66fd3a27b2 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js @@ -28,7 +28,8 @@ $(function () { .saveNotes(applicantId, { fundingHistoryComments: $('#FundingHistoryComments').val(), issueTrackingComments: $('#IssueTrackingComments').val(), - auditComments: $('#AuditComments').val() + auditComments: $('#AuditComments').val(), + reportsComments: $('#ReportsComments').val() }) .done(function () { abp.notify.success('History notes saved.'); @@ -151,6 +152,50 @@ $(function () { ].map(function (col, i) { col.index = i; col.targets = [i]; return col; }); } + function getReportsHistoryColumns() { + return [ + { title: 'Fiscal Year', data: 'fiscalYear', name: 'fiscalYear', className: 'data-table-header', render: (d) => d ?? nullPlaceholder }, + { + title: 'Report Date', data: 'reportDate', name: 'reportDate', className: 'data-table-header', + render: function (d) { + if (!d) return nullPlaceholder; + try { + return luxon.DateTime.fromISO(d, { + locale: abp.localization.currentCulture.name, + }).toUTC().toLocaleString(); + } catch (e) { console.warn('Report date parse error:', e); return d; } + } + }, + { title: 'Outstanding', data: 'outstanding', name: 'outstanding', className: 'data-table-header', width: '100px', render: (d) => d === true ? 'Yes' : 'No' }, + { title: 'Incomplete Report', data: 'incompleteReport', name: 'incompleteReport', className: 'data-table-header', width: '130px', render: (d) => d === true ? 'Yes' : 'No' }, + { + title: 'Note', data: 'note', name: 'note', className: 'data-table-header', width: '200px', + createdCell: function (td) { $(td).css('min-width', '200px'); }, + render: (d) => d ?? nullPlaceholder + }, + { + title: 'Actions', data: null, name: 'actions', orderable: false, className: 'data-table-header', width: '70px', + render: function (data, type, row) { + let $wrapper = $('
').addClass('d-flex align-items-center gap-2'); + + let $editBtn = $('
-
-
-
-
- +
- + -
-
- -
-
-
Application Analysis
+ +
+
+ +
+
+
Application Analysis
-
-
-
- -
-
- -
-
-
Application Scoring
+ +
+
+ +
+
+
Application Scoring
-
-
-
- -
-
+ +
+
} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.cshtml index 4d26174ee2..ba651545ab 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.cshtml @@ -15,114 +15,112 @@ }
@foreach (CommentDto comment in Model.Comments.Where(c => c != null)) {
-
- -
- @if (comment.IsPinned && comment.PinDateTime != null) - { - - } else +
+
+ @if (comment.IsPinned && comment.PinDateTime != null) + { + + } + else + { + + } +
+ @if (Model.CurrentUserId == comment.CommenterId || Model.CanPinComments) { - - } -
- @if (Model.CurrentUserId == comment.CommenterId || Model.CanPinComments) - { - -
- @if (comment.IsPinned && comment.PinDateTime != null) - { -
-
- @comment.Commenter + } +
-
- } -
-
- @* XSS Protection is provided by the MarkdownRenderer *@ - @Html.Raw(comment.Comment) + } +
+
+ @if (comment.IsPinned && comment.PinDateTime != null) + { +
+
+ @comment.Commenter
- @if (@comment.LastModificationTime != null) - { -
@L["ApplicationDetails:Comments.Modified"].Value @comment.LastModificationTime?.ToString("yyyy-MM-dd h:mm tt")
- } - else - { -
@L["ApplicationDetails:Comments.Created"].Value @comment.CreationTime.ToString("yyyy-MM-dd h:mm tt")
- } - + } +
+
+ @* XSS Protection is provided by the MarkdownRenderer *@ + @Html.Raw(comment.Comment) +
+ @if (comment.LastModificationTime != null) + { +
@L["ApplicationDetails:Comments.Modified"].Value @comment.LastModificationTime?.ToString("yyyy-MM-dd h:mm tt")
+ } + else + { +
@L["ApplicationDetails:Comments.Created"].Value @comment.CreationTime.ToString("yyyy-MM-dd h:mm tt")
+ } +
- }
From 23a56fe43b123ca3c4f276e898b2f93dee963f39 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 6 May 2026 14:23:14 -0700 Subject: [PATCH 133/134] feature/AB#32874-FixBaseUrl Co-authored-by: Copilot --- .../EmailNotificationService.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs index 5471937cb6..0a31ff2c46 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using System; @@ -16,7 +17,6 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Features; using Volo.Abp.SettingManagement; -using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.Users; namespace Unity.Notifications.EmailNotifications; @@ -29,7 +29,7 @@ public class EmailNotificationService( IExternalUserLookupServiceProvider externalUserLookupServiceProvider, ISettingManager settingManager, IFeatureChecker featureChecker, - IAppUrlProvider appUrlProvider) : ApplicationService, IEmailNotificationService + IHttpContextAccessor httpContextAccessor) : ApplicationService, IEmailNotificationService { public async Task InitializeDraftAsync(Guid applicationId) @@ -72,10 +72,24 @@ protected virtual async Task NotifyTeamsChannel(string chesEmailError) await notificationAppService.PostToTeamsAsync(activityTitle, activitySubtitle); } - public async Task GetBaseUrlAsync() + public Task GetBaseUrlAsync() { - var appUrl = await appUrlProvider.GetUrlAsync(appName: "MVC"); - return appUrl; + var httpContext = httpContextAccessor.HttpContext + ?? throw new InvalidOperationException("No active HTTP context available to resolve base URL."); + + var request = httpContext.Request; + + var host = request.Headers["X-Forwarded-Host"].FirstOrDefault() + ?? request.Host.Value; + + var scheme = request.Headers["X-Forwarded-Proto"].FirstOrDefault() + ?? request.Scheme; + + var pathBase = request.Headers["X-Forwarded-Prefix"].FirstOrDefault() + ?? request.PathBase.Value + ?? string.Empty; + + return Task.FromResult($"{scheme}://{host}{pathBase}".TrimEnd('/')); } public async Task SendCommentNotification(EmailCommentDto input) From 4208a74e8404dc83a783c2879e8432165413fcc9 Mon Sep 17 00:00:00 2001 From: Velang Date: Wed, 6 May 2026 14:34:44 -0700 Subject: [PATCH 134/134] fixed few issues --- .../cypress/pages/ApplicationDetailsPage.ts | 6 ++++-- .../cypress/regression/ApprovalFlow.cy.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts index 031802e548..b87f49e182 100644 --- a/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts +++ b/applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts @@ -344,11 +344,13 @@ export class ApplicationDetailsPage extends BasePage { * Click Payment Info Save button */ clickPaymentInfoSave(): this { - cy.intercept("PUT", "**/api/app/grant-application/supplier-number/**").as("saveSupplierNumber"); cy.get("#savePaymentInfoBtn", { timeout: 20000 }) .should("be.visible") + .and("not.be.disabled") .click({ force: true }); - cy.wait("@saveSupplierNumber"); + // Wait for the button to become disabled (saving in-progress) or re-enabled (save complete). + // A cy.reload() always follows immediately, so we just need the click to register. + cy.wait(1500); return this; } diff --git a/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts b/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts index 1230e7d791..3cc858c15e 100644 --- a/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts +++ b/applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts @@ -252,6 +252,17 @@ const APPLICATIONS_PATH = "GrantApplications"; } if ($body.find(".modal.show").length > 0) { + // Actively close the modal — a leftover from a retry can keep it open indefinitely. + // Try the modal's own close button first; fall back to Escape so Bootstrap + // can run its hide animation before we assert the element is gone. + const $closeBtn = $body.find( + ".modal.show .btn-close, .modal.show [data-bs-dismiss='modal'], .modal.show button.close", + ); + if ($closeBtn.length > 0) { + cy.wrap($closeBtn.first()).click({ force: true }); + } else { + cy.get("body").type("{esc}", { force: true }); + } cy.get(".modal.show", { timeout: 20000 }).should("not.exist"); cy.get(".modal-backdrop", { timeout: 20000 }).should("not.exist"); }