From 286e9c833a561e65fbcb6dee6f386fde8a3b0ae4 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 30 Apr 2026 16:13:23 -0700 Subject: [PATCH 01/34] AB#32302 stream attachment summaries for lower memory Replace the byte[]-based attachment extraction pipeline with a streaming one to avoid full in-memory buffering of CHEFS attachments when generating AI summaries. - ISubmissionAppService gains GetChefsFileAttachmentStream returning a ChefsFileAttachmentStream (Stream + content-type) backed by a temp file with FileOptions.DeleteOnClose. Implementation uses HttpCompletionOption.ResponseHeadersRead so the HTTP layer streams the response body directly to disk instead of buffering it. Temp file is cleaned up on copy/open failure. - IResilientHttpRequest.HttpAsync gains an optional HttpCompletionOption parameter (default ResponseContentRead preserves all existing callers). - ITextExtractionService.ExtractTextAsync now takes a Stream. The byte[] overload, the byte[] extractor dictionary, and the per-format byte[] private methods are removed. PDF/Word/Excel/PowerPoint extractors consume the stream directly; the text-file extractor reads incrementally via StreamReader. - AttachmentSummaryRequest drops byte[] FileContent and gains string? ExtractedText. AttachmentSummaryService streams the file, extracts text once, and passes ExtractedText to the AI runtime. - OpenAIRuntimeService.GenerateAttachmentSummaryAsync uses request.ExtractedText directly; the byte[] extraction fallback, sizeBytes payload field, and ITextExtractionService dependency are removed. Build clean. Application.Tests 337/337 and Web.Tests 16/16 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AI/Extraction/ITextExtractionService.cs | 3 +- .../AI/Requests/AttachmentSummaryRequest.cs | 6 +- .../AI/Extraction/TextExtractionService.cs | 157 +++++++++--------- .../AI/Operations/AttachmentSummaryService.cs | 29 ++-- .../AI/Runtime/OpenAIRuntimeService.cs | 26 ++- .../Http/IResilientHttpRequest.cs | 1 + .../Http/ResilientHttpRequest.cs | 8 +- .../Intakes/ChefsFileAttachmentStream.cs | 28 ++++ .../Intakes/ISubmissionAppService.cs | 6 + .../Intakes/SubmissionAppService.cs | 81 +++++++++ 10 files changed, 227 insertions(+), 118 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs index d1c4d9f992..5ad9336026 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs @@ -1,9 +1,10 @@ +using System.IO; using System.Threading.Tasks; namespace Unity.AI.Extraction { public interface ITextExtractionService { - Task ExtractTextAsync(string fileName, byte[] fileContent, string contentType); + Task ExtractTextAsync(string fileName, Stream fileContent, string contentType); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs index e1703a01ff..4731a6e631 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs @@ -7,12 +7,12 @@ public class AttachmentSummaryRequest [JsonPropertyName("fileName")] public string FileName { get; set; } = string.Empty; - [JsonPropertyName("fileContent")] - public byte[] FileContent { get; set; } = System.Array.Empty(); - [JsonPropertyName("contentType")] public string ContentType { get; set; } = "application/octet-stream"; + [JsonPropertyName("extractedText")] + public string? ExtractedText { get; set; } + [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs index 8d91759dce..e7490cfacc 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs @@ -26,30 +26,17 @@ public partial class TextExtractionService : ITextExtractionService, ITransientD private const int MaxDocxTableCellsPerRow = 50; private const int MaxPowerPointSlides = 200; private readonly ILogger _logger; - private readonly Dictionary> _extractorsByExtension; public TextExtractionService(ILogger logger) { _logger = logger; - _extractorsByExtension = new Dictionary>(StringComparer.OrdinalIgnoreCase) - { - [".txt"] = (_, content) => ExtractTextFromTextFile(content), - [".csv"] = (_, content) => ExtractTextFromTextFile(content), - [".json"] = (_, content) => ExtractTextFromTextFile(content), - [".xml"] = (_, content) => ExtractTextFromTextFile(content), - [".pdf"] = ExtractTextFromPdfFile, - [".docx"] = ExtractTextFromWordDocx, - [".xls"] = ExtractTextFromExcelFile, - [".xlsx"] = ExtractTextFromExcelFile, - [".pptx"] = ExtractTextFromPowerPointFile - }; } - public Task ExtractTextAsync(string fileName, byte[] fileContent, string contentType) + public Task ExtractTextAsync(string fileName, Stream fileContent, string contentType) { - if (fileContent == null || fileContent.Length == 0) + if (fileContent == null) { - _logger.LogDebug("File content is empty for {FileName}", fileName); + _logger.LogDebug("File content stream is null for {FileName}", fileName); return Task.FromResult(string.Empty); } @@ -64,48 +51,23 @@ public Task ExtractTextAsync(string fileName, byte[] fileContent, string return Task.FromResult(string.Empty); } - if (_extractorsByExtension.TryGetValue(extension, out var extractor)) + var rawText = extension switch { - var rawText = extractor(fileName, fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); - } - - if (normalizedContentType.Contains("text/")) - { - var rawText = ExtractTextFromTextFile(fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); - } - - if (normalizedContentType.Contains("pdf")) - { - var rawText = ExtractTextFromPdfFile(fileName, fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); - } - - if (normalizedContentType.Contains("word") || - normalizedContentType.Contains("msword") || - normalizedContentType.Contains("officedocument.wordprocessingml")) - { - var rawText = ExtractTextFromWordDocx(fileName, fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); - } - - if (normalizedContentType.Contains("excel") || normalizedContentType.Contains("spreadsheet")) + ".txt" or ".csv" or ".json" or ".xml" => ExtractTextFromTextFile(fileContent), + ".pdf" => ExtractTextFromPdfFile(fileName, fileContent), + ".docx" => ExtractTextFromWordDocx(fileName, fileContent), + ".xls" or ".xlsx" => ExtractTextFromExcelFile(fileName, fileContent), + ".pptx" => ExtractTextFromPowerPointFile(fileName, fileContent), + _ => ExtractByContentType(fileName, fileContent, normalizedContentType) + }; + + if (string.IsNullOrEmpty(rawText)) { - var rawText = ExtractTextFromExcelFile(fileName, fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); + _logger.LogDebug("No text extraction available for content type {ContentType} with extension {Extension}", + contentType, extension); } - if (normalizedContentType.Contains("presentation") || - normalizedContentType.Contains("powerpoint")) - { - var rawText = ExtractTextFromPowerPointFile(fileName, fileContent); - return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); - } - - _logger.LogDebug("No text extraction available for content type {ContentType} with extension {Extension}", - contentType, extension); - return Task.FromResult(string.Empty); + return Task.FromResult(NormalizeAndLimitText(rawText, fileName)); } catch (Exception ex) { @@ -114,25 +76,64 @@ public Task ExtractTextAsync(string fileName, byte[] fileContent, string } } - private string ExtractTextFromTextFile(byte[] fileContent) + private string ExtractByContentType(string fileName, Stream fileContent, string normalizedContentType) { - try + if (normalizedContentType.Contains("text/")) { - var text = Encoding.UTF8.GetString(fileContent); + return ExtractTextFromTextFile(fileContent); + } + if (normalizedContentType.Contains("pdf")) + { + return ExtractTextFromPdfFile(fileName, fileContent); + } + if (normalizedContentType.Contains("word") || + normalizedContentType.Contains("msword") || + normalizedContentType.Contains("officedocument.wordprocessingml")) + { + return ExtractTextFromWordDocx(fileName, fileContent); + } + if (normalizedContentType.Contains("excel") || normalizedContentType.Contains("spreadsheet")) + { + return ExtractTextFromExcelFile(fileName, fileContent); + } + if (normalizedContentType.Contains("presentation") || normalizedContentType.Contains("powerpoint")) + { + return ExtractTextFromPowerPointFile(fileName, fileContent); + } + return string.Empty; + } - if (text.Contains('\uFFFD')) - { - text = Encoding.ASCII.GetString(fileContent); - } + private static void RewindIfPossible(Stream stream) + { + if (stream.CanSeek) + { + stream.Position = 0; + } + } - if (text.Length > MaxExtractedTextLength) + private string ExtractTextFromTextFile(Stream fileContent) + { + try + { + RewindIfPossible(fileContent); + using var reader = new StreamReader(fileContent, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true); + var buffer = new char[Math.Min(MaxExtractedTextLength, 8192)]; + var builder = new StringBuilder(capacity: Math.Min(MaxExtractedTextLength, 8192)); + int read; + while ((read = reader.Read(buffer, 0, buffer.Length)) > 0) { - text = text.Substring(0, MaxExtractedTextLength); - _logger.LogDebug("Truncated text content to {MaxLength} characters", MaxExtractedTextLength); + var remaining = MaxExtractedTextLength - builder.Length; + if (remaining <= 0) break; + builder.Append(buffer, 0, Math.Min(read, remaining)); + if (builder.Length >= MaxExtractedTextLength) + { + _logger.LogDebug("Truncated text content to {MaxLength} characters", MaxExtractedTextLength); + break; + } } - _logger.LogDebug("Extracted {CharacterCount} characters from text-based content.", text.Length); - return text; + _logger.LogDebug("Extracted {CharacterCount} characters from text-based content.", builder.Length); + return builder.ToString(); } catch (Exception ex) { @@ -141,12 +142,12 @@ private string ExtractTextFromTextFile(byte[] fileContent) } } - private string ExtractTextFromPdfFile(string fileName, byte[] fileContent) + private string ExtractTextFromPdfFile(string fileName, Stream fileContent) { try { - using var stream = new MemoryStream(fileContent, writable: false); - using var document = PdfDocument.Open(stream); + RewindIfPossible(fileContent); + using var document = PdfDocument.Open(fileContent); var builder = new StringBuilder(); var processedPageCount = 0; var pageTexts = document.GetPages() @@ -177,12 +178,12 @@ private string ExtractTextFromPdfFile(string fileName, byte[] fileContent) } } - private string ExtractTextFromWordDocx(string fileName, byte[] fileContent) + private string ExtractTextFromWordDocx(string fileName, Stream fileContent) { try { - using var stream = new MemoryStream(fileContent, writable: false); - using var document = new XWPFDocument(stream); + RewindIfPossible(fileContent); + using var document = new XWPFDocument(fileContent); var builder = new StringBuilder(); var processedParagraphCount = AppendDocxParagraphText(document, builder); var processedTableRowCount = AppendDocxTableText(document, builder); @@ -268,12 +269,12 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil return processedTableRowCount; } - private string ExtractTextFromExcelFile(string fileName, byte[] fileContent) + private string ExtractTextFromExcelFile(string fileName, Stream fileContent) { try { - using var stream = new MemoryStream(fileContent, writable: false); - using var workbook = WorkbookFactory.Create(stream); + RewindIfPossible(fileContent); + using var workbook = WorkbookFactory.Create(fileContent); var builder = new StringBuilder(); var sheetCount = Math.Min(workbook.NumberOfSheets, MaxExcelSheets); var processedSheetCount = 0; @@ -314,12 +315,12 @@ private string ExtractTextFromExcelFile(string fileName, byte[] fileContent) } } - private string ExtractTextFromPowerPointFile(string fileName, byte[] fileContent) + private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent) { try { - using var stream = new MemoryStream(fileContent, writable: false); - using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false); + RewindIfPossible(fileContent); + using var archive = new ZipArchive(fileContent, ZipArchiveMode.Read, leaveOpen: true); var builder = new StringBuilder(); var slideEntries = GetOrderedPowerPointSlideEntries(archive) .Take(MaxPowerPointSlides); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs index 1225970df5..b3a74688e9 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Unity.AI.Extraction; using Unity.AI.Requests; using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; @@ -13,23 +14,25 @@ namespace Unity.AI.Operations; public class AttachmentSummaryService( IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, ISubmissionAppService submissionAppService, + ITextExtractionService textExtractionService, IAIService aiService, ILogger logger) : IAttachmentSummaryService, ITransientDependency { - private const string DefaultContentType = "application/octet-stream"; private const string SummaryGenerationFailedMessage = "AI summary generation failed."; public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null) { var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? "unknown" : attachment.FileName; - var (fileContent, contentType) = await GetAttachmentContentForSummaryAsync(attachment, fileName); + + await using var attachmentStream = await OpenAttachmentStreamAsync(attachment, fileName); + var extractedText = await textExtractionService.ExtractTextAsync(fileName, attachmentStream.Content, attachmentStream.ContentType); var summaryResponse = await aiService.GenerateAttachmentSummaryAsync(new AttachmentSummaryRequest { FileName = fileName, - FileContent = fileContent, - ContentType = contentType, + ContentType = attachmentStream.ContentType, + ExtractedText = extractedText, PromptVersion = promptVersion, }); @@ -68,7 +71,7 @@ public async Task> GenerateForApplicationAsync(Guid applicationId, return await GenerateAndSaveAsync(attachmentIds, promptVersion); } - private async Task<(byte[] Content, string ContentType)> GetAttachmentContentForSummaryAsync(ApplicationChefsFileAttachment attachment, string fileName) + private async Task OpenAttachmentStreamAsync(ApplicationChefsFileAttachment attachment, string fileName) { if (!Guid.TryParse(attachment.ChefsSubmissionId, out var submissionId) || !Guid.TryParse(attachment.ChefsFileId, out var fileId)) @@ -76,21 +79,13 @@ public async Task> GenerateForApplicationAsync(Guid applicationId, logger.LogWarning( "Attachment {AttachmentId} has invalid CHEFS IDs. Falling back to metadata-only summary generation.", attachment.Id); - return (Array.Empty(), DefaultContentType); + return ChefsFileAttachmentStream.Empty; } try { - var fileDto = await submissionAppService.GetChefsFileAttachment(submissionId, fileId, fileName); - if (fileDto?.Content == null) - { - logger.LogWarning( - "Attachment {AttachmentId} has no retrievable content. Falling back to metadata-only summary generation.", - attachment.Id); - return (Array.Empty(), DefaultContentType); - } - - return (fileDto.Content, string.IsNullOrWhiteSpace(fileDto.ContentType) ? DefaultContentType : fileDto.ContentType); + var stream = await submissionAppService.GetChefsFileAttachmentStream(submissionId, fileId, fileName); + return stream ?? ChefsFileAttachmentStream.Empty; } catch (Exception ex) { @@ -98,7 +93,7 @@ public async Task> GenerateForApplicationAsync(Guid applicationId, ex, "Failed retrieving CHEFS content for attachment {AttachmentId}. Falling back to metadata-only summary generation.", attachment.Id); - return (Array.Empty(), DefaultContentType); + return ChefsFileAttachmentStream.Empty; } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs index d9886d1f97..7d7ad49e34 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Unity.AI.Extraction; using Unity.AI.Models; using Unity.AI.Prompts; using Unity.AI.Requests; @@ -20,7 +19,6 @@ public class OpenAIRuntimeService : IAIService, ITransientDependency { private readonly IConfiguration _configuration; private readonly ILogger _logger; - private readonly ITextExtractionService _textExtractionService; private readonly OpenAITransportService _openAITransportService; private readonly OpenAIConfigurationResolver _openAIConfigurationResolver; private const string ApplicationAnalysisPromptType = AIPromptTypes.ApplicationAnalysis; @@ -51,13 +49,11 @@ public class OpenAIRuntimeService : IAIService, ITransientDependency public OpenAIRuntimeService( IConfiguration configuration, ILogger logger, - ITextExtractionService textExtractionService, OpenAITransportService openAITransportService, OpenAIConfigurationResolver openAIConfigurationResolver) { _configuration = configuration; _logger = logger; - _textExtractionService = textExtractionService; _openAITransportService = openAITransportService; _openAIConfigurationResolver = openAIConfigurationResolver; } @@ -132,19 +128,18 @@ public async Task GenerateAttachmentSummaryAsync(Atta { ArgumentNullException.ThrowIfNull(request); var fileName = request.FileName ?? string.Empty; - var fileContent = request.FileContent ?? Array.Empty(); var contentType = request.ContentType ?? "application/octet-stream"; var promptVersion = OpenAIPromptRenderer.ResolvePromptVersion(request.PromptVersion ?? ResolvePromptVersionSetting(AttachmentSummaryPromptType)); try { - var extractedText = await _textExtractionService.ExtractTextAsync(fileName, fileContent, contentType); + var extractedText = request.ExtractedText; var prompt = OpenAIPromptRenderer.BuildAttachmentSummarySystemPrompt(promptVersion); var attachmentText = string.IsNullOrWhiteSpace(extractedText) ? null : extractedText; if (attachmentText != null) { - _logger.LogDebug("Extracted {TextLength} characters from {FileName}", extractedText.Length, fileName); + _logger.LogDebug("Received {TextLength} extracted characters for {FileName}", attachmentText.Length, fileName); } else { @@ -155,21 +150,20 @@ public async Task GenerateAttachmentSummaryAsync(Atta { name = fileName, contentType, - sizeBytes = fileContent.Length, text = attachmentText }; var attachment = JsonSerializer.Serialize(attachmentPayload, JsonLogOptions); var contentToAnalyze = OpenAIPromptRenderer.BuildAttachmentSummaryUserPrompt(promptVersion, attachment); await LogPromptInputAsync(AttachmentSummaryPromptType, promptVersion, prompt, contentToAnalyze); - var result = await GenerateWithRetryAsync( - () => _openAITransportService.GenerateSummaryAsync( - contentToAnalyze, - prompt, - AttachmentSummaryCompletionTokens, - operationName: AttachmentSummaryPromptType, - promptVersion: promptVersion, - fileName: fileName), + var result = await GenerateWithRetryAsync( + () => _openAITransportService.GenerateSummaryAsync( + contentToAnalyze, + prompt, + AttachmentSummaryCompletionTokens, + operationName: AttachmentSummaryPromptType, + promptVersion: promptVersion, + fileName: fileName), AIProviderPayloadValidator.IsValidAttachmentSummaryText, "attachment summary"); await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput); diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs index 8e83068427..ea461b2d2c 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs @@ -17,6 +17,7 @@ Task HttpAsync( object? body = null, string? authToken = null, (string username, string password)? basicAuth = null, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default); /// diff --git a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs index 3933d93c2f..109c0a816d 100644 --- a/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs +++ b/applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs @@ -113,10 +113,11 @@ public async Task HttpAsync( object? body = null, string? authToken = null, (string username, string password)? basicAuth = null, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { return await SendWithClientAsync( - _httpClient, httpVerb, resource, body, authToken, basicAuth, cancellationToken); + _httpClient, httpVerb, resource, body, authToken, basicAuth, completionOption, cancellationToken); } @@ -137,7 +138,7 @@ public Task HttpAsyncSecured( EnsureMutualTlsClient(certPath, certPassword); return SendWithClientAsync( - _mtlsClient!, httpVerb, resource, body, authToken, basicAuth, cancellationToken); + _mtlsClient!, httpVerb, resource, body, authToken, basicAuth, HttpCompletionOption.ResponseContentRead, cancellationToken); } @@ -191,6 +192,7 @@ private async Task SendWithClientAsync( object? body, string? authToken, (string username, string password)? basicAuth, + HttpCompletionOption completionOption, CancellationToken cancellationToken) { // Build final URL @@ -208,7 +210,7 @@ private async Task SendWithClientAsync( using var requestMessage = BuildRequestMessage(httpVerb, fullUrl, body, authToken, basicAuth); - return await client.SendAsync(requestMessage, ct) + return await client.SendAsync(requestMessage, completionOption, ct) .ConfigureAwait(false); }, cancellationToken); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs new file mode 100644 index 0000000000..ed9f19f4be --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Unity.GrantManager.Intakes; + +/// +/// Stream of a CHEFS file attachment plus its content type. +/// The Content stream owns its underlying temp file; dispose to release. +/// +public sealed class ChefsFileAttachmentStream : IDisposable, IAsyncDisposable +{ + public Stream Content { get; } + public string ContentType { get; } + + public ChefsFileAttachmentStream(Stream content, string contentType) + { + Content = content ?? throw new ArgumentNullException(nameof(content)); + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType; + } + + public static ChefsFileAttachmentStream Empty { get; } = + new(Stream.Null, "application/octet-stream"); + + public void Dispose() => Content.Dispose(); + + public ValueTask DisposeAsync() => Content.DisposeAsync(); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ISubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ISubmissionAppService.cs index 618cf756ac..da04b9c730 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ISubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ISubmissionAppService.cs @@ -33,4 +33,10 @@ public interface ISubmissionAppService : IApplicationService /// File name of the chefs attachment /// BlobDto Task GetChefsFileAttachment(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name); + + /// + /// Get a CHEFS file attachment as a Stream backed by a temp file (deleted on close). + /// Avoids buffering the full file in managed memory. + /// + Task GetChefsFileAttachmentStream(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs index 7d2faae6a8..a00e104e9c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/SubmissionAppService.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -111,6 +112,86 @@ public async Task GetChefsFileAttachment(Guid? formSubmissionId, Guid? return new BlobDto { Name = name, Content = contentBytes, ContentType = contentType }; } + [AllowAnonymous] + public async Task GetChefsFileAttachmentStream(Guid? formSubmissionId, Guid? chefsFileAttachmentId, string name) + { + if (formSubmissionId == null) + { + throw new ApiException(400, "Missing required parameter 'formId' when calling GetSubmission"); + } + + if (chefsFileAttachmentId == null) + { + throw new ApiException(400, "Missing required parameter 'chefsFileAttachmentId' when calling GetFileAttachment"); + } + + ApplicationForm? applicationForm = await GetApplicationFormBySubmissionId(formSubmissionId) ?? throw new ApiException(400, "Missing Form configuration"); + if (applicationForm.ChefsApplicationFormGuid == null) + { + throw new ApiException(400, "Missing CHEFS form Id"); + } + + if (applicationForm.ApiKey == null) + { + throw new ApiException(400, "Missing CHEFS Api Key"); + } + + string chefsApi = await endpointManagementAppService.GetChefsApiBaseUrlAsync(); + string url = $"{chefsApi}/files/{chefsFileAttachmentId}"; + var decryptedApiKey = stringEncryptionService.Decrypt(applicationForm.ApiKey!); + + using var response = await resilientRestClient.HttpAsync( + HttpMethod.Get, + url, + null, + null, + basicAuth: (applicationForm.ChefsApplicationFormGuid!, decryptedApiKey ?? string.Empty), + completionOption: HttpCompletionOption.ResponseHeadersRead + ); + + if (((int)response.StatusCode) != 200) + { + var errorContent = response.Content != null ? await response.Content.ReadAsStringAsync() : string.Empty; + throw new ApiException((int)response.StatusCode, "Error calling GetChefsFileAttachment: " + errorContent, response.ReasonPhrase ?? $"{response.StatusCode}"); + } + + var contentType = response.Content?.Headers?.ContentType?.MediaType ?? "application/octet-stream"; + var extension = !string.IsNullOrEmpty(name) ? Path.GetExtension(Uri.UnescapeDataString(name)) : string.Empty; + var tempPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}{extension}"); + + try + { + await using (var writeStream = new FileStream(tempPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 81920, FileOptions.Asynchronous | FileOptions.SequentialScan)) + await using (var contentStream = await response.Content!.ReadAsStreamAsync()) + { + await contentStream.CopyToAsync(writeStream); + } + + var readStream = new FileStream(tempPath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, FileOptions.Asynchronous | FileOptions.SequentialScan | FileOptions.DeleteOnClose); + return new ChefsFileAttachmentStream(readStream, contentType); + } + catch + { + TryDeleteTempFile(tempPath); + throw; + } + } + + private static void TryDeleteTempFile(string tempPath) + { + try + { + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + catch + { + // Best-effort cleanup; never throw from cleanup path. + } + } + public async Task GetApplicationFormBySubmissionId(Guid? formSubmissionId) { From ee560255f75b966157541dfa79da2ed9db7ab8b6 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 30 Apr 2026 13:40:33 -0700 Subject: [PATCH 02/34] AB#32543 integrate AI reporting page --- .../Pages/AIReporting/Index.cshtml | 173 +++- .../Pages/AIReporting/Index.cshtml.cs | 6 +- .../Pages/AIReporting/Index.css | 1 + .../Pages/AIReporting/Index.js | 765 +++++++++++++++++- 4 files changed, 899 insertions(+), 46 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml index 89dbce3511..541a903c95 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml @@ -1,34 +1,171 @@ -@page +@page @model Unity.GrantManager.Web.Pages.AIReporting.IndexModel -@using Unity.GrantManager.Web.Pages.AIReporting @using Unity.Modules.Shared.Permissions @using Volo.Abp.Features +@inject IFeatureChecker FeatureChecker @section styles { - + @if (await FeatureChecker.IsEnabledAsync("Unity.AIReporting") || User.IsInRole(IdentityConsts.ITAdminRoleName)) + { + + } } -@section scripts -{ - - + +@section scripts { @if (await FeatureChecker.IsEnabledAsync("Unity.AIReporting") || User.IsInRole(IdentityConsts.ITAdminRoleName)) { } } -@inject IFeatureChecker FeatureChecker - + + +
+ +
+
+

What would you like to know?

+
+
+ + +
+
+
+
-
\ No newline at end of file + + +
+ + + +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs index d83d79c390..e86bbb2f06 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml.cs @@ -1,16 +1,16 @@ -using Microsoft.AspNetCore.Mvc.RazorPages; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; using Unity.GrantManager.Integrations; namespace Unity.GrantManager.Web.Pages.AIReporting { public class IndexModel(IEndpointManagementAppService endpointManagementAppService) : PageModel { - public string ReportingAiUrl { get; set; } = string.Empty; + public string ReportingAiApiBaseUrl { get; private set; } = string.Empty; public async Task OnGetAsync() { - ReportingAiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.REPORTING_AI); + ReportingAiApiBaseUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.REPORTING_AI); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css new file mode 100644 index 0000000000..b99ce14729 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css @@ -0,0 +1 @@ +/* =========================================================================== AI Reporting page — port of .working/unity-ai/applications/Unity.AI.Reporting.Frontend Sources: app.css, sidebar.css, sql-explanation.ts inline styles. =========================================================================== *//* Local CSS variables (reference uses Metabase global vars; we inline) */:root { --mb-radius: 8px; --mb-blue-600: #2563eb; --mb-blue-700: #1d4ed8; --mb-teal-600: #0d9488; --mb-gray-200: #e2e8f0; --mb-gray-700: #334155; --mb-red-700: #b91c1c; --mb-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); --mb-font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;}/* ---------- layout shell ---------- */.outer-container { display: grid; grid-template-columns: auto 1fr; /* Use dvh so the layout follows the actual visible viewport (handles browser chrome, address bar, and zoom changes more reliably than 100vh). */ height: calc(100dvh - 6.75rem); min-height: 480px; background: #fff;}.container { display: flex; flex-direction: column; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none;}.container::-webkit-scrollbar { display: none; }/* ---------- sidebar ---------- */.sidebar { border-right: 1px solid #e9ecef; display: flex; flex-direction: column; height: 100%; width: 220px; background: #fff; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);}.sidebar-header { padding: 16px 12px 6px 12px; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0;}.sidebar-header h2 { margin: 14px 0 0 14px; font-size: 0.8rem; font-weight: 500; color: #6c757d; text-align: left;}.new-chat-btn { background: transparent; color: #333; border: none; padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.9rem; font-weight: 500; display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;}.new-chat-btn:hover,.chat-item:hover,.chat-item.active { background-color: #e9ecef;}.new-chat-icon { font-size: 16px;}.sidebar-content { flex: 1; overflow-y: auto; padding: 0;}.loading,.empty-state { padding: 20px; text-align: center; color: #6c757d; font-style: italic; font-size: 0.9rem;}.chat-list { padding: 0;}.chat-item { margin: 6px 12px; padding: 8px 14px; cursor: pointer; position: relative; display: flex; flex-direction: column; border-radius: 8px;}.chat-title { font-weight: 400; color: #333; font-size: 0.9rem; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 30px;}.delete-chat-btn { position: absolute; top: 50%; right: 12px; transform: translateY(-50%); background: none; border: none; cursor: pointer; border-radius: 4px; opacity: 0; font-size: 16px; color: #6c757d; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; line-height: 1; padding: 0;}.chat-item:hover .delete-chat-btn { opacity: 1; }.delete-chat-btn:hover { background-color: #d9d9d9; color: #333;}.sidebar-footer { border-top: 1px solid #e9ecef; padding: 12px; display: flex; justify-content: flex-end; flex-shrink: 0; background: #fff;}.footer-btn { background: transparent; border: 1px solid transparent; border-radius: 8px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #6c757d; transition: all 0.2s ease;}.footer-btn:hover { background-color: #f8f9fa; border-color: #dee2e6; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);}.info-btn:hover { background-color: #d1ecf1; border-color: #17a2b8; color: #0c5460;}/* ---------- empty welcome ---------- */.empty-chat-container { display: flex; align-items: center; justify-content: center; height: 100%; flex: 1;}.welcome-content { text-align: center; width: 100%; max-width: 800px;}.welcome-title { font-size: 2rem; color: #333; margin: 0 0 40px 0; line-height: 1.2; font-weight: 500;}/* ---------- chat layout ---------- */.chat-container { display: grid; grid-template-rows: 1fr auto; height: 100%; min-height: 0; flex: 1;}.turns { height: 100%; overflow: hidden; /* Scale padding gracefully across viewport widths — 15vw on big screens, but capped so the chat doesn't get squeezed on smaller laptops/zoom levels. */ padding: 0 clamp(16px, 15vw, 240px); position: relative; min-height: 0;}.turn { display: flex; flex-direction: column; height: 100%; box-sizing: border-box; padding: 24px 0;}/* ---------- bubbles ---------- */.bubble { border-radius: 18px; padding: 12px 16px; font-size: 16px; box-shadow: var(--mb-shadow-sm); border: 1px solid var(--mb-gray-200); background: #fff;}.bubble.bot { width: 100%; position: relative; height: 100%; overflow-y: auto; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; padding: 8px; background: radial-gradient(ellipse at center, rgba(66, 153, 225, 0.08) 0%, rgba(255, 255, 255, 0.95) 70%);}.bubble.bot::-webkit-scrollbar { display: none; }.bubble.bot > div:not(.spinner-center) { height: 100%; align-self: flex-start; display: flex; flex-direction: column;}.bot-inner { opacity: 0; transition: opacity 0.6s; padding: 0; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center;}.bot-inner.loaded { opacity: 1; }/* ---------- ask row ---------- */.ask-row-container { margin: 0 clamp(12px, 10vw, 160px) 16px; position: relative; padding: 20px 20px 12px 20px; background: #fff; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); border-radius: 40px;}.welcome-ask-row { margin: 0; padding: 10px 10px 10px 24px;}.ask-row { display: flex; gap: 12px;}.ask-row-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 16px;}.bottom-left-controls { display: flex; align-items: center; gap: 12px;}.ask-row input[type="text"] { flex: 1 1 0; border: none; border-radius: var(--mb-radius); font-size: 16px; padding: 8px 4px; background: transparent;}.ask-row input[type="text"]:focus { outline: none; }.ask-question-btn { background: var(--mb-blue-600); color: #fff; border: none; border-radius: 50%; cursor: pointer; transition: background-color 0.12s, box-shadow 0.12s; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.ask-question-btn:hover { background: var(--mb-blue-700); box-shadow: var(--mb-shadow-sm);}.ask-question-btn svg { color: #fff; }/* ---------- action buttons ---------- */.action-buttons { display: flex; align-items: center; gap: 8px;}.action-button { display: flex; align-items: center; justify-content: center; padding: 8px; background: transparent; border: 1px solid transparent; border-radius: 20px; font-size: 14px; font-weight: 500; color: #495057; cursor: pointer; transition: all 0.2s ease; white-space: nowrap; width: 32px; height: 32px; box-sizing: border-box;}.action-button:hover:not(:disabled) { background: #f8f9fa; border-color: #dee2e6;}.action-button:disabled { cursor: not-allowed; opacity: 0.4;}.action-button.delete-action:hover:not(:disabled) { background: #f8d7da; border-color: #f5c6cb; color: #721c24;}.action-button svg { width: 16px; height: 16px; }.nav-controls { display: flex; align-items: center; gap: 8px;}.action-button.nav-button:disabled { background: transparent; border-color: transparent; color: #adb5bd; opacity: 0.6;}.turn-counter { font-size: 0.875rem; color: #6c757d; font-weight: 500; min-width: 60px; text-align: center; white-space: nowrap;}/* ---------- loading state ---------- */.sql-loader-container { flex: 1 1 auto; display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden; position: relative; width: 100%; height: 100%; min-height: 400px;}.loading-animation-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; padding: 20px; background: rgba(255, 255, 255, 0.9); border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10;}.loading-dots { display: flex; gap: 8px; align-items: center;}.loading-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--mb-blue-600); animation: pulse 1.5s ease-in-out infinite;}.loading-dot:nth-child(1) { animation-delay: 0s; }.loading-dot:nth-child(2) { animation-delay: 0.2s; }.loading-dot:nth-child(3) { animation-delay: 0.4s; }@keyframes pulse { 0%, 80%, 100% { transform: scale(0.8); opacity: 0.6; } 40% { transform: scale(1.2); opacity: 1; }}.loading-text { font-size: 16px; font-weight: 500; color: #666; text-align: center;}/* ---------- failure state ---------- */.failure-container { padding: 24px; height: 100%; box-sizing: border-box; position: relative;}.failure-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; padding: 20px; background: rgba(255, 255, 255, 0.9); border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10; max-width: 480px;}.failure-icon { font-size: 32px; flex-shrink: 0; }.failure-title { font-size: 18px; font-weight: 600; color: #c53030; margin: 0;}.failure-message { font-size: 14px; color: #718096; line-height: 1.5; margin: 0; text-align: center;}.failure-actions { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 4px;}.retry-btn { padding: 8px 16px; font-size: 14px; font-weight: 500; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; background: #3182ce; color: #fff;}.retry-btn:hover { background: #2c5aa0; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);}.retry-btn--disabled,.retry-btn--disabled:hover { background: #64748b; color: #fff; cursor: not-allowed; transform: none; box-shadow: none;}.retry-hint { font-size: 0.9rem; color: #666; margin: 0;}/* ---------- semantic cache badge ---------- */.cache-badge { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; padding: 4px 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 0.8rem; color: #334155; margin-bottom: 8px; cursor: default; user-select: none; flex-shrink: 0;}.cache-badge-hint { color: #64748b; }.cache-fresh-btn { background: none; border: none; padding: 0; margin-left: 4px; font-size: inherit; color: #2563eb; text-decoration: underline; cursor: pointer;}.cache-fresh-btn:hover:not(:disabled) { color: #1d4ed8; }.cache-fresh-btn:disabled { color: #94a3b8; cursor: not-allowed; text-decoration: none;}/* ---------- Metabase view button ---------- */.metabase-view-container { display: flex; justify-content: center; align-items: center; flex: 1;}.metabase-view-btn { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 16px 32px; background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%); color: #fff; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);}.metabase-view-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(66, 153, 225, 0.4); background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); color: #fff;}.metabase-view-btn:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);}.metabase-view-btn svg { flex-shrink: 0; transition: all 0.3s ease; }.metabase-view-btn:hover svg { transform: translateX(2px) translateY(-2px); }.metabase-view-btn span { position: relative; z-index: 1; letter-spacing: 0.3px; }/* ---------- SQL panel (overlay) ---------- */.sql-panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 5; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; display: flex; flex-direction: column; justify-content: flex-start; box-sizing: border-box; padding: 8px 16px;}.sql-panel::-webkit-scrollbar { display: none; }.sql-panel-header { padding: 8px 0 16px 0; font-weight: 600; font-size: 18px; color: var(--mb-gray-700); flex-shrink: 0;}.sql-code { padding: 12px 16px; margin: 0; font-family: 'Fira Mono', Consolas, 'Courier New', monospace; font-size: 14px; line-height: 1.35; color: #1e1e1e; background: #fff; white-space: pre-wrap; overflow-y: auto; border-radius: 8px; border: 1px solid var(--mb-gray-200); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); flex: 1;}/* ---------- SQL explanation bubble (typewriter) ---------- */.sql-explanation-bubble { position: relative; background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 12px; padding: 0; margin: 12px 8px 8px 8px; font-size: 0.85em; color: #075985; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); animation: slideIn 0.3s ease-out; flex-shrink: 0;}@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); }}.bubble-content { padding: 10px 14px; line-height: 1.4;}.bubble-tail { position: absolute; top: -6px; left: 30px; width: 12px; height: 12px; background: #f0f9ff; border-left: 1px solid #bae6fd; border-top: 1px solid #bae6fd; transform: rotate(45deg);}.cursor { animation: blink 1s infinite; font-weight: normal; opacity: 0.8; color: #075985;}@keyframes blink { 0%, 50% { opacity: 0.8; } 51%, 100% { opacity: 0; }}/* ---------- responsive ---------- */@media (max-width: 768px) { .outer-container { grid-template-columns: 1fr; } .sidebar { width: 100%; } .turns { padding: 0 16px; } .ask-row-container { margin-left: 12px; margin-right: 12px; }} \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js index 1d55598c3f..33942eedcc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js @@ -1,38 +1,753 @@ -(async () => { - const token = await unity.grantManager.identity.jwtToken.generateJWTToken(); - const iframe = document.createElement('iframe'); +/* ========================================================================== + Unity AI Reporting — vanilla JS port of .working/unity-ai (Angular). + ========================================================================== */ +(function () { + 'use strict'; - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.border = 'none'; + // ─── DOM refs ─────────────────────────────────────────────────────────── + const root = document.getElementById('ai-reporting-root'); + if (!root) return; - const targetOrigin = new URL(window.reportingAiUrl).origin; + const chatList = document.getElementById('chat-list'); + const emptyState = document.getElementById('empty-state'); + const chatContainer = document.getElementById('chat-container'); + const turnsContainer = document.getElementById('turns-container'); + const questionEmpty = document.getElementById('question-input-empty'); + const questionActive = document.getElementById('question-input-active'); + const navControls = document.getElementById('nav-controls'); + const turnCounter = document.getElementById('turn-counter'); + const btnPrev = document.getElementById('btn-prev-turn'); + const btnNext = document.getElementById('btn-next-turn'); + const btnMetabase = document.getElementById('btn-metabase'); + const btnSql = document.getElementById('btn-sql'); + const btnExplain = document.getElementById('btn-explain'); + const btnDeleteQ = document.getElementById('btn-delete-question'); - // Listen for "READY" message from iframe before sending auth token - const messageHandler = (event) => { - if (event.origin !== targetOrigin) return; - if (event.data?.type === 'READY') { + const apiBase = (window.reportingAiApiBaseUrl || '').replace(/\/+$/, '') + '/api'; + const MAX_RETRIES = 2; + + // ─── State ────────────────────────────────────────────────────────────── + let turnIdSeq = 0; + const newTurnId = () => `turn-${++turnIdSeq}`; + + const state = { + conversation: [], // Turn[] + currentChatId: null, + currentTurnIndex: 0, + chats: [], + }; + + let resizeTimer = null; + + // ─── Utilities ────────────────────────────────────────────────────────── + const escapeHtml = (v) => String(v ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + + const isLoading = () => state.conversation.some(t => t.safeUrl === 'loading'); + const findTurn = (id) => state.conversation.find(t => t.id === id); + + const notify = abp.notify; + + // Fresh turn (matches Turn interface from reference) + const createTurn = (question, retryCount = 0) => ({ + id: newTurnId(), + question, + embed: null, + safeUrl: 'loading', // 'loading' | 'failure' | null (success) + iframeLoaded: false, + sqlPanelOpen: false, + sql_explanation_visible: false, + sql_explanation_text: '', // local rendered text (typewriter) + sqlExplanationStreaming: false, + errorType: null, + errorMessage: null, + errorDetail: null, + retryCount, + canRetry: true, + }); + + // ─── API ──────────────────────────────────────────────────────────────── + const authHeader = async () => { + const token = await unity.grantManager.identity.jwtToken.generateJWTToken(); + return { Authorization: `Bearer ${token}` }; + }; + + const apiFetch = async (path, options = {}) => { + const headers = { + 'Content-Type': 'application/json', + ...(await authHeader()), + ...(options.headers || {}), + }; + const response = await fetch(`${apiBase}${path}`, { ...options, headers }); + let body = null; + try { body = await response.json(); } catch { /* no body */ } + if (!response.ok) { + const err = new Error(body?.message || body?.error || `Request failed: ${response.status}`); + err.status = response.status; + err.errorType = body?.error_type ?? null; + err.errorDetail = body?.detail ?? null; + throw err; + } + return body; + }; + + // Specific endpoints (mirror api.service.ts) + const api = { + ask: (question, conversation, isRetry, retryErrorType, retryErrorDetail) => { + const payload = { question, conversation, is_retry: !!isRetry }; + if (isRetry && retryErrorType) payload.retry_error_type = retryErrorType; + if (isRetry && retryErrorDetail) payload.retry_error_detail = retryErrorDetail; + return apiFetch('/ask', { method: 'POST', body: JSON.stringify(payload) }); + }, + deleteCard: (cardId) => apiFetch('/delete', { method: 'POST', body: JSON.stringify({ card_id: cardId }) }), + explainSql: (sql) => apiFetch('/explain_sql', { method: 'POST', body: JSON.stringify({ sql }) }), + getChats: () => apiFetch('/chats', { method: 'POST', body: '{}' }), + getChat: (id) => apiFetch(`/chats/${encodeURIComponent(id)}`, { method: 'POST', body: '{}' }), + saveChat: (chatId, conversation, title) => + apiFetch('/chats/save', { method: 'POST', body: JSON.stringify({ chat_id: chatId, conversation, title }) }), + deleteChat: (id) => apiFetch(`/chats/${encodeURIComponent(id)}`, { method: 'DELETE', body: '{}' }), + getMetabaseUrl:() => apiFetch('/metabase-url', { method: 'GET' }), + }; + + // ─── Rendering ────────────────────────────────────────────────────────── + + // Build the inner HTML of a turn's bot bubble (just the bubble contents). + const renderTurnInner = (turn) => { + // Loading + if (turn.safeUrl === 'loading') { + return ` +
+
+
+
+
+
+
+
Generating Report...
+
+
`; + } + + // Failure + if (turn.safeUrl === 'failure') { + const icon = + turn.errorType === 'rate_limit' ? '⏳' : + turn.errorType === 'connection_error' ? '🔌' : + '⚠️'; + const title = + turn.errorType === 'rate_limit' ? 'Service Busy' : + turn.errorType === 'connection_error' ? 'Connection Error' : + turn.errorType === 'ai_failure' ? 'Unable to Generate Report' : + turn.errorType === 'server_error' ? 'Something Went Wrong' : + 'Something Went Wrong'; + const canRetry = (turn.retryCount ?? 0) < MAX_RETRIES && !!turn.canRetry; + const hint = turn.errorType === 'ai_failure' + ? 'Try rephrasing your question or adding more detail.' + : 'Please start a new question.'; + const actions = canRetry + ? `` + : ` +

${escapeHtml(hint)}

`; + return ` +
+
+
${icon}
+
${escapeHtml(title)}
+
${escapeHtml(turn.errorMessage || '')}
+
${actions}
+
+
`; + } + + // Success (safeUrl === null) + const embed = turn.embed || {}; + + // SQL explanation typewriter bubble + const showCursor = turn.sqlExplanationStreaming || (!turn.sql_explanation_text && turn.sql_explanation_visible); + const explanationHtml = turn.sql_explanation_visible + ? `
+
+ ${escapeHtml(turn.sql_explanation_text || '')} + ${showCursor ? '' : ''} +
+
+
` + : ''; + + // Cache badge + const cacheBadgeHtml = embed.cache_hit_type === 'llm_judge_hit' + ? `
+ Based on a similar previous question. + ${embed.cache_original_query + ? `Previous question: “${escapeHtml(embed.cache_original_query)}”` + : 'Check if this matches what you meant.'} + +
` + : ''; + + // Metabase view button + const metabaseHtml = ` +
+ +
`; + + // SQL panel overlay + const sqlPanelHtml = turn.sqlPanelOpen + ? `
+
Generated SQL
+
${escapeHtml(embed.SQL || '')}
+
` + : ''; + + return ` +
+ ${explanationHtml} + ${cacheBadgeHtml} + ${metabaseHtml} + ${sqlPanelHtml} +
`; + }; + + const renderTurnElement = (turn) => ` +
+
+ ${renderTurnInner(turn)} +
+
`; + + const renderConversation = () => { + const has = state.conversation.length > 0; + emptyState.hidden = has; + chatContainer.hidden = !has; + turnsContainer.innerHTML = state.conversation.map(renderTurnElement).join(''); + syncControls(); + }; + + // Re-render only one bubble (avoids destroying scroll/focus elsewhere). + const updateBubble = (turnId) => { + const turn = findTurn(turnId); + if (!turn) return; + const bubble = turnsContainer.querySelector(`[data-bubble-id="${CSS.escape(turnId)}"]`); + if (bubble) bubble.innerHTML = renderTurnInner(turn); + }; + + const syncControls = () => { + const len = state.conversation.length; + const idx = state.currentTurnIndex; + const current = state.conversation[idx] || null; + const isSuccess = current?.safeUrl === null; + const hasSql = isSuccess && !!current?.embed?.SQL; + + turnCounter.textContent = len ? `${idx + 1} / ${len}` : '0 / 0'; + navControls.hidden = len <= 1; + btnPrev.disabled = idx <= 0; + btnNext.disabled = idx >= len - 1; + + btnMetabase.disabled = !isSuccess; + btnSql.disabled = !hasSql; + btnExplain.disabled = !hasSql; + btnDeleteQ.disabled = !isSuccess; + }; + + // Smooth scroll to a turn (matches reference scrollToTurn). + const scrollToTurn = (index) => { + const turns = turnsContainer.querySelectorAll('.turn'); + const el = turns[index]; + if (!el) return; + const containerRect = turnsContainer.getBoundingClientRect(); + const turnRect = el.getBoundingClientRect(); + const top = turnsContainer.scrollTop + (turnRect.top - containerRect.top); + turnsContainer.scrollTo({ top, behavior: 'smooth' }); + }; + + // ─── Sidebar ──────────────────────────────────────────────────────────── + const renderChats = () => { + if (!state.chats.length) { + chatList.innerHTML = '
No reports yet. Start a new one!
'; + return; + } + chatList.innerHTML = state.chats.map(c => ` +
+
${escapeHtml(c.title || 'Untitled report')}
+ +
`).join(''); + }; + + const loadChats = async () => { + try { + const chats = await api.getChats(); + state.chats = Array.isArray(chats) ? chats : []; + renderChats(); + } catch (err) { + console.error('Failed to load chats:', err); + state.chats = []; + chatList.innerHTML = '
Unable to load reports.
'; + } + }; + + // ─── Save / load chat ─────────────────────────────────────────────────── + const saveChat = async () => { + if (state.conversation.length === 0) return; + const turnsToSave = state.conversation.filter(t => t.safeUrl !== 'loading'); + if (!turnsToSave.length) return; + + const mostRecent = turnsToSave[turnsToSave.length - 1]; + const title = mostRecent?.embed?.title || state.conversation[0]?.question || 'New Report'; + const conversation = turnsToSave.map(t => ({ + question: t.question, + embed: t.embed, + safeUrl: t.safeUrl, + iframeLoaded: t.iframeLoaded, + sqlPanelOpen: t.sqlPanelOpen, + sql_explanation_visible: t.sql_explanation_visible, + errorType: t.errorType, + errorMessage: t.errorMessage, + errorDetail: t.errorDetail, + retryCount: t.retryCount, + canRetry: t.canRetry, + })); + + try { + const res = await api.saveChat(state.currentChatId, conversation, title); + if (res?.chat_id) state.currentChatId = res.chat_id; + await loadChats(); + } catch (err) { + console.error('Failed to save chat:', err); + notify.error('Failed to save report. Please try again.'); + } + }; + + const loadChat = async (chatId) => { + try { + const data = await api.getChat(chatId); + const raw = Array.isArray(data?.conversation) ? data.conversation : []; + state.conversation = raw.map(r => ({ + id: newTurnId(), + question: r.question || '', + embed: r.embed || null, + safeUrl: r.safeUrl ?? null, + iframeLoaded: true, + sqlPanelOpen: r.sqlPanelOpen ?? false, + sql_explanation_visible: r.sql_explanation_visible ?? false, + sql_explanation_text: r.embed?.sql_explanation || '', + sqlExplanationStreaming: false, + errorType: r.errorType ?? null, + errorMessage: r.errorMessage ?? null, + errorDetail: r.errorDetail ?? null, + retryCount: r.retryCount ?? 0, + canRetry: r.canRetry ?? true, + })); + state.currentChatId = chatId; + state.currentTurnIndex = Math.max(0, state.conversation.length - 1); + renderConversation(); + renderChats(); + requestAnimationFrame(() => scrollToTurn(state.currentTurnIndex)); + } catch (err) { + console.error('Failed to load chat:', err); + notify.error('Failed to load chat. Please try again.'); + } + }; + + const newChat = () => { + state.conversation = []; + state.currentChatId = null; + state.currentTurnIndex = 0; + renderConversation(); + renderChats(); + questionEmpty?.focus(); + }; + + // ─── Ask question (and retry / fresh answer) ──────────────────────────── + const askQuestion = async (text, opts = {}) => { + const trimmed = (text || '').trim(); + if (!trimmed) { + notify.info('Please enter a question.'); + return; + } + if (isLoading()) return; + + const { retryCount = 0, isRetry = false, retryErrorType = null, retryErrorDetail = null } = opts; + + const turn = createTurn(trimmed, retryCount); + state.conversation.push(turn); + state.currentTurnIndex = state.conversation.length - 1; + renderConversation(); + requestAnimationFrame(() => scrollToTurn(state.currentTurnIndex)); + + if (questionEmpty) questionEmpty.value = ''; + if (questionActive) questionActive.value = ''; + + // Build conversation context (success turns only, excluding the new one). + const context = state.conversation + .slice(0, -1) + .filter(t => t.safeUrl === null) + .map(t => ({ question: t.question, embed: t.embed })); + + try { + const result = await api.ask(trimmed, context, isRetry, retryErrorType, retryErrorDetail); + const t = findTurn(turn.id); + if (!t) return; + t.embed = { + card_id: result?.card_id, + x_field: result?.x_field || '', + y_field: result?.y_field || '', + title: result?.title || trimmed, + visualization_options: result?.visualization_options || [], + SQL: result?.SQL || '', + sql_explanation: result?.sql_explanation || '', + tokens: result?.tokens || null, + from_cache: result?.from_cache, + cache_similarity: result?.cache_similarity, + cache_hit_type: result?.cache_hit_type || null, + cache_original_query: result?.cache_original_query || null, + }; + t.safeUrl = null; + t.iframeLoaded = true; + updateBubble(t.id); + syncControls(); + await saveChat(); + } catch (err) { + console.error('Failed to process question:', err); + const t = findTurn(turn.id); + if (!t) return; + t.iframeLoaded = true; + t.safeUrl = 'failure'; + t.errorDetail = err?.errorDetail ?? null; + + const status = err?.status; + const errorType = err?.errorType; + const message = err?.message; + + if (errorType === 'rate_limit' || status === 429) { + t.errorType = 'rate_limit'; + t.errorMessage = message || 'Rate limit exceeded. Please wait a moment and try again.'; + t.canRetry = true; + } else if (errorType === 'connection_error' || status === 503) { + t.errorType = 'connection_error'; + t.errorMessage = message || 'Connection error. The service may be temporarily unavailable.'; + t.canRetry = true; + } else if (errorType === 'ai_failure' || status === 422) { + t.errorType = 'ai_failure'; + t.errorMessage = message || "I couldn't generate a report from that question."; + t.canRetry = false; + } else if (errorType === 'server_error' || (status && status >= 500)) { + t.errorType = 'server_error'; + t.errorMessage = message || 'Something went wrong on our end. Please try again.'; + t.canRetry = true; + } else { + t.errorType = 'unknown'; + t.errorMessage = message || 'Something went wrong. Please try again.'; + t.canRetry = true; + } + + updateBubble(t.id); + syncControls(); + } + }; + + const retryQuestion = (turnId) => { + const turn = findTurn(turnId); + if (!turn) return; + const nextRetry = (turn.retryCount ?? 0) + 1; + const errType = turn.errorType; + const errDetail = turn.errorDetail; + const question = turn.question; + const idx = state.conversation.findIndex(t => t.id === turnId); + state.conversation.splice(idx, 1); + if (state.currentTurnIndex >= state.conversation.length) { + state.currentTurnIndex = Math.max(0, state.conversation.length - 1); + } + askQuestion(question, { retryCount: nextRetry, isRetry: true, retryErrorType: errType, retryErrorDetail: errDetail }); + }; + + const getFreshAnswer = (turnId) => { + if (isLoading()) return; + const turn = findTurn(turnId); + if (!turn) return; + const question = turn.question; + const idx = state.conversation.findIndex(t => t.id === turnId); + state.conversation.splice(idx, 1); + if (state.currentTurnIndex >= state.conversation.length) { + state.currentTurnIndex = Math.max(0, state.conversation.length - 1); + } + // retryCount=1 + isRetry=true causes backend to skip the semantic cache. + askQuestion(question, { retryCount: 1, isRetry: true }); + }; + + // ─── SQL panel ────────────────────────────────────────────────────────── + const toggleSqlPanel = (turnId) => { + const turn = findTurn(turnId); + if (!turn) return; + turn.sqlPanelOpen = !turn.sqlPanelOpen; + updateBubble(turnId); + }; + + // ─── SQL explanation (typewriter) ─────────────────────────────────────── + const streamExplanation = (turnId, text) => { + const turn = findTurn(turnId); + if (!turn) return; + turn.sqlExplanationStreaming = true; + turn.sql_explanation_text = ''; + let i = 0; + const interval = setInterval(() => { + const t = findTurn(turnId); + if (!t || !t.sql_explanation_visible) { + clearInterval(interval); + if (t) t.sqlExplanationStreaming = false; + return; + } + if (i < text.length) { + t.sql_explanation_text += text[i++]; + updateBubble(turnId); + } else { + t.sqlExplanationStreaming = false; + clearInterval(interval); + updateBubble(turnId); + } + }, 10); + }; + + const generateSqlExplanation = async (turnId) => { + const turn = findTurn(turnId); + if (!turn?.embed?.SQL) return; + + // Toggle visibility (matches reference) + turn.sql_explanation_visible = !turn.sql_explanation_visible; + updateBubble(turnId); + + if (turn.sql_explanation_visible && !turn.embed.sql_explanation) { try { - iframe.contentWindow.postMessage( - { type: 'AUTH_TOKEN', token: token }, - targetOrigin - ); - } catch (error) { - console.error('Failed to send authentication token to AI Reporting iframe:', error); + const res = await api.explainSql(turn.embed.SQL); + const t = findTurn(turnId); + if (!t || !t.sql_explanation_visible) return; + t.embed.sql_explanation = res?.explanation || ''; + if (t.embed.tokens && res?.tokens) { + t.embed.tokens.prompt_tokens += res.tokens.prompt_tokens || 0; + t.embed.tokens.completion_tokens += res.tokens.completion_tokens || 0; + t.embed.tokens.total_tokens += res.tokens.total_tokens || 0; + } + streamExplanation(turnId, t.embed.sql_explanation); + } catch (err) { + console.error('Failed to generate SQL explanation:', err); + const status = err?.status; + let msg = 'Failed to generate SQL explanation. '; + if (status === 429) msg += 'Rate limit exceeded. Please try again later.'; + else if (status >= 500) msg += 'Server error. Please try again.'; + else msg += 'Please try again or contact support if the issue persists.'; + notify.error(msg); + const t = findTurn(turnId); + if (t) { + t.embed.sql_explanation = 'Unable to generate explanation at this time.'; + t.sql_explanation_text = t.embed.sql_explanation; + updateBubble(turnId); + } } - window.removeEventListener('message', messageHandler); + } else if (turn.sql_explanation_visible && turn.embed.sql_explanation && !turn.sql_explanation_text) { + // Already have the text from a saved chat — render it directly without streaming. + turn.sql_explanation_text = turn.embed.sql_explanation; + updateBubble(turnId); } + + await saveChat(); }; - window.addEventListener('message', messageHandler); + // ─── Metabase redirect ────────────────────────────────────────────────── + const isValidCardId = (id) => Number.isInteger(Number(id)) && Number(id) > 0 && Number(id) <= 999999999; - iframe.onerror = () => { - console.error('Failed to load AI Reporting iframe'); - window.removeEventListener('message', messageHandler); + const isValidRedirectUrl = (full, base) => { + try { + const f = new URL(full); + const b = new URL(base); + if (f.origin !== b.origin) return false; + return /^\/question\/\d+$/.test(f.pathname); + } catch { return false; } }; - iframe.src = window.reportingAiUrl; - document.getElementById('container').appendChild(iframe); -})(); + const redirectToMetabase = async (cardId) => { + if (!isValidCardId(cardId)) { + notify.error('Unable to open Metabase — invalid card ID'); + return; + } + try { + const res = await api.getMetabaseUrl(); + const baseUrl = res?.metabase_url; + if (!baseUrl) { + notify.error('Unable to open Metabase — invalid configuration'); + return; + } + const full = `${baseUrl.replace(/\/+$/, '')}/question/${cardId}`; + if (!isValidRedirectUrl(full, baseUrl)) { + notify.error('Unable to open Metabase — security validation failed'); + return; + } + window.open(full, '_blank', 'noopener,noreferrer'); + } catch (err) { + console.error('Error redirecting to Metabase:', err); + notify.error('Unable to open Metabase'); + } + }; + + // ─── Delete question / chat ───────────────────────────────────────────── + const deleteQuestion = async (turnId) => { + const turn = findTurn(turnId); + if (!turn) return; + const ok = await abp.message.confirm('Are you sure you want to delete this question? This action cannot be undone.', 'Delete Question'); + if (!ok) return; + + try { + if (turn.embed?.card_id) { + await api.deleteCard(turn.embed.card_id); + } + const idx = state.conversation.findIndex(t => t.id === turnId); + if (idx >= 0) state.conversation.splice(idx, 1); + + // Last turn → delete entire chat. + if (state.conversation.length === 0 && state.currentChatId) { + try { + await api.deleteChat(state.currentChatId); + state.currentChatId = null; + await loadChats(); + notify.success('Report deleted successfully'); + } catch (e) { + console.error('Error deleting empty chat:', e); + notify.error('Failed to delete report. Please try again.'); + } + renderConversation(); + return; + } + + // Adjust currentTurnIndex + if (idx <= state.currentTurnIndex && state.currentTurnIndex > 0) { + state.currentTurnIndex = Math.max(0, state.currentTurnIndex - 1); + } else if (state.currentTurnIndex >= state.conversation.length) { + state.currentTurnIndex = Math.max(0, state.conversation.length - 1); + } + + await saveChat(); + renderConversation(); + requestAnimationFrame(() => scrollToTurn(state.currentTurnIndex)); + notify.success('Question deleted successfully'); + } catch (err) { + console.error('Error deleting question:', err); + notify.error('Failed to delete question. Please try again.'); + } + }; + + const deleteChatPrompt = async (chatId) => { + const chat = state.chats.find(c => c.id === chatId); + const ok = await abp.message.confirm('Are you sure you want to delete this chat? This action cannot be undone.', 'Delete Chat'); + if (!ok) return; + try { + await api.deleteChat(chatId); + state.chats = state.chats.filter(c => c.id !== chatId); + if (state.currentChatId === chatId) newChat(); + else renderChats(); + notify.success(`Report${chat?.title ? ` "${chat.title}"` : ''} deleted successfully`); + } catch (err) { + console.error('Failed to delete chat:', err); + notify.error('Failed to delete report. Please try again.'); + } + }; + + // ─── Helpers for current turn ─────────────────────────────────────────── + const currentTurn = () => state.conversation[state.currentTurnIndex] || null; + // ─── Event delegation ─────────────────────────────────────────────────── + document.body.addEventListener('click', async (event) => { + const actionEl = event.target.closest('[data-action]'); + if (!actionEl) return; + const action = actionEl.getAttribute('data-action'); + switch (action) { + // sidebar + case 'new-chat': newChat(); break; + case 'select-chat': loadChat(actionEl.getAttribute('data-chat-id')); break; + case 'delete-chat': + event.stopPropagation(); + deleteChatPrompt(actionEl.getAttribute('data-chat-id')); + break; + + // ask row + case 'ask': { + const input = actionEl.closest('.ask-row-container')?.querySelector('input[type="text"]'); + askQuestion(input?.value || ''); + break; + } + + // toolbar (operates on current turn) + case 'metabase': { + const t = currentTurn(); + if (t?.embed?.card_id) redirectToMetabase(t.embed.card_id); + break; + } + case 'toggle-sql': { + const t = currentTurn(); + if (t) toggleSqlPanel(t.id); + break; + } + case 'explain-sql': { + const t = currentTurn(); + if (t) generateSqlExplanation(t.id); + break; + } + case 'delete-question': { + const t = currentTurn(); + if (t) deleteQuestion(t.id); + break; + } + case 'prev-turn': + if (state.currentTurnIndex > 0) { + state.currentTurnIndex--; + syncControls(); + scrollToTurn(state.currentTurnIndex); + } + break; + case 'next-turn': + if (state.currentTurnIndex < state.conversation.length - 1) { + state.currentTurnIndex++; + syncControls(); + scrollToTurn(state.currentTurnIndex); + } + break; + + // in-bubble actions + case 'metabase-turn': { + const t = findTurn(actionEl.getAttribute('data-turn-id')); + if (t?.embed?.card_id) redirectToMetabase(t.embed.card_id); + break; + } + case 'fresh-answer': getFreshAnswer(actionEl.getAttribute('data-turn-id')); break; + case 'retry-question': retryQuestion(actionEl.getAttribute('data-turn-id')); break; + } + }); + + // Enter key submits + [questionEmpty, questionActive].forEach((input) => { + input?.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); askQuestion(input.value); } + }); + }); + + // Resize listener — keep current turn in view + window.addEventListener('resize', () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + if (state.conversation.length > 0) scrollToTurn(state.currentTurnIndex); + }, 150); + }); + + // ─── Init ─────────────────────────────────────────────────────────────── + renderConversation(); + loadChats(); +})(); From 0b7a7c64f2551844bbe9a8780d2994e6c2ed0417 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 1 May 2026 14:37:17 -0700 Subject: [PATCH 03/34] AB#32452 refine prompt tools layout and access --- .../AIPromptToolViewOptionsProvider.cs | 29 +++-- .../IAIPromptToolViewOptionsProvider.cs | 6 +- .../Pages/GrantApplications/Details.cshtml | 117 ++++++++---------- .../Pages/GrantApplications/Details.cshtml.cs | 10 +- .../Pages/GrantApplications/Details.css | 84 +++++++++++++ .../Pages/GrantApplications/Details.js | 100 +++++---------- 6 files changed, 204 insertions(+), 142 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolViewOptionsProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolViewOptionsProvider.cs index eb99661503..51d7221147 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolViewOptionsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolViewOptionsProvider.cs @@ -1,16 +1,31 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; -using System; +using System.Threading.Tasks; +using Unity.Modules.Shared.Permissions; using Volo.Abp.DependencyInjection; +using Volo.Abp.Security.Claims; namespace Unity.AI.Web.PromptTools; -public class AIPromptToolViewOptionsProvider( - IWebHostEnvironment webHostEnvironment, - IConfiguration configuration) : IAIPromptToolViewOptionsProvider, ITransientDependency +public class AIPromptToolAccessProvider( + IAuthorizationService authorizationService, + ICurrentPrincipalAccessor currentPrincipalAccessor, + IConfiguration configuration) : IAIPromptToolAccessProvider, ITransientDependency { - public bool IsDevPromptControlsEnabled => - string.Equals(webHostEnvironment.EnvironmentName, "Development", StringComparison.OrdinalIgnoreCase); + public async Task CanViewPromptToolsAsync() + { + var principal = currentPrincipalAccessor.Principal; + if (principal?.Identity?.IsAuthenticated != true) + { + return false; + } + + var authorizationResult = await authorizationService.AuthorizeAsync( + principal, + IdentityConsts.ITOperationsPolicyName); + + return authorizationResult.Succeeded; + } public string DefaultPromptVersion { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolViewOptionsProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolViewOptionsProvider.cs index c9d75a4883..80d0560ba7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolViewOptionsProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolViewOptionsProvider.cs @@ -1,7 +1,9 @@ +using System.Threading.Tasks; + namespace Unity.AI.Web.PromptTools; -public interface IAIPromptToolViewOptionsProvider +public interface IAIPromptToolAccessProvider { - bool IsDevPromptControlsEnabled { get; } + Task CanViewPromptToolsAsync(); string DefaultPromptVersion { get; } } 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 294997e161..0d5bf7268b 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 @@ -298,12 +298,12 @@ } - @if (Model.IsDevPromptControlsEnabled) - { - - } + @if (Model.CanViewPromptTools) + { + + }
@@ -524,42 +524,33 @@
} @*-------- AI Analysis Tab Section END ---------*@ - @if (Model.IsDevPromptControlsEnabled) - { -
-
-
AI Dev Tools
-
-
- + @if (Model.DefaultPromptVersion == "v0") + { } - else - { - - - } - - @if (aiAttachmentSummariesEnabled && aiApplicationAnalysisEnabled && aiScoringEnabled) - { - - } -
-
-
+ else + { + + + } + +
+ +
-
-
-
Attachment Summary
+
+
+
Attachment Summary
-
-
-
- -
-
- -
-
-
Application Analysis
+ +
+
+ +
+
+
Application Analysis
-
-
-
- -
-
- -
-
-
Application Scoring
+ +
+
+ +
+
+
Application Scoring
-
-
-
- -
-
+ +
+
} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs index 499f338e15..829d28a058 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs @@ -33,6 +33,7 @@ public class DetailsModel : AbpPageModel private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; private readonly IScoresheetRepository _scoresheetRepository; private readonly IFeatureChecker _featureChecker; + private readonly IAIPromptToolAccessProvider _aiPromptToolAccessProvider; protected readonly IZoneManagementAppService _zoneManagementAppService; [BindProperty(SupportsGet = true)] @@ -95,7 +96,7 @@ public class DetailsModel : AbpPageModel public HashSet ZoneStateSet { get; set; } = []; [BindProperty(SupportsGet = true)] - public bool IsDevPromptControlsEnabled { get; set; } + public bool CanViewPromptTools { get; set; } [BindProperty(SupportsGet = true)] public string DefaultPromptVersion { get; set; } @@ -111,7 +112,7 @@ public DetailsModel( IFeatureChecker featureChecker, ICurrentUser currentUser, IConfiguration configuration, - IAIPromptToolViewOptionsProvider aiPromptToolViewOptionsProvider, + IAIPromptToolAccessProvider aiPromptToolAccessProvider, IZoneManagementAppService zoneManagementAppService) { _grantApplicationAppService = grantApplicationAppService; @@ -120,6 +121,7 @@ public DetailsModel( _applicationFormVersionAppService = applicationFormVersionAppService; _scoresheetRepository = scoresheetRepository; _zoneManagementAppService = zoneManagementAppService; + _aiPromptToolAccessProvider = aiPromptToolAccessProvider; CurrentUserId = currentUser.Id; CurrentUserName = currentUser.SurName + ", " + currentUser.Name; @@ -127,12 +129,12 @@ public DetailsModel( MaxFileSize = configuration["S3:MaxFileSize"] ?? ""; EmailAttachmentMaxFileSize = configuration["S3:EmailAttachmentMaxFileSize"] ?? "20"; TotalEmailAttachmentMaxFileSize = configuration["S3:EmailAttachmentsTotalMaxFileSize"] ?? "25"; - IsDevPromptControlsEnabled = aiPromptToolViewOptionsProvider.IsDevPromptControlsEnabled; - DefaultPromptVersion = aiPromptToolViewOptionsProvider.DefaultPromptVersion; + DefaultPromptVersion = aiPromptToolAccessProvider.DefaultPromptVersion; } public async Task OnGetAsync() { + CanViewPromptTools = await _aiPromptToolAccessProvider.CanViewPromptToolsAsync(); ApplicationFormSubmission applicationFormSubmission = await _grantApplicationAppService.GetFormSubmissionByApplicationId(ApplicationId); ZoneStateSet = await _zoneManagementAppService.GetZoneStateSetAsync(applicationFormSubmission.ApplicationFormId); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css index bac97ee3cb..ff2e4603af 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css @@ -813,3 +813,87 @@ form label.error { background-color: #f9fafb; border-color: #9ca3af; } +.prompt-tools-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem 1rem; + flex-wrap: nowrap; +} + +.prompt-tools-header .prompt-tools-toolbar-row { + display: flex; + align-items: center; + gap: 0.5rem 0.75rem; +} + +.prompt-tools-header select, +.prompt-tools-header .form-select { + width: auto; + white-space: nowrap; +} + +.prompt-tools-header .prompt-tools-toolbar-row { + margin-left: auto; +} + +.prompt-tools-section { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e7ebef; +} + +.prompt-tools-section:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: 0; +} + +.prompt-tools-section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem 1rem; + margin-bottom: 0.5rem; +} + +.prompt-tools-section-header h6 { + min-width: 0; + flex: 1 1 auto; +} + +.prompt-tools-output-container { + position: relative; +} + +.prompt-tools-output-container .prompt-tools-output-actions { + position: absolute; + top: 0.375rem; + right: 0.5rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; + z-index: 1; + padding: 0 0.25rem; + background: #ffffff; + border-radius: 999px; +} + +.prompt-tools-output-container .prompt-tools-output { + min-height: 10rem; + max-height: 24rem; + overflow: auto; + resize: vertical; + white-space: pre; + font-family: Consolas, "Courier New", monospace; + line-height: 1.35; + padding-top: 0.75rem; + padding-right: 2.5rem; +} + +.prompt-tools-output-container .prompt-tools-output-copy-btn { + width: 2rem; + height: 2rem; + padding: 0; + color: #5c6b7a; +} 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 1e7cabeb86..fa611d5c07 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 @@ -164,14 +164,14 @@ $(function () { ]); globalThis.getSelectedPromptVersion = function() { - return $('#devPromptVersion').val() || null; + return $('#promptVersion').val() || null; }; - function setDevAiOutput(selector, value) { + function setPromptToolsOutput(selector, value) { $(selector).val(value || ''); } - function setDevAiOutputTimestamp(selector, value) { + function setPromptToolsTimestamp(selector, value) { $(selector).text(value ? `(${value})` : ''); } @@ -267,7 +267,7 @@ $(function () { function formatAttachmentAiOutput(attachments) { const attachmentBody = formatAttachmentSummaryBody(attachments); if (!attachmentBody) { - setDevAiOutputTimestamp('#attachmentAiOutputTimestamp', ''); + setPromptToolsTimestamp('#attachmentOutputTimestamp', ''); return ''; } @@ -284,20 +284,20 @@ $(function () { .sort() .at(-1); - setDevAiOutputTimestamp('#attachmentAiOutputTimestamp', formatTimestamp(latestTimestamp)); + setPromptToolsTimestamp('#attachmentOutputTimestamp', formatTimestamp(latestTimestamp)); return attachmentBody; } - function loadDevAiOutputs() { + function loadPromptToolsOutputs() { const applicationId = $('#DetailsViewApplicationId').val(); if (!applicationId) { - setDevAiOutput('#analysisAiOutput', ''); - setDevAiOutput('#scoringAiOutput', ''); - setDevAiOutput('#attachmentAiOutput', ''); - setDevAiOutputTimestamp('#analysisAiOutputTimestamp', ''); - setDevAiOutputTimestamp('#scoringAiOutputTimestamp', ''); - setDevAiOutputTimestamp('#attachmentAiOutputTimestamp', ''); + setPromptToolsOutput('#analysisOutput', ''); + setPromptToolsOutput('#scoringOutput', ''); + setPromptToolsOutput('#attachmentOutput', ''); + setPromptToolsTimestamp('#analysisOutputTimestamp', ''); + setPromptToolsTimestamp('#scoringOutputTimestamp', ''); + setPromptToolsTimestamp('#attachmentOutputTimestamp', ''); return; } @@ -311,10 +311,10 @@ $(function () { const updatedAt = application?.lastModificationTime || application?.creationTime || null; const formattedUpdatedAt = formatTimestamp(updatedAt); const attachmentSection = formatSectionBody('ATTACHMENTS', formatAttachmentSummaryJson(attachments)); - setDevAiOutputTimestamp('#analysisAiOutputTimestamp', formattedUpdatedAt); - setDevAiOutputTimestamp('#scoringAiOutputTimestamp', formattedUpdatedAt); - setDevAiOutput( - '#analysisAiOutput', + setPromptToolsTimestamp('#analysisOutputTimestamp', formattedUpdatedAt); + setPromptToolsTimestamp('#scoringOutputTimestamp', formattedUpdatedAt); + setPromptToolsOutput( + '#analysisOutput', formatOutputBody('APPLICATION ANALYSIS', [ formatSectionBody('DATA', getPromptDataPayload()), attachmentSection, @@ -324,8 +324,8 @@ $(function () { ) ]) ); - setDevAiOutput( - '#scoringAiOutput', + setPromptToolsOutput( + '#scoringOutput', formatOutputBody('APPLICATION SCORING', [ formatSectionBody('SCORESHEET', formatJsonOrRaw(getScoresheetSchemaJson())), formatSectionBody('DATA', getPromptDataPayload()), @@ -336,18 +336,18 @@ $(function () { ) ]) ); - setDevAiOutput( - '#attachmentAiOutput', + setPromptToolsOutput( + '#attachmentOutput', formatOutputBody('ATTACHMENT SUMMARY', [formatAttachmentAiOutput(attachments)]) ); }) .fail(function() { - setDevAiOutput('#analysisAiOutput', ''); - setDevAiOutput('#scoringAiOutput', ''); - setDevAiOutput('#attachmentAiOutput', ''); - setDevAiOutputTimestamp('#analysisAiOutputTimestamp', ''); - setDevAiOutputTimestamp('#scoringAiOutputTimestamp', ''); - setDevAiOutputTimestamp('#attachmentAiOutputTimestamp', ''); + setPromptToolsOutput('#analysisOutput', ''); + setPromptToolsOutput('#scoringOutput', ''); + setPromptToolsOutput('#attachmentOutput', ''); + setPromptToolsTimestamp('#analysisOutputTimestamp', ''); + setPromptToolsTimestamp('#scoringOutputTimestamp', ''); + setPromptToolsTimestamp('#attachmentOutputTimestamp', ''); }); } @@ -371,16 +371,16 @@ $(function () { stopAIGenerationPolling(); globalThis.AIGenerationButtonState?.restore(restoreButton); restoreButton.html(originalHtml).prop('disabled', false); - loadDevAiOutputs(); + loadPromptToolsOutputs(); abp.message.error(request?.failureReason || 'AI generate all failed.'); return; } if (!request || request.isActive === false || statusText === 'Completed') { stopAIGenerationPolling(); - setDevAiOutputTimestamp('#analysisAiOutputTimestamp', request?.completedAt || request?.startedAt || null); - setDevAiOutputTimestamp('#scoringAiOutputTimestamp', request?.completedAt || request?.startedAt || null); - loadDevAiOutputs(); + setPromptToolsTimestamp('#analysisOutputTimestamp', request?.completedAt || request?.startedAt || null); + setPromptToolsTimestamp('#scoringOutputTimestamp', request?.completedAt || request?.startedAt || null); + loadPromptToolsOutputs(); globalThis.AIGenerationButtonState?.setCompleted(restoreButton); restoreButton.html('Completed').prop('disabled', true); return; @@ -463,41 +463,9 @@ $(function () { ); }; - globalThis.refreshDevAiOutputs = loadDevAiOutputs; + globalThis.refreshPromptToolsOutputs = loadPromptToolsOutputs; - globalThis.generateAllAIDevOutputs = function(triggerButton = null) { - const $button = triggerButton ? $(triggerButton) : $('#generateAllAiDevToolsBtn'); - const existingHtml = $button.html(); - const applicationId = $('#DetailsViewApplicationId').val(); - const promptVersion = globalThis.getSelectedPromptVersion?.() || null; - - if (!applicationId || $button.prop('disabled')) { - return; - } - - $button - .html('Generating...') - .prop('disabled', true); - globalThis.AIGenerationButtonState?.setGenerating($button); - - unity.grantManager.grantApplications.grantApplication - .queueAllAIStages(applicationId, promptVersion) - .done(function(request) { - pollAIGenerationStatus(applicationId, 'pipeline', promptVersion, $button, existingHtml); - }) - .fail(function() { - abp.message.error('Failed to queue AI generate all. Please try again.'); - globalThis.AIGenerationButtonState?.restore($button); - $button.html(existingHtml).prop('disabled', false); - }) - ; - }; - - $('#generateAllAiDevToolsBtn').on('click', function() { - globalThis.generateAllAIDevOutputs(this); - }); - - $(document).on('click', '.ai-dev-output-copy-btn', async function () { + $(document).on('click', '.prompt-tools-output-copy-btn', async function () { const targetSelector = $(this).data('target'); const text = $(targetSelector).val(); @@ -543,7 +511,7 @@ $(function () { updateLinksCounters(); renderSubmission(); loadAIAnalysis(); - loadDevAiOutputs(); + loadPromptToolsOutputs(); applyTabHeightOffset(); } @@ -831,7 +799,7 @@ $(function () { PubSub.subscribe('refresh_assessment_scores', (msg, data) => { assessmentScoresWidgetManager.refresh(); updateSubtotal(); - loadDevAiOutputs(); + loadPromptToolsOutputs(); }); PubSub.subscribe('refresh_chefs_attachment_list', () => { From 774b56e7662c094bf5c2b956aca25fa9f529cdf2 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 1 May 2026 14:52:12 -0700 Subject: [PATCH 04/34] AB#32543 clean up AI reporting state naming --- .../Pages/AIReporting/Index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js index 33942eedcc..2559b0aa44 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js @@ -52,13 +52,13 @@ const notify = abp.notify; - // Fresh turn (matches Turn interface from reference) + // Fresh turn const createTurn = (question, retryCount = 0) => ({ id: newTurnId(), question, embed: null, safeUrl: 'loading', // 'loading' | 'failure' | null (success) - iframeLoaded: false, + loaded: false, sqlPanelOpen: false, sql_explanation_visible: false, sql_explanation_text: '', // local rendered text (typewriter) @@ -308,7 +308,7 @@ question: t.question, embed: t.embed, safeUrl: t.safeUrl, - iframeLoaded: t.iframeLoaded, + loaded: t.loaded, sqlPanelOpen: t.sqlPanelOpen, sql_explanation_visible: t.sql_explanation_visible, errorType: t.errorType, @@ -337,7 +337,7 @@ question: r.question || '', embed: r.embed || null, safeUrl: r.safeUrl ?? null, - iframeLoaded: true, + loaded: true, sqlPanelOpen: r.sqlPanelOpen ?? false, sql_explanation_visible: r.sql_explanation_visible ?? false, sql_explanation_text: r.embed?.sql_explanation || '', @@ -413,7 +413,7 @@ cache_original_query: result?.cache_original_query || null, }; t.safeUrl = null; - t.iframeLoaded = true; + t.loaded = true; updateBubble(t.id); syncControls(); await saveChat(); @@ -421,7 +421,7 @@ console.error('Failed to process question:', err); const t = findTurn(turn.id); if (!t) return; - t.iframeLoaded = true; + t.loaded = true; t.safeUrl = 'failure'; t.errorDetail = err?.errorDetail ?? null; From 79eb45ad46c8d16410301967923b553bd170fbc3 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 1 May 2026 15:05:51 -0700 Subject: [PATCH 05/34] AB#32451 no-op branch checkpoint From 0ee6cba000350b4a35f0b9e127dcbd7edfd283b7 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 1 May 2026 15:49:53 -0700 Subject: [PATCH 06/34] bugfix/AB#32451-FixReviewlistJS Co-authored-by: Copilot --- .../Components/ReviewList/ReviewList.js | 202 +++++++++--------- 1 file changed, 101 insertions(+), 101 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 e5368d36ff..257407605c 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 pageApplicationId = decodeURIComponent(document.querySelector("#DetailsViewApplicationId")?.value ?? ''); +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); } @@ -367,7 +367,7 @@ function renderApproval(data) { } } async function getActionButtonConfigMap() { - let applicationId = document.getElementById('DetailsViewApplicationId').value; + let applicationId = document.getElementById('DetailsViewApplicationId')?.value; let applicationStatus = await unity.grantManager.grantApplications.grantApplication.getApplicationStatus(applicationId).then(data => { return data; }); @@ -455,97 +455,97 @@ function unityWorkflowButtonAction(e, dt, button, config) { } } -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 = globalThis.AIGenerationButtonState?.resolveStatus(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) { - const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - - if (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) { +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 = globalThis.AIGenerationButtonState?.resolveStatus(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) { + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; + + if (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 97c75fedda0f9b7b9c902947f3b538b63c019bb7 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 1 May 2026 15:55:47 -0700 Subject: [PATCH 07/34] bugfix/AB#32451-FixReviewlistJS --- .../Automation/BackgroundJobs/RunApplicationAIPipelineJob.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 def126a49e..c749f3ff88 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 @@ -31,7 +31,7 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args) { if (string.IsNullOrWhiteSpace(args.RequestKey)) { - throw new ArgumentException("RequestKey is required.", nameof(args.RequestKey)); + throw new ArgumentException("RequestKey is required.", nameof(args)); } using (currentTenant.Change(args.TenantId)) From 73ea87f935a382909e1c1796cea839adfc6ec035 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 1 May 2026 16:03:46 -0700 Subject: [PATCH 08/34] AB#32451 remove stale PDF script references --- .../Pages/GrantApplications/Details.cshtml | 2 -- 1 file changed, 2 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 294997e161..3256376c63 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 @@ -70,8 +70,6 @@ - - } From 65d53b0102fbb329cc23f1a59fb68900a99c2554 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 1 May 2026 16:24:16 -0700 Subject: [PATCH 09/34] AB#32543 simplify AI reporting page setup --- .../Pages/AIReporting/Index.cshtml | 13 +- .../Pages/AIReporting/Index.css | 719 +++++++++++++++++- .../Pages/AIReporting/Index.js | 11 +- 3 files changed, 735 insertions(+), 8 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml index 541a903c95..105b27e25a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.cshtml @@ -4,24 +4,29 @@ @using Volo.Abp.Features @inject IFeatureChecker FeatureChecker +@{ + var canViewAiReporting = await FeatureChecker.IsEnabledAsync("Unity.AIReporting") + || User.IsInRole(IdentityConsts.ITAdminRoleName); +} + @section styles { - @if (await FeatureChecker.IsEnabledAsync("Unity.AIReporting") || User.IsInRole(IdentityConsts.ITAdminRoleName)) + @if (canViewAiReporting) { } } @section scripts { - @if (await FeatureChecker.IsEnabledAsync("Unity.AIReporting") || User.IsInRole(IdentityConsts.ITAdminRoleName)) + @if (canViewAiReporting) { } } -@if (await FeatureChecker.IsEnabledAsync("Unity.AIReporting") || User.IsInRole(IdentityConsts.ITAdminRoleName)) +@if (canViewAiReporting) {
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css index b99ce14729..c74a9eab14 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.css @@ -1 +1,718 @@ -/* =========================================================================== AI Reporting page — port of .working/unity-ai/applications/Unity.AI.Reporting.Frontend Sources: app.css, sidebar.css, sql-explanation.ts inline styles. =========================================================================== *//* Local CSS variables (reference uses Metabase global vars; we inline) */:root { --mb-radius: 8px; --mb-blue-600: #2563eb; --mb-blue-700: #1d4ed8; --mb-teal-600: #0d9488; --mb-gray-200: #e2e8f0; --mb-gray-700: #334155; --mb-red-700: #b91c1c; --mb-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); --mb-font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;}/* ---------- layout shell ---------- */.outer-container { display: grid; grid-template-columns: auto 1fr; /* Use dvh so the layout follows the actual visible viewport (handles browser chrome, address bar, and zoom changes more reliably than 100vh). */ height: calc(100dvh - 6.75rem); min-height: 480px; background: #fff;}.container { display: flex; flex-direction: column; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none;}.container::-webkit-scrollbar { display: none; }/* ---------- sidebar ---------- */.sidebar { border-right: 1px solid #e9ecef; display: flex; flex-direction: column; height: 100%; width: 220px; background: #fff; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);}.sidebar-header { padding: 16px 12px 6px 12px; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0;}.sidebar-header h2 { margin: 14px 0 0 14px; font-size: 0.8rem; font-weight: 500; color: #6c757d; text-align: left;}.new-chat-btn { background: transparent; color: #333; border: none; padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 0.9rem; font-weight: 500; display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;}.new-chat-btn:hover,.chat-item:hover,.chat-item.active { background-color: #e9ecef;}.new-chat-icon { font-size: 16px;}.sidebar-content { flex: 1; overflow-y: auto; padding: 0;}.loading,.empty-state { padding: 20px; text-align: center; color: #6c757d; font-style: italic; font-size: 0.9rem;}.chat-list { padding: 0;}.chat-item { margin: 6px 12px; padding: 8px 14px; cursor: pointer; position: relative; display: flex; flex-direction: column; border-radius: 8px;}.chat-title { font-weight: 400; color: #333; font-size: 0.9rem; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 30px;}.delete-chat-btn { position: absolute; top: 50%; right: 12px; transform: translateY(-50%); background: none; border: none; cursor: pointer; border-radius: 4px; opacity: 0; font-size: 16px; color: #6c757d; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; line-height: 1; padding: 0;}.chat-item:hover .delete-chat-btn { opacity: 1; }.delete-chat-btn:hover { background-color: #d9d9d9; color: #333;}.sidebar-footer { border-top: 1px solid #e9ecef; padding: 12px; display: flex; justify-content: flex-end; flex-shrink: 0; background: #fff;}.footer-btn { background: transparent; border: 1px solid transparent; border-radius: 8px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #6c757d; transition: all 0.2s ease;}.footer-btn:hover { background-color: #f8f9fa; border-color: #dee2e6; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);}.info-btn:hover { background-color: #d1ecf1; border-color: #17a2b8; color: #0c5460;}/* ---------- empty welcome ---------- */.empty-chat-container { display: flex; align-items: center; justify-content: center; height: 100%; flex: 1;}.welcome-content { text-align: center; width: 100%; max-width: 800px;}.welcome-title { font-size: 2rem; color: #333; margin: 0 0 40px 0; line-height: 1.2; font-weight: 500;}/* ---------- chat layout ---------- */.chat-container { display: grid; grid-template-rows: 1fr auto; height: 100%; min-height: 0; flex: 1;}.turns { height: 100%; overflow: hidden; /* Scale padding gracefully across viewport widths — 15vw on big screens, but capped so the chat doesn't get squeezed on smaller laptops/zoom levels. */ padding: 0 clamp(16px, 15vw, 240px); position: relative; min-height: 0;}.turn { display: flex; flex-direction: column; height: 100%; box-sizing: border-box; padding: 24px 0;}/* ---------- bubbles ---------- */.bubble { border-radius: 18px; padding: 12px 16px; font-size: 16px; box-shadow: var(--mb-shadow-sm); border: 1px solid var(--mb-gray-200); background: #fff;}.bubble.bot { width: 100%; position: relative; height: 100%; overflow-y: auto; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; padding: 8px; background: radial-gradient(ellipse at center, rgba(66, 153, 225, 0.08) 0%, rgba(255, 255, 255, 0.95) 70%);}.bubble.bot::-webkit-scrollbar { display: none; }.bubble.bot > div:not(.spinner-center) { height: 100%; align-self: flex-start; display: flex; flex-direction: column;}.bot-inner { opacity: 0; transition: opacity 0.6s; padding: 0; height: 100%; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center;}.bot-inner.loaded { opacity: 1; }/* ---------- ask row ---------- */.ask-row-container { margin: 0 clamp(12px, 10vw, 160px) 16px; position: relative; padding: 20px 20px 12px 20px; background: #fff; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); border-radius: 40px;}.welcome-ask-row { margin: 0; padding: 10px 10px 10px 24px;}.ask-row { display: flex; gap: 12px;}.ask-row-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 16px;}.bottom-left-controls { display: flex; align-items: center; gap: 12px;}.ask-row input[type="text"] { flex: 1 1 0; border: none; border-radius: var(--mb-radius); font-size: 16px; padding: 8px 4px; background: transparent;}.ask-row input[type="text"]:focus { outline: none; }.ask-question-btn { background: var(--mb-blue-600); color: #fff; border: none; border-radius: 50%; cursor: pointer; transition: background-color 0.12s, box-shadow 0.12s; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;}.ask-question-btn:hover { background: var(--mb-blue-700); box-shadow: var(--mb-shadow-sm);}.ask-question-btn svg { color: #fff; }/* ---------- action buttons ---------- */.action-buttons { display: flex; align-items: center; gap: 8px;}.action-button { display: flex; align-items: center; justify-content: center; padding: 8px; background: transparent; border: 1px solid transparent; border-radius: 20px; font-size: 14px; font-weight: 500; color: #495057; cursor: pointer; transition: all 0.2s ease; white-space: nowrap; width: 32px; height: 32px; box-sizing: border-box;}.action-button:hover:not(:disabled) { background: #f8f9fa; border-color: #dee2e6;}.action-button:disabled { cursor: not-allowed; opacity: 0.4;}.action-button.delete-action:hover:not(:disabled) { background: #f8d7da; border-color: #f5c6cb; color: #721c24;}.action-button svg { width: 16px; height: 16px; }.nav-controls { display: flex; align-items: center; gap: 8px;}.action-button.nav-button:disabled { background: transparent; border-color: transparent; color: #adb5bd; opacity: 0.6;}.turn-counter { font-size: 0.875rem; color: #6c757d; font-weight: 500; min-width: 60px; text-align: center; white-space: nowrap;}/* ---------- loading state ---------- */.sql-loader-container { flex: 1 1 auto; display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden; position: relative; width: 100%; height: 100%; min-height: 400px;}.loading-animation-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; padding: 20px; background: rgba(255, 255, 255, 0.9); border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10;}.loading-dots { display: flex; gap: 8px; align-items: center;}.loading-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--mb-blue-600); animation: pulse 1.5s ease-in-out infinite;}.loading-dot:nth-child(1) { animation-delay: 0s; }.loading-dot:nth-child(2) { animation-delay: 0.2s; }.loading-dot:nth-child(3) { animation-delay: 0.4s; }@keyframes pulse { 0%, 80%, 100% { transform: scale(0.8); opacity: 0.6; } 40% { transform: scale(1.2); opacity: 1; }}.loading-text { font-size: 16px; font-weight: 500; color: #666; text-align: center;}/* ---------- failure state ---------- */.failure-container { padding: 24px; height: 100%; box-sizing: border-box; position: relative;}.failure-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; padding: 20px; background: rgba(255, 255, 255, 0.9); border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10; max-width: 480px;}.failure-icon { font-size: 32px; flex-shrink: 0; }.failure-title { font-size: 18px; font-weight: 600; color: #c53030; margin: 0;}.failure-message { font-size: 14px; color: #718096; line-height: 1.5; margin: 0; text-align: center;}.failure-actions { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 4px;}.retry-btn { padding: 8px 16px; font-size: 14px; font-weight: 500; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; background: #3182ce; color: #fff;}.retry-btn:hover { background: #2c5aa0; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);}.retry-btn--disabled,.retry-btn--disabled:hover { background: #64748b; color: #fff; cursor: not-allowed; transform: none; box-shadow: none;}.retry-hint { font-size: 0.9rem; color: #666; margin: 0;}/* ---------- semantic cache badge ---------- */.cache-badge { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; padding: 4px 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 0.8rem; color: #334155; margin-bottom: 8px; cursor: default; user-select: none; flex-shrink: 0;}.cache-badge-hint { color: #64748b; }.cache-fresh-btn { background: none; border: none; padding: 0; margin-left: 4px; font-size: inherit; color: #2563eb; text-decoration: underline; cursor: pointer;}.cache-fresh-btn:hover:not(:disabled) { color: #1d4ed8; }.cache-fresh-btn:disabled { color: #94a3b8; cursor: not-allowed; text-decoration: none;}/* ---------- Metabase view button ---------- */.metabase-view-container { display: flex; justify-content: center; align-items: center; flex: 1;}.metabase-view-btn { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 16px 32px; background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%); color: #fff; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);}.metabase-view-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(66, 153, 225, 0.4); background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); color: #fff;}.metabase-view-btn:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);}.metabase-view-btn svg { flex-shrink: 0; transition: all 0.3s ease; }.metabase-view-btn:hover svg { transform: translateX(2px) translateY(-2px); }.metabase-view-btn span { position: relative; z-index: 1; letter-spacing: 0.3px; }/* ---------- SQL panel (overlay) ---------- */.sql-panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #fff; z-index: 5; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; display: flex; flex-direction: column; justify-content: flex-start; box-sizing: border-box; padding: 8px 16px;}.sql-panel::-webkit-scrollbar { display: none; }.sql-panel-header { padding: 8px 0 16px 0; font-weight: 600; font-size: 18px; color: var(--mb-gray-700); flex-shrink: 0;}.sql-code { padding: 12px 16px; margin: 0; font-family: 'Fira Mono', Consolas, 'Courier New', monospace; font-size: 14px; line-height: 1.35; color: #1e1e1e; background: #fff; white-space: pre-wrap; overflow-y: auto; border-radius: 8px; border: 1px solid var(--mb-gray-200); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); flex: 1;}/* ---------- SQL explanation bubble (typewriter) ---------- */.sql-explanation-bubble { position: relative; background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 12px; padding: 0; margin: 12px 8px 8px 8px; font-size: 0.85em; color: #075985; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); animation: slideIn 0.3s ease-out; flex-shrink: 0;}@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); }}.bubble-content { padding: 10px 14px; line-height: 1.4;}.bubble-tail { position: absolute; top: -6px; left: 30px; width: 12px; height: 12px; background: #f0f9ff; border-left: 1px solid #bae6fd; border-top: 1px solid #bae6fd; transform: rotate(45deg);}.cursor { animation: blink 1s infinite; font-weight: normal; opacity: 0.8; color: #075985;}@keyframes blink { 0%, 50% { opacity: 0.8; } 51%, 100% { opacity: 0; }}/* ---------- responsive ---------- */@media (max-width: 768px) { .outer-container { grid-template-columns: 1fr; } .sidebar { width: 100%; } .turns { padding: 0 16px; } .ask-row-container { margin-left: 12px; margin-right: 12px; }} \ No newline at end of file +/* ========================================================================== + AI Reporting page — port of .working/unity-ai/applications/Unity.AI.Reporting.Frontend + Sources: app.css, sidebar.css, sql-explanation.ts inline styles. + ========================================================================== */ +/* Local CSS variables (reference uses Metabase global vars; we inline) */ +:root { + --mb-radius: 8px; + --mb-blue-600: #2563eb; + --mb-blue-700: #1d4ed8; + --mb-teal-600: #0d9488; + --mb-gray-200: #e2e8f0; + --mb-gray-700: #334155; + --mb-red-700: #b91c1c; + --mb-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --mb-font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} +/* ---------- layout shell ---------- */ +.outer-container { + display: grid; + grid-template-columns: auto 1fr; + /* Use dvh so the layout follows the actual visible viewport (handles browser chrome, address bar, and zoom changes more reliably than 100vh). */ + height: calc(100dvh - 6.75rem); + min-height: 480px; + background: #fff; +} +.container { + display: flex; + flex-direction: column; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} +.container::-webkit-scrollbar { + display: none; +} +/* ---------- sidebar ---------- */ +.sidebar { + border-right: 1px solid #e9ecef; + display: flex; + flex-direction: column; + height: 100%; + width: 220px; + background: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} +.sidebar-header { + padding: 16px 12px 6px 12px; + display: flex; + flex-direction: column; + gap: 8px; + flex-shrink: 0; +} +.sidebar-header h2 { + margin: 14px 0 0 14px; + font-size: 0.8rem; + font-weight: 500; + color: #6c757d; + text-align: left; +} +.new-chat-btn { + background: transparent; + color: #333; + border: none; + padding: 8px 14px; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; +} +.new-chat-btn:hover, +.chat-item:hover, +.chat-item.active { + background-color: #e9ecef; +} +.new-chat-icon { + font-size: 16px; +} +.sidebar-content { + flex: 1; + overflow-y: auto; + padding: 0; +} +.loading, +.empty-state { + padding: 20px; + text-align: center; + color: #6c757d; + font-style: italic; + font-size: 0.9rem; +} +.chat-list { + padding: 0; +} +.chat-item { + margin: 6px 12px; + padding: 8px 14px; + cursor: pointer; + position: relative; + display: flex; + flex-direction: column; + border-radius: 8px; +} +.chat-title { + font-weight: 400; + color: #333; + font-size: 0.9rem; + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 30px; +} +.delete-chat-btn { + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + border-radius: 4px; + opacity: 0; + font-size: 16px; + color: #6c757d; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + padding: 0; +} +.chat-item:hover .delete-chat-btn { + opacity: 1; +} +.delete-chat-btn:hover { + background-color: #d9d9d9; + color: #333; +} +.sidebar-footer { + border-top: 1px solid #e9ecef; + padding: 12px; + display: flex; + justify-content: flex-end; + flex-shrink: 0; + background: #fff; +} +.footer-btn { + background: transparent; + border: 1px solid transparent; + border-radius: 8px; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #6c757d; + transition: all 0.2s ease; +} +.footer-btn:hover { + background-color: #f8f9fa; + border-color: #dee2e6; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} +.info-btn:hover { + background-color: #d1ecf1; + border-color: #17a2b8; + color: #0c5460; +} +/* ---------- empty welcome ---------- */ +.empty-chat-container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + flex: 1; +} +.welcome-content { + text-align: center; + width: 100%; + max-width: 800px; +} +.welcome-title { + font-size: 2rem; + color: #333; + margin: 0 0 40px 0; + line-height: 1.2; + font-weight: 500; +} +/* ---------- chat layout ---------- */ +.chat-container { + display: grid; + grid-template-rows: 1fr auto; + height: 100%; + min-height: 0; + flex: 1; +} +.turns { + height: 100%; + overflow: hidden; + /* Scale padding gracefully across viewport widths — 15vw on big screens, but capped so the chat doesn't get squeezed on smaller laptops/zoom levels. */ + padding: 0 clamp(16px, 15vw, 240px); + position: relative; + min-height: 0; +} +.turn { + display: flex; + flex-direction: column; + height: 100%; + box-sizing: border-box; + padding: 24px 0; +} +/* ---------- bubbles ---------- */ +.bubble { + border-radius: 18px; + padding: 12px 16px; + font-size: 16px; + box-shadow: var(--mb-shadow-sm); + border: 1px solid var(--mb-gray-200); + background: #fff; +} +.bubble.bot { + width: 100%; + position: relative; + height: 100%; + overflow-y: auto; + box-sizing: border-box; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 8px; + background: radial-gradient(ellipse at center, rgba(66, 153, 225, 0.08) 0%, rgba(255, 255, 255, 0.95) 70%); +} +.bubble.bot::-webkit-scrollbar { + display: none; +} +.bubble.bot > div:not(.spinner-center) { + height: 100%; + align-self: flex-start; + display: flex; + flex-direction: column; +} +.bot-inner { + opacity: 0; + transition: opacity 0.6s; + padding: 0; + height: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.bot-inner.loaded { + opacity: 1; +} +/* ---------- ask row ---------- */ +.ask-row-container { + margin: 0 clamp(12px, 10vw, 160px) 16px; + position: relative; + padding: 20px 20px 12px 20px; + background: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + border-radius: 40px; +} +.welcome-ask-row { + margin: 0; + padding: 10px 10px 10px 24px; +} +.ask-row { + display: flex; + gap: 12px; +} +.ask-row-bottom { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; +} +.bottom-left-controls { + display: flex; + align-items: center; + gap: 12px; +} +.ask-row input[type="text"] { + flex: 1 1 0; + border: none; + border-radius: var(--mb-radius); + font-size: 16px; + padding: 8px 4px; + background: transparent; +} +.ask-row input[type="text"]:focus { + outline: none; +} +.ask-question-btn { + background: var(--mb-blue-600); + color: #fff; + border: none; + border-radius: 50%; + cursor: pointer; + transition: background-color 0.12s, box-shadow 0.12s; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.ask-question-btn:hover { + background: var(--mb-blue-700); + box-shadow: var(--mb-shadow-sm); +} +.ask-question-btn svg { + color: #fff; +} +/* ---------- action buttons ---------- */ +.action-buttons { + display: flex; + align-items: center; + gap: 8px; +} +.action-button { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + background: transparent; + border: 1px solid transparent; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + width: 32px; + height: 32px; + box-sizing: border-box; +} +.action-button:hover:not(:disabled) { + background: #f8f9fa; + border-color: #dee2e6; +} +.action-button:disabled { + cursor: not-allowed; + opacity: 0.4; +} +.action-button.delete-action:hover:not(:disabled) { + background: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} +.action-button svg { + width: 16px; + height: 16px; +} +.nav-controls { + display: flex; + align-items: center; + gap: 8px; +} +.action-button.nav-button:disabled { + background: transparent; + border-color: transparent; + color: #adb5bd; + opacity: 0.6; +} +.turn-counter { + font-size: 0.875rem; + color: #6c757d; + font-weight: 500; + min-width: 60px; + text-align: center; + white-space: nowrap; +} +/* ---------- loading state ---------- */ +.sql-loader-container { + flex: 1 1 auto; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; + min-height: 400px; +} +.loading-animation-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + padding: 20px; + background: rgba(255, 255, 255, 0.9); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; +} +.loading-dots { + display: flex; + gap: 8px; + align-items: center; +} +.loading-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--mb-blue-600); + animation: pulse 1.5s ease-in-out infinite; +} +.loading-dot:nth-child(1) { + animation-delay: 0s; +} +.loading-dot:nth-child(2) { + animation-delay: 0.2s; +} +.loading-dot:nth-child(3) { + animation-delay: 0.4s; +} +@keyframes pulse { + 0%, 80%, 100% { + transform: scale(0.8); + opacity: 0.6; + } + 40% { + transform: scale(1.2); + opacity: 1; + } +} +.loading-text { + font-size: 16px; + font-weight: 500; + color: #666; + text-align: center; +} +/* ---------- failure state ---------- */ +.failure-container { + padding: 24px; + height: 100%; + box-sizing: border-box; + position: relative; +} +.failure-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + padding: 20px; + background: rgba(255, 255, 255, 0.9); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 10; + max-width: 480px; +} +.failure-icon { + font-size: 32px; + flex-shrink: 0; +} +.failure-title { + font-size: 18px; + font-weight: 600; + color: #c53030; + margin: 0; +} +.failure-message { + font-size: 14px; + color: #718096; + line-height: 1.5; + margin: 0; + text-align: center; +} +.failure-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 4px; +} +.retry-btn { + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + background: #3182ce; + color: #fff; +} +.retry-btn:hover { + background: #2c5aa0; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.retry-btn--disabled, +.retry-btn--disabled:hover { + background: #64748b; + color: #fff; + cursor: not-allowed; + transform: none; + box-shadow: none; +} +.retry-hint { + font-size: 0.9rem; + color: #666; + margin: 0; +} +/* ---------- semantic cache badge ---------- */ +.cache-badge { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + padding: 4px 10px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 4px; + font-size: 0.8rem; + color: #334155; + margin-bottom: 8px; + cursor: default; + user-select: none; + flex-shrink: 0; +} +.cache-badge-hint { + color: #64748b; +} +.cache-fresh-btn { + background: none; + border: none; + padding: 0; + margin-left: 4px; + font-size: inherit; + color: #2563eb; + text-decoration: underline; + cursor: pointer; +} +.cache-fresh-btn:hover:not(:disabled) { + color: #1d4ed8; +} +.cache-fresh-btn:disabled { + color: #94a3b8; + cursor: not-allowed; + text-decoration: none; +} +/* ---------- Metabase view button ---------- */ +.metabase-view-container { + display: flex; + justify-content: center; + align-items: center; + flex: 1; +} +.metabase-view-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 16px 32px; + background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%); + color: #fff; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3); +} +.metabase-view-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(66, 153, 225, 0.4); + background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); + color: #fff; +} +.metabase-view-btn:active { + transform: translateY(0); + box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3); +} +.metabase-view-btn svg { + flex-shrink: 0; + transition: all 0.3s ease; +} +.metabase-view-btn:hover svg { + transform: translateX(2px) translateY(-2px); +} +.metabase-view-btn span { + position: relative; + z-index: 1; + letter-spacing: 0.3px; +} +/* ---------- SQL panel (overlay) ---------- */ +.sql-panel { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + z-index: 5; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + display: flex; + flex-direction: column; + justify-content: flex-start; + box-sizing: border-box; + padding: 8px 16px; +} +.sql-panel::-webkit-scrollbar { + display: none; +} +.sql-panel-header { + padding: 8px 0 16px 0; + font-weight: 600; + font-size: 18px; + color: var(--mb-gray-700); + flex-shrink: 0; +} +.sql-code { + padding: 12px 16px; + margin: 0; + font-family: 'Fira Mono', Consolas, 'Courier New', monospace; + font-size: 14px; + line-height: 1.35; + color: #1e1e1e; + background: #fff; + white-space: pre-wrap; + overflow-y: auto; + border-radius: 8px; + border: 1px solid var(--mb-gray-200); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + flex: 1; +} +/* ---------- SQL explanation bubble (typewriter) ---------- */ +.sql-explanation-bubble { + position: relative; + background: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 12px; + padding: 0; + margin: 12px 8px 8px 8px; + font-size: 0.85em; + color: #075985; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + animation: slideIn 0.3s ease-out; + flex-shrink: 0; +} +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +.bubble-content { + padding: 10px 14px; + line-height: 1.4; +} +.bubble-tail { + position: absolute; + top: -6px; + left: 30px; + width: 12px; + height: 12px; + background: #f0f9ff; + border-left: 1px solid #bae6fd; + border-top: 1px solid #bae6fd; + transform: rotate(45deg); +} +.cursor { + animation: blink 1s infinite; + font-weight: normal; + opacity: 0.8; + color: #075985; +} +@keyframes blink { + 0%, 50% { + opacity: 0.8; + } + 51%, 100% { + opacity: 0; + } +} +/* ---------- responsive ---------- */ +@media (max-width: 768px) { + .outer-container { + grid-template-columns: 1fr; + } + .sidebar { + width: 100%; + } + .turns { + padding: 0 16px; + } + .ask-row-container { + margin-left: 12px; + margin-right: 12px; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js index 2559b0aa44..4a67ba5042 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AIReporting/Index.js @@ -23,7 +23,14 @@ const btnExplain = document.getElementById('btn-explain'); const btnDeleteQ = document.getElementById('btn-delete-question'); - const apiBase = (window.reportingAiApiBaseUrl || '').replace(/\/+$/, '') + '/api'; + const notify = abp.notify; + const reportingAiApiBaseUrl = (window.reportingAiApiBaseUrl || '').trim(); + if (!reportingAiApiBaseUrl) { + notify.error('AI Reporting is not configured.'); + return; + } + + const apiBase = reportingAiApiBaseUrl.replace(/\/+$/, '') + '/api'; const MAX_RETRIES = 2; // ─── State ────────────────────────────────────────────────────────────── @@ -50,8 +57,6 @@ const isLoading = () => state.conversation.some(t => t.safeUrl === 'loading'); const findTurn = (id) => state.conversation.find(t => t.id === id); - const notify = abp.notify; - // Fresh turn const createTurn = (question, retryCount = 0) => ({ id: newTurnId(), From f417b09e78548f1c7479e850910132aee6367804 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Fri, 1 May 2026 18:08:46 -0700 Subject: [PATCH 10/34] AB#32436: Applicant Profile - Reports History in History Tab --- .../History/CreateUpdateReportsHistoryDto.cs | 13 + .../History/IApplicantHistoryAppService.cs | 6 + .../History/ReportsHistoryDto.cs | 14 + .../History/SaveApplicantHistoryNotesDto.cs | 1 + .../History/ApplicantHistoryAppService.cs | 34 + ...rantManagerApplicationAutoMapperProfile.cs | 16 +- .../Applications/Applicant.cs | 1 + .../Applications/IReportsHistoryRepository.cs | 11 + .../Applications/ReportsHistory.cs | 16 + .../GrantTenantDbContext.cs | 8 + ...260501230927_AddReportsHistory.Designer.cs | 5018 +++++++++++++++++ .../20260501230927_AddReportsHistory.cs | 69 + .../GrantTenantDbContextModelSnapshot.cs | 86 +- .../Repositories/ReportsHistoryRepository.cs | 29 + .../CreateReportsHistoryModal.cshtml | 81 + .../CreateReportsHistoryModal.cshtml.cs | 44 + .../EditReportsHistoryModal.cshtml | 82 + .../EditReportsHistoryModal.cshtml.cs | 54 + .../ReportsHistoryModalViewModel.cs | 28 + .../ApplicantHistoryViewComponent.cs | 3 +- .../ApplicantHistoryViewModel.cs | 1 + .../ApplicantHistory/Default.cshtml | 25 + .../Components/ApplicantHistory/Default.css | 6 +- .../Components/ApplicantHistory/Default.js | 118 +- 24 files changed, 5746 insertions(+), 18 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateReportsHistoryDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/ReportsHistoryDto.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IReportsHistoryRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ReportsHistory.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.Designer.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ReportsHistoryRepository.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/ReportsHistoryModalViewModel.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateReportsHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateReportsHistoryDto.cs new file mode 100644 index 0000000000..e3d66bfb30 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/CreateUpdateReportsHistoryDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Unity.GrantManager.ApplicantProfile; + +public class CreateUpdateReportsHistoryDto +{ + public Guid? ApplicantId { get; set; } + public string? FiscalYear { get; set; } + public DateTime? ReportDate { get; set; } + public bool? Outstanding { get; set; } + public bool? IncompleteReport { get; set; } + public string? Note { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs index 8265fd590a..cdedad5441 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/IApplicantHistoryAppService.cs @@ -25,5 +25,11 @@ public interface IApplicantHistoryAppService : IApplicationService Task UpdateAuditHistoryAsync(Guid id, CreateUpdateAuditHistoryDto input); Task DeleteAuditHistoryAsync(Guid id); + Task> GetReportsHistoryListAsync(Guid applicantId); + Task GetReportsHistoryAsync(Guid id); + Task CreateReportsHistoryAsync(CreateUpdateReportsHistoryDto input); + Task UpdateReportsHistoryAsync(Guid id, CreateUpdateReportsHistoryDto input); + Task DeleteReportsHistoryAsync(Guid id); + Task SaveNotesAsync(Guid applicantId, SaveApplicantHistoryNotesDto input); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/ReportsHistoryDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/ReportsHistoryDto.cs new file mode 100644 index 0000000000..a9161dc025 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/ReportsHistoryDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.GrantManager.ApplicantProfile; + +public class ReportsHistoryDto : AuditedEntityDto +{ + public Guid? ApplicantId { get; set; } + public string? FiscalYear { get; set; } + public DateTime? ReportDate { get; set; } + public bool? Outstanding { get; set; } + public bool? IncompleteReport { get; set; } + public string? Note { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs index 8e72c9c8c5..790db08bf7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/History/SaveApplicantHistoryNotesDto.cs @@ -5,4 +5,5 @@ public class SaveApplicantHistoryNotesDto public string? FundingHistoryComments { get; set; } public string? IssueTrackingComments { get; set; } public string? AuditComments { get; set; } + public string? ReportsComments { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs index 04e6c0b14e..6531f66cce 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/History/ApplicantHistoryAppService.cs @@ -9,6 +9,7 @@ public class ApplicantHistoryAppService( IFundingHistoryRepository fundingHistoryRepository, IIssueTrackingRepository issueTrackingRepository, IAuditHistoryRepository auditHistoryRepository, + IReportsHistoryRepository reportsHistoryRepository, IApplicantRepository applicantRepository) : GrantManagerAppService, IApplicantHistoryAppService { public async Task> GetFundingHistoryListAsync(Guid applicantId) @@ -107,12 +108,45 @@ public async Task DeleteAuditHistoryAsync(Guid id) await auditHistoryRepository.DeleteAsync(id, autoSave: true); } + public async Task> GetReportsHistoryListAsync(Guid applicantId) + { + var items = await reportsHistoryRepository.GetByApplicantIdAsync(applicantId); + return ObjectMapper.Map, List>(items); + } + + public async Task GetReportsHistoryAsync(Guid id) + { + var entity = await reportsHistoryRepository.GetAsync(id); + return ObjectMapper.Map(entity); + } + + public async Task CreateReportsHistoryAsync(CreateUpdateReportsHistoryDto input) + { + var entity = ObjectMapper.Map(input); + await reportsHistoryRepository.InsertAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task UpdateReportsHistoryAsync(Guid id, CreateUpdateReportsHistoryDto input) + { + var entity = await reportsHistoryRepository.GetAsync(id); + ObjectMapper.Map(input, entity); + await reportsHistoryRepository.UpdateAsync(entity, autoSave: true); + return ObjectMapper.Map(entity); + } + + public async Task DeleteReportsHistoryAsync(Guid id) + { + await reportsHistoryRepository.DeleteAsync(id, autoSave: true); + } + public async Task SaveNotesAsync(Guid applicantId, SaveApplicantHistoryNotesDto input) { var applicant = await applicantRepository.GetAsync(applicantId); applicant.FundingHistoryComments = input.FundingHistoryComments; applicant.IssueTrackingComments = input.IssueTrackingComments; applicant.AuditComments = input.AuditComments; + applicant.ReportsComments = input.ReportsComments; await applicantRepository.UpdateAsync(applicant, autoSave: true); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs index ab80deee8b..eed6cb79e6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationAutoMapperProfile.cs @@ -95,12 +95,12 @@ public GrantManagerApplicationAutoMapperProfile() CreateMap(); CreateMap(); CreateMap(); - CreateMap(); - CreateMap() - .ForMember(dest => dest.Tag, opt => opt.MapFrom(src => src.Tag)); - CreateMap(); - - //-- APPLICANT HISTORY + CreateMap(); + CreateMap() + .ForMember(dest => dest.Tag, opt => opt.MapFrom(src => src.Tag)); + CreateMap(); + + //-- APPLICANT HISTORY CreateMap(); CreateMap(); CreateMap(); @@ -113,6 +113,10 @@ public GrantManagerApplicationAutoMapperProfile() CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + //-- PROJECT INFO CreateMap() .IgnoreNullAndDefaultValues(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs index 57bab4cb5f..b69fac3393 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/Applicant.cs @@ -35,4 +35,5 @@ public class Applicant : AuditedAggregateRoot, IMultiTenant public string? FundingHistoryComments { get; set; } public string? IssueTrackingComments { get; set; } public string? AuditComments { get; set; } + public string? ReportsComments { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IReportsHistoryRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IReportsHistoryRepository.cs new file mode 100644 index 0000000000..17ace90ffc --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/IReportsHistoryRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Unity.GrantManager.Applications; + +public interface IReportsHistoryRepository : IRepository +{ + Task> GetByApplicantIdAsync(Guid applicantId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ReportsHistory.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ReportsHistory.cs new file mode 100644 index 0000000000..cd6f4e683a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Applications/ReportsHistory.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.Applications; + +public class ReportsHistory : AuditedAggregateRoot, IMultiTenant +{ + public Guid? ApplicantId { get; set; } + public string? FiscalYear { get; set; } + public DateTime? ReportDate { get; set; } + public bool? Outstanding { get; set; } + public bool? IncompleteReport { get; set; } + public string? Note { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs index eca253ff34..fb1f585a96 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantTenantDbContext.cs @@ -51,6 +51,7 @@ public class GrantTenantDbContext : AbpDbContext public DbSet FundingHistories { get; set; } public DbSet IssueTrackings { get; set; } public DbSet AuditHistories { get; set; } + public DbSet ReportsHistories { get; set; } #endregion public GrantTenantDbContext(DbContextOptions options) : base(options) @@ -391,6 +392,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) b.HasOne().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired(false); }); + modelBuilder.Entity(b => + { + b.ToTable(GrantManagerConsts.TenantTablePrefix + "ReportsHistories", GrantManagerConsts.DbSchema); + b.ConfigureByConvention(); + b.HasOne().WithMany().HasForeignKey(x => x.ApplicantId).IsRequired(false); + }); + var allEntityTypes = modelBuilder.Model.GetEntityTypes(); foreach (var type in allEntityTypes.Where(t => t.ClrType != typeof(ExtraPropertyDictionary)).Select(t => t.ClrType)) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.Designer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.Designer.cs new file mode 100644 index 0000000000..bb4b7c698d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.Designer.cs @@ -0,0 +1,5018 @@ +// +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("20260501230927_AddReportsHistory")] + partial class AddReportsHistory + { + /// + 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("IsArchived") + .HasColumnType("boolean"); + + 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("Definition") + .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("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("ReportsComments") + .HasColumnType("text"); + + b.Property("Sector") + .HasColumnType("text"); + + b.Property("SectorSubSectorIndustryDesc") + .HasColumnType("text"); + + 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("DefaultSiteId") + .HasColumnType("uuid"); + + 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.Applications.ReportsHistory", 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("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .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("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (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("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.Applications.ReportsHistory", 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/20260501230927_AddReportsHistory.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.cs new file mode 100644 index 0000000000..9207abb5ef --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260501230927_AddReportsHistory.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Unity.GrantManager.Migrations.TenantMigrations +{ + /// + public partial class AddReportsHistory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + migrationBuilder.AddColumn( + name: "ReportsComments", + table: "Applicants", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "ReportsHistories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApplicantId = table.Column(type: "uuid", nullable: true), + FiscalYear = table.Column(type: "text", nullable: true), + ReportDate = table.Column(type: "timestamp without time zone", nullable: true), + Outstanding = table.Column(type: "boolean", nullable: true), + IncompleteReport = table.Column(type: "boolean", nullable: true), + Note = table.Column(type: "text", nullable: true), + TenantId = table.Column(type: "uuid", nullable: true), + ExtraProperties = table.Column(type: "text", nullable: false), + ConcurrencyStamp = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), + CreationTime = table.Column(type: "timestamp without time zone", nullable: false), + CreatorId = table.Column(type: "uuid", nullable: true), + LastModificationTime = table.Column(type: "timestamp without time zone", nullable: true), + LastModifierId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ReportsHistories", x => x.Id); + table.ForeignKey( + name: "FK_ReportsHistories_Applicants_ApplicantId", + column: x => x.ApplicantId, + principalTable: "Applicants", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_ReportsHistories_ApplicantId", + table: "ReportsHistories", + column: "ApplicantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ReportsHistories"); + + migrationBuilder.DropColumn( + name: "ReportsComments", + table: "Applicants"); + + + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs index 500cae0f37..f05e882f74 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs @@ -679,6 +679,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("ExtraProperties"); + b.Property("IsArchived") + .HasColumnType("boolean"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -700,11 +703,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Published") .HasColumnType("boolean"); - b.Property("IsArchived") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false); - b.Property("ReportColumns") .IsRequired() .HasColumnType("text"); @@ -747,6 +745,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasColumnName("CreatorId"); + b.Property("Definition") + .HasColumnType("jsonb"); + b.Property("DeleterId") .HasColumnType("uuid") .HasColumnName("DeleterId"); @@ -776,9 +777,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Order") .HasColumnType("bigint"); - b.Property("Definition") - .HasColumnType("jsonb"); - b.Property("TenantId") .HasColumnType("uuid") .HasColumnName("TenantId"); @@ -885,6 +883,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RedStop") .HasColumnType("boolean"); + b.Property("ReportsComments") + .HasColumnType("text"); + b.Property("Sector") .HasColumnType("text"); @@ -2364,6 +2365,68 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("IssueTrackings", (string)null); }); + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", 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("FiscalYear") + .HasColumnType("text"); + + b.Property("IncompleteReport") + .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("Outstanding") + .HasColumnType("boolean"); + + b.Property("ReportDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ApplicantId"); + + b.ToTable("ReportsHistories", (string)null); + }); + modelBuilder.Entity("Unity.GrantManager.Assessments.Assessment", b => { b.Property("Id") @@ -4676,6 +4739,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ApplicantId"); }); + modelBuilder.Entity("Unity.GrantManager.Applications.ReportsHistory", 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") diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ReportsHistoryRepository.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ReportsHistoryRepository.cs new file mode 100644 index 0000000000..a4386901bb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Repositories/ReportsHistoryRepository.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Unity.GrantManager.Applications; +using Unity.GrantManager.EntityFrameworkCore; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Unity.GrantManager.Repositories; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IReportsHistoryRepository))] +public class ReportsHistoryRepository : EfCoreRepository, IReportsHistoryRepository +{ + public ReportsHistoryRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async Task> GetByApplicantIdAsync(Guid applicantId) + { + var dbContext = await GetDbContextAsync(); + return await dbContext.ReportsHistories + .Where(x => x.ApplicantId == applicantId) + .ToListAsync(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml new file mode 100644 index 0000000000..76a8e1fc29 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml @@ -0,0 +1,81 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.CreateReportsHistoryModal +@{ + Layout = null; +} + +
+ + + + + +
+ + +
Fiscal Year is required.
+
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ + +
+
+ + + + +
+
+ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml.cs new file mode 100644 index 0000000000..bd6e691cb5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/CreateReportsHistoryModal.cshtml.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class CreateReportsHistoryModal : AbpPageModel +{ + [BindProperty] + public ReportsHistoryModalViewModel? ReportsHistoryForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public CreateReportsHistoryModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public void OnGet(Guid applicantId) + { + ReportsHistoryForm = new ReportsHistoryModalViewModel + { + ApplicantId = applicantId + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateReportsHistoryDto + { + ApplicantId = ReportsHistoryForm!.ApplicantId, + FiscalYear = ReportsHistoryForm.FiscalYear, + ReportDate = ReportsHistoryForm.ReportDate, + Outstanding = ReportsHistoryForm.Outstanding, + IncompleteReport = ReportsHistoryForm.IncompleteReport, + Note = ReportsHistoryForm.Note + }; + + await _applicantHistoryAppService.CreateReportsHistoryAsync(dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml new file mode 100644 index 0000000000..fbe0aa0997 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml @@ -0,0 +1,82 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Unity.GrantManager.Web.Pages.ApplicantHistory.EditReportsHistoryModal +@{ + Layout = null; +} + +
+ + + + + + +
+ + +
Fiscal Year is required.
+
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ + +
+
+ + + + +
+
+ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml.cs new file mode 100644 index 0000000000..de58139700 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/EditReportsHistoryModal.cshtml.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.ApplicantProfile; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class EditReportsHistoryModal : AbpPageModel +{ + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public ReportsHistoryModalViewModel? ReportsHistoryForm { get; set; } + + private readonly IApplicantHistoryAppService _applicantHistoryAppService; + + public EditReportsHistoryModal(IApplicantHistoryAppService applicantHistoryAppService) + { + _applicantHistoryAppService = applicantHistoryAppService; + } + + public async Task OnGetAsync(Guid id) + { + Id = id; + var record = await _applicantHistoryAppService.GetReportsHistoryAsync(id); + ReportsHistoryForm = new ReportsHistoryModalViewModel + { + ApplicantId = record.ApplicantId ?? Guid.Empty, + FiscalYear = record.FiscalYear, + ReportDate = record.ReportDate, + Outstanding = record.Outstanding, + IncompleteReport = record.IncompleteReport, + Note = record.Note + }; + } + + public async Task OnPostAsync() + { + var dto = new CreateUpdateReportsHistoryDto + { + ApplicantId = ReportsHistoryForm!.ApplicantId, + FiscalYear = ReportsHistoryForm.FiscalYear, + ReportDate = ReportsHistoryForm.ReportDate, + Outstanding = ReportsHistoryForm.Outstanding, + IncompleteReport = ReportsHistoryForm.IncompleteReport, + Note = ReportsHistoryForm.Note + }; + + await _applicantHistoryAppService.UpdateReportsHistoryAsync(Id, dto); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/ReportsHistoryModalViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/ReportsHistoryModalViewModel.cs new file mode 100644 index 0000000000..c7cf9b8522 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/ReportsHistoryModalViewModel.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace Unity.GrantManager.Web.Pages.ApplicantHistory; + +public class ReportsHistoryModalViewModel +{ + [HiddenInput] + public Guid ApplicantId { get; set; } + + [DisplayName("Fiscal Year")] + public string? FiscalYear { get; set; } + + [DisplayName("Report Date")] + [DataType(DataType.Date)] + public DateTime? ReportDate { get; set; } + + [DisplayName("Outstanding")] + public bool? Outstanding { get; set; } + + [DisplayName("Incomplete Report")] + public bool? IncompleteReport { get; set; } + + [DisplayName("Note")] + public string? Note { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs index 8a97579040..b96412c2f9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewComponent.cs @@ -37,7 +37,8 @@ public async Task InvokeAsync(Guid applicantId) ApplicantId = applicantId, FundingHistoryComments = applicant.FundingHistoryComments, IssueTrackingComments = applicant.IssueTrackingComments, - AuditComments = applicant.AuditComments + AuditComments = applicant.AuditComments, + ReportsComments = applicant.ReportsComments }; return View(viewModel); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs index aaf1a25e63..c3ce4ef2f3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/ApplicantHistoryViewModel.cs @@ -8,4 +8,5 @@ public class ApplicantHistoryViewModel public string? FundingHistoryComments { get; set; } public string? IssueTrackingComments { get; set; } public string? AuditComments { get; set; } + public string? ReportsComments { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml index 5fa5f68f18..49557a0dc5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.cshtml @@ -92,5 +92,30 @@ rows="4">@Model.AuditComments
+ + @* ── Reports History ── *@ +
+
+
Reports History
+
+
+ Reports Notes +
+
+ +
+
+ + +
+ +
+ + +
+
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 c1b950c4696523e4ab9d7bd5dc61042e8195af82 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Wed, 6 May 2026 14:03:54 -0700 Subject: [PATCH 29/34] AB#32684: Register AmazonS3Client as singleton for reusability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AmazonS3Client internally owns an HttpClient, which maintains a pool of TCP connections to S3. Creating a new instance per request means a new connection pool per request — those pools are expensive to spin up and are never explicitly cleaned up when the class doesn't implement IDisposable. --- .../Emails/EmailAttachmentService.cs | 21 +++--------- .../NotificationsApplicationModule.cs | 17 ++++++++++ .../AttachmentPreviewAppService.cs | 33 +++---------------- .../Attachments/S3BlobProvider.cs | 32 +++++------------- .../GrantManagerApplicationModule.cs | 14 ++++++++ 5 files changed, 48 insertions(+), 69 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs index 6a0172dd08..03793d3383 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Emails/EmailAttachmentService.cs @@ -17,7 +17,7 @@ public class EmailAttachmentService : ITransientDependency { private const string S3BucketConfigKey = "S3:Bucket"; - private readonly AmazonS3Client _amazonS3Client; + private readonly IAmazonS3 _amazonS3Client; private readonly IEmailLogAttachmentRepository _emailLogAttachmentRepository; private readonly IConfiguration _configuration; private readonly ICurrentUser _currentUser; @@ -27,27 +27,14 @@ public EmailAttachmentService( IConfiguration configuration, IEmailLogAttachmentRepository emailLogAttachmentRepository, ICurrentUser currentUser, - ILogger logger) + ILogger logger, + IAmazonS3 amazonS3Client) { _configuration = configuration; _emailLogAttachmentRepository = emailLogAttachmentRepository; _currentUser = currentUser; _logger = logger; - - // Initialize S3 client (same pattern as S3BlobProvider) - var s3Config = new AmazonS3Config - { - RegionEndpoint = null, - ServiceURL = configuration["S3:Endpoint"], - AllowAutoRedirect = true, - ForcePathStyle = true - }; - - _amazonS3Client = new AmazonS3Client( - configuration["S3:AccessKeyId"], - configuration["S3:SecretAccessKey"], - s3Config - ); + _amazonS3Client = amazonS3Client; } public async Task UploadAttachmentAsync( diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs index 5cb5c5a912..dd57cecee1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs @@ -1,3 +1,5 @@ +using Amazon.S3; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; @@ -11,6 +13,7 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Http.Client; using Unity.Modules.Shared.Http; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Unity.Notifications; @@ -27,6 +30,20 @@ public class NotificationsApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { + var configuration = context.Services.GetConfiguration(); + + context.Services.TryAddSingleton(_ => + { + var s3Config = new AmazonS3Config + { + RegionEndpoint = null, + ServiceURL = configuration["S3:Endpoint"], + AllowAutoRedirect = true, + ForcePathStyle = true + }; + return new AmazonS3Client(configuration["S3:AccessKeyId"], configuration["S3:SecretAccessKey"], s3Config); + }); + context.Services.AddAutoMapperObjectMapper(); context.Services.AddTransient(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/AttachmentPreviewAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/AttachmentPreviewAppService.cs index df2c80b7ca..1a4fba4af6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/AttachmentPreviewAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/AttachmentPreviewAppService.cs @@ -10,11 +10,11 @@ namespace Unity.GrantManager.Attachments; -public class AttachmentPreviewAppService : ApplicationService, IAttachmentPreviewAppService, ITransientDependency, IDisposable +public class AttachmentPreviewAppService : ApplicationService, IAttachmentPreviewAppService, ITransientDependency { private readonly IFileAppService _fileAppService; private readonly ILibreOfficeConversionService _libreOfficeConversionService; - private readonly AmazonS3Client _amazonS3Client; + private readonly IAmazonS3 _amazonS3Client; private readonly string _bucket; private readonly string _applicationFolder; private readonly string _assessmentFolder; @@ -23,22 +23,12 @@ public class AttachmentPreviewAppService : ApplicationService, IAttachmentPrevie public AttachmentPreviewAppService( IFileAppService fileAppService, ILibreOfficeConversionService libreOfficeConversionService, - IConfiguration configuration) + IConfiguration configuration, + IAmazonS3 amazonS3Client) { _fileAppService = fileAppService; _libreOfficeConversionService = libreOfficeConversionService; - - var s3Config = new AmazonS3Config - { - RegionEndpoint = null, - ServiceURL = configuration["S3:Endpoint"], - AllowAutoRedirect = true, - ForcePathStyle = true - }; - _amazonS3Client = new AmazonS3Client( - configuration["S3:AccessKeyId"], - configuration["S3:SecretAccessKey"], - s3Config); + _amazonS3Client = amazonS3Client; _bucket = configuration["S3:Bucket"] ?? throw new InvalidOperationException("Missing server configuration: S3:Bucket"); _applicationFolder = NormalizeFolder(configuration["S3:ApplicationS3Folder"] ?? throw new InvalidOperationException("Missing server configuration: S3:ApplicationS3Folder")); @@ -165,17 +155,4 @@ private static string EscapeKeyFileName(string s3ObjectKey) if (lastSlash < 0) return Uri.EscapeDataString(s3ObjectKey); return s3ObjectKey[..(lastSlash + 1)] + Uri.EscapeDataString(s3ObjectKey[(lastSlash + 1)..]); } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _amazonS3Client.Dispose(); - } - } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/S3BlobProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/S3BlobProvider.cs index e97eeea408..c744885522 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/S3BlobProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Attachments/S3BlobProvider.cs @@ -1,19 +1,18 @@ -using Amazon.S3.Model; -using Amazon.S3; +using Amazon.S3; +using Amazon.S3.Model; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Primitives; using System; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Unity.GrantManager.Applications; using Volo.Abp.BlobStoring; using Volo.Abp.DependencyInjection; using Volo.Abp.Validation; -using Microsoft.AspNetCore.StaticFiles; -using Microsoft.Extensions.Configuration; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Routing; namespace Unity.GrantManager.Attachments; @@ -23,30 +22,15 @@ public partial class S3BlobProvider : BlobProviderBase, ITransientDependency private readonly IApplicationAttachmentRepository _applicationAttachmentRepository; private readonly IAssessmentAttachmentRepository _assessmentAttachmentRepository; private readonly IApplicantAttachmentRepository _applicantAttachmentRepository; - private readonly AmazonS3Client _amazonS3Client; + private readonly IAmazonS3 _amazonS3Client; - public S3BlobProvider(IHttpContextAccessor httpContextAccessor, IApplicationAttachmentRepository attachmentRepository, IAssessmentAttachmentRepository assessmentAttachmentRepository, IApplicantAttachmentRepository applicantAttachmentRepository, IConfiguration configuration) + public S3BlobProvider(IHttpContextAccessor httpContextAccessor, IApplicationAttachmentRepository attachmentRepository, IAssessmentAttachmentRepository assessmentAttachmentRepository, IApplicantAttachmentRepository applicantAttachmentRepository, IAmazonS3 amazonS3Client) { _httpContextAccessor = httpContextAccessor; _applicationAttachmentRepository = attachmentRepository; _assessmentAttachmentRepository = assessmentAttachmentRepository; _applicantAttachmentRepository = applicantAttachmentRepository; - - AmazonS3Config s3config = new() - { - RegionEndpoint = null, - ServiceURL = configuration["S3:Endpoint"], - AllowAutoRedirect = true, - ForcePathStyle = true - }; - - - AmazonS3Client s3Client = new( - configuration["S3:AccessKeyId"], - configuration["S3:SecretAccessKey"], - s3config - ); - _amazonS3Client = s3Client; + _amazonS3Client = amazonS3Client; } public override async Task DeleteAsync(BlobProviderDeleteArgs args) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs index b76bd5707e..d3651ffa93 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantManagerApplicationModule.cs @@ -1,3 +1,4 @@ +using Amazon.S3; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -47,6 +48,7 @@ using Unity.GrantManager.GrantsPortal.Handlers; using Unity.GrantManager.Messaging; using Unity.GrantManager.Analytics; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Unity.GrantManager; @@ -131,6 +133,18 @@ public override void ConfigureServices(ServiceConfigurationContext context) options.AddMaps(); }); + context.Services.TryAddSingleton(_ => + { + var s3Config = new AmazonS3Config + { + RegionEndpoint = null, + ServiceURL = configuration["S3:Endpoint"], + AllowAutoRedirect = true, + ForcePathStyle = true + }; + return new AmazonS3Client(configuration["S3:AccessKeyId"], configuration["S3:SecretAccessKey"], s3Config); + }); + context.Services.AddSingleton(); context.Services.AddTransient(); context.Services.AddTransient(); From 23a56fe43b123ca3c4f276e898b2f93dee963f39 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 6 May 2026 14:23:14 -0700 Subject: [PATCH 30/34] 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 31/34] 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"); } From bb7c7f6f5f11d421be63ea123409c60696bc2fad Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 6 May 2026 17:22:50 -0700 Subject: [PATCH 32/34] AB#32452 remove prompt tools panel --- .../PromptTools/AIPromptToolAccessProvider.cs | 45 -- .../IAIPromptToolAccessProvider.cs | 9 - .../Pages/GrantApplications/Details.cshtml | 96 +--- .../Pages/GrantApplications/Details.cshtml.cs | 12 - .../Pages/GrantApplications/Details.css | 81 --- .../Pages/GrantApplications/Details.js | 506 ------------------ .../Pages/GrantApplications/ai-analysis.js | 5 +- .../AssessmentScoresWidget/Default.js | 5 +- .../ChefsAttachments/ChefsAttachments.js | 8 +- .../Components/ReviewList/ReviewList.js | 183 ++++--- 10 files changed, 99 insertions(+), 851 deletions(-) delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolAccessProvider.cs delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolAccessProvider.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolAccessProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolAccessProvider.cs deleted file mode 100644 index 51d7221147..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/AIPromptToolAccessProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Configuration; -using System.Threading.Tasks; -using Unity.Modules.Shared.Permissions; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Security.Claims; - -namespace Unity.AI.Web.PromptTools; - -public class AIPromptToolAccessProvider( - IAuthorizationService authorizationService, - ICurrentPrincipalAccessor currentPrincipalAccessor, - IConfiguration configuration) : IAIPromptToolAccessProvider, ITransientDependency -{ - public async Task CanViewPromptToolsAsync() - { - var principal = currentPrincipalAccessor.Principal; - if (principal?.Identity?.IsAuthenticated != true) - { - return false; - } - - var authorizationResult = await authorizationService.AuthorizeAsync( - principal, - IdentityConsts.ITOperationsPolicyName); - - return authorizationResult.Succeeded; - } - - public string DefaultPromptVersion - { - get - { - var configuredPromptVersion = configuration["Azure:Operations:Defaults:PromptVersion"]; - if (string.IsNullOrWhiteSpace(configuredPromptVersion)) - { - configuredPromptVersion = configuration["Azure:OpenAI:PromptVersion"]; - } - - return string.IsNullOrWhiteSpace(configuredPromptVersion) - ? "v1" - : configuredPromptVersion.Trim().ToLowerInvariant(); - } - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolAccessProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolAccessProvider.cs deleted file mode 100644 index 80d0560ba7..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/PromptTools/IAIPromptToolAccessProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Unity.AI.Web.PromptTools; - -public interface IAIPromptToolAccessProvider -{ - Task CanViewPromptToolsAsync(); - string DefaultPromptVersion { get; } -} 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 6c970406ee..1b2cd15d4d 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 @@ -286,13 +286,7 @@ } - @if (Model.CanViewPromptTools) - { - - } - +
@@ -512,93 +506,7 @@
} @*-------- AI Analysis Tab Section END ---------*@ - @if (Model.CanViewPromptTools) - { -
-
-
Prompt Tools
-
-
- -
-
-
-
- -
-
-
Attachment Summary
- -
-
-
- -
- -
-
- -
-
-
Application Analysis
- -
-
-
- -
- -
-
- -
-
-
Application Scoring
- -
-
-
- -
- -
-
-
-
- } -
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs index 829d28a058..151093e328 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs @@ -16,7 +16,6 @@ using Unity.GrantManager.Applications; using Unity.GrantManager.Flex; using Unity.GrantManager.GrantApplications; -using Unity.AI.Web.PromptTools; using Unity.GrantManager.Zones; using Unity.Modules.Shared.Correlation; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; @@ -33,7 +32,6 @@ public class DetailsModel : AbpPageModel private readonly IApplicationFormVersionAppService _applicationFormVersionAppService; private readonly IScoresheetRepository _scoresheetRepository; private readonly IFeatureChecker _featureChecker; - private readonly IAIPromptToolAccessProvider _aiPromptToolAccessProvider; protected readonly IZoneManagementAppService _zoneManagementAppService; [BindProperty(SupportsGet = true)] @@ -95,12 +93,6 @@ public class DetailsModel : AbpPageModel [BindProperty] public HashSet ZoneStateSet { get; set; } = []; - [BindProperty(SupportsGet = true)] - public bool CanViewPromptTools { get; set; } - - [BindProperty(SupportsGet = true)] - public string DefaultPromptVersion { get; set; } - [BindProperty(SupportsGet = true)] public string? ApplicationScoresheetSchemaJson { get; set; } @@ -112,7 +104,6 @@ public DetailsModel( IFeatureChecker featureChecker, ICurrentUser currentUser, IConfiguration configuration, - IAIPromptToolAccessProvider aiPromptToolAccessProvider, IZoneManagementAppService zoneManagementAppService) { _grantApplicationAppService = grantApplicationAppService; @@ -121,7 +112,6 @@ public DetailsModel( _applicationFormVersionAppService = applicationFormVersionAppService; _scoresheetRepository = scoresheetRepository; _zoneManagementAppService = zoneManagementAppService; - _aiPromptToolAccessProvider = aiPromptToolAccessProvider; CurrentUserId = currentUser.Id; CurrentUserName = currentUser.SurName + ", " + currentUser.Name; @@ -129,12 +119,10 @@ public DetailsModel( MaxFileSize = configuration["S3:MaxFileSize"] ?? ""; EmailAttachmentMaxFileSize = configuration["S3:EmailAttachmentMaxFileSize"] ?? "20"; TotalEmailAttachmentMaxFileSize = configuration["S3:EmailAttachmentsTotalMaxFileSize"] ?? "25"; - DefaultPromptVersion = aiPromptToolAccessProvider.DefaultPromptVersion; } public async Task OnGetAsync() { - CanViewPromptTools = await _aiPromptToolAccessProvider.CanViewPromptToolsAsync(); ApplicationFormSubmission applicationFormSubmission = await _grantApplicationAppService.GetFormSubmissionByApplicationId(ApplicationId); ZoneStateSet = await _zoneManagementAppService.GetZoneStateSetAsync(applicationFormSubmission.ApplicationFormId); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css index 7cd0f382d1..2262bfdf1c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.css @@ -724,84 +724,3 @@ form label.error { background-color: #f9fafb; border-color: #9ca3af; } -.prompt-tools-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem 1rem; - flex-wrap: nowrap; -} - -.prompt-tools-header .prompt-tools-toolbar-row { - display: flex; - align-items: center; - gap: 0.5rem 0.75rem; - margin-left: auto; -} - -.prompt-tools-header select, -.prompt-tools-header .form-select { - width: auto; - white-space: nowrap; -} - -.prompt-tools-section { - margin-bottom: 1rem; - padding-bottom: 1rem; - border-bottom: 1px solid #e7ebef; -} - -.prompt-tools-section:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: 0; -} - -.prompt-tools-section-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem 1rem; - margin-bottom: 0.5rem; -} - -.prompt-tools-section-header h6 { - min-width: 0; - flex: 1 1 auto; -} - -.prompt-tools-output-container { - position: relative; -} - -.prompt-tools-output-container .prompt-tools-output-actions { - position: absolute; - top: 0.375rem; - right: 0.5rem; - display: inline-flex; - align-items: center; - gap: 0.25rem; - z-index: 1; - padding: 0 0.25rem; - background: #ffffff; - border-radius: 999px; -} - -.prompt-tools-output-container .prompt-tools-output { - min-height: 10rem; - max-height: 24rem; - overflow: auto; - resize: vertical; - white-space: pre; - font-family: Consolas, "Courier New", monospace; - line-height: 1.35; - padding-top: 0.75rem; - padding-right: 2.5rem; -} - -.prompt-tools-output-container .prompt-tools-output-copy-btn { - width: 2rem; - height: 2rem; - padding: 0; - color: #5c6b7a; -} 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 dbdca9ddda..72b1bd1b33 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 @@ -2,507 +2,7 @@ * Grant Application Details Page * Dependencies: ai-analysis.js - handles AI analysis rendering and management */ -function formatJsonOrRaw(value) { - if (!value) { - return ''; - } - - if (typeof value !== 'string') { - return JSON.stringify(value, null, 2); - } - - try { - return JSON.stringify(JSON.parse(value), null, 2); - } catch { - return value; - } -} - -function formatSectionBody(title, value) { - if (!value) { - return ''; - } - - return `${title}:\n${value}`; -} - -function formatOutputBody(title, sections) { - const content = sections.filter(Boolean).join('\n\n'); - if (!content) { - return ''; - } - - return `${title}\n\n${content}`; -} - -function getAIGenerationFailureMessage(operationType) { - switch (operationType) { - case 'attachment-summary': - return 'AI attachment summary failed.'; - case 'application-scoring': - return 'AI application scoring failed.'; - default: - return 'AI operation failed.'; - } -} - -function unwrapWhenResult(result) { - if ( - Array.isArray(result) && - result.length === 3 && - typeof result[1] === 'string' - ) { - return result[0]; - } - - return result; -} - -function extractSubmissionDataObject(root) { - if (!root || typeof root !== 'object' || Array.isArray(root)) { - return null; - } - - if (root.data && typeof root.data === 'object' && !Array.isArray(root.data)) { - return root.data; - } - - if ( - root.submission && - typeof root.submission === 'object' && - !Array.isArray(root.submission) && - root.submission.data && - typeof root.submission.data === 'object' && - !Array.isArray(root.submission.data) - ) { - return root.submission.data; - } - - return root; -} - -function formatTimestamp(value) { - if (!value) { - return ''; - } - - const timestamp = new Date(value); - if (Number.isNaN(timestamp.getTime())) { - return ''; - } - - return timestamp.toLocaleString(); -} - -function getAttachmentSummaryValue(attachment) { - return attachment?.aiSummary ?? attachment?.aISummary ?? ''; -} - -function formatAttachmentSummaryBody(attachments) { - if (!Array.isArray(attachments) || attachments.length === 0) { - return ''; - } - - const summarizedAttachments = attachments.filter( - (attachment) => { - const summary = getAttachmentSummaryValue(attachment); - return summary && summary.trim() !== ''; - } - ); - - if (summarizedAttachments.length === 0) { - return ''; - } - - return summarizedAttachments.map(function(attachment) { - const summary = getAttachmentSummaryValue(attachment); - return [ - 'NAME:', - attachment.fileName || '', - '', - 'SUMMARY:', - summary - ].join('\n'); - }).join('\n\n----------------------------------------\n\n'); -} - -function formatAttachmentSummaryJson(attachments) { - if (!Array.isArray(attachments) || attachments.length === 0) { - return ''; - } - - const summarizedAttachments = attachments - .map((attachment) => { - const summary = getAttachmentSummaryValue(attachment); - if (!summary || summary.trim() === '') { - return null; - } - - return { - name: attachment.fileName || '', - summary - }; - }) - .filter((attachment) => attachment !== null); - - if (summarizedAttachments.length === 0) { - return ''; - } - - return JSON.stringify(summarizedAttachments, null, 2); -} - $(function () { - const excludedPromptDataKeys = new Set([ - 'simplefile', - 'applicantAgent', - 'submit', - 'lateEntry', - 'metadata', - 'full_application_form_submission', - 'files', - 'file', - 'attachments' - ]); - - const nonDataComponentTypes = new Set([ - 'button', - 'simplebuttonadvanced', - 'html', - 'htmlelement', - 'content', - 'simpleseparator' - ]); - - globalThis.getSelectedPromptVersion = function() { - return $('#promptVersion').val() || null; - }; - - function setPromptToolsOutput(selector, value) { - $(selector).val(value || ''); - } - - function setPromptToolsTimestamp(selector, value) { - $(selector).text(value ? `(${value})` : ''); - } - - function getScoresheetSchemaJson() { - return $('#ApplicationScoresheetSchemaJson').val() || - $('#AssessmentScoresheetSchemaJson').val() || - ''; - } - - function hasPromptTools() { - return $('#prompt-tools').length > 0 && $('#promptVersion').length > 0; - } - - function getPromptDataPayload() { - const submissionJson = $('#ApplicationFormSubmissionData').val(); - if (!submissionJson) { - return ''; - } - - try { - const root = JSON.parse(submissionJson); - const submissionData = extractSubmissionDataObject(root); - if (!submissionData || typeof submissionData !== 'object' || Array.isArray(submissionData)) { - return ''; - } - - const filteredValues = { ...submissionData }; - for (const key of excludedPromptDataKeys) { - delete filteredValues[key]; - } - - const allowedSchemaKeys = extractAllowedSchemaKeys($('#ApplicationFormSchema').val()); - const payload = allowedSchemaKeys.size > 0 - ? Object.fromEntries( - Object.entries(filteredValues).filter(([key]) => allowedSchemaKeys.has(key)) - ) - : filteredValues; - - return JSON.stringify(payload, null, 2); - } catch { - return ''; - } - } - - function extractAllowedSchemaKeys(formSchema) { - if (!formSchema) { - return new Set(); - } - - try { - const schema = JSON.parse(formSchema); - const keys = new Set(); - extractSchemaKeys(schema.components, keys); - return keys; - } catch { - return new Set(); - } - } - - function extractSchemaKeys(components, keys) { - if (!Array.isArray(components)) { - return; - } - - for (const component of components) { - if (!component || typeof component !== 'object') { - continue; - } - - const key = component.key; - const type = component.type; - const isInput = component.input === true; - - if ( - typeof key === 'string' && - typeof type === 'string' && - !nonDataComponentTypes.has(type.toLowerCase()) && - isInput - ) { - keys.add(key); - } - - if (Array.isArray(component.components)) { - extractSchemaKeys(component.components, keys); - } - - if (Array.isArray(component.columns)) { - for (const column of component.columns) { - if (column && Array.isArray(column.components)) { - extractSchemaKeys(column.components, keys); - } - } - } - } - } - - function formatAttachmentAiOutput(attachments) { - const attachmentBody = formatAttachmentSummaryBody(attachments); - if (!attachmentBody) { - setPromptToolsTimestamp('#attachmentOutputTimestamp', ''); - return ''; - } - - const summarizedAttachments = attachments.filter( - (attachment) => { - const summary = getAttachmentSummaryValue(attachment); - return summary && summary.trim() !== ''; - } - ); - - const latestTimestamp = summarizedAttachments - .map((attachment) => attachment.lastModificationTime || attachment.creationTime || null) - .filter((timestamp) => !!timestamp) - .sort() - .at(-1); - - setPromptToolsTimestamp('#attachmentOutputTimestamp', formatTimestamp(latestTimestamp)); - return attachmentBody; - } - - function loadPromptToolsOutputs() { - if (!hasPromptTools()) { - return; - } - - const applicationId = $('#DetailsViewApplicationId').val(); - - if (!applicationId) { - setPromptToolsOutput('#analysisOutput', ''); - setPromptToolsOutput('#scoringOutput', ''); - setPromptToolsOutput('#attachmentOutput', ''); - setPromptToolsTimestamp('#analysisOutputTimestamp', ''); - setPromptToolsTimestamp('#scoringOutputTimestamp', ''); - setPromptToolsTimestamp('#attachmentOutputTimestamp', ''); - return; - } - - $.when( - unity.grantManager.grantApplications.grantApplication.get(applicationId), - unity.grantManager.attachments.attachment.getApplicationChefsFileAttachments(applicationId) - ) - .done(function(applicationResponse, attachmentsResponse) { - const application = unwrapWhenResult(applicationResponse); - const attachments = unwrapWhenResult(attachmentsResponse); - const updatedAt = application?.lastModificationTime || application?.creationTime || null; - const formattedUpdatedAt = formatTimestamp(updatedAt); - const attachmentSection = formatSectionBody('ATTACHMENTS', formatAttachmentSummaryJson(attachments)); - setPromptToolsTimestamp('#analysisOutputTimestamp', formattedUpdatedAt); - setPromptToolsTimestamp('#scoringOutputTimestamp', formattedUpdatedAt); - setPromptToolsOutput( - '#analysisOutput', - formatOutputBody('APPLICATION ANALYSIS', [ - formatSectionBody('DATA', getPromptDataPayload()), - attachmentSection, - formatSectionBody( - 'OUTPUT', - formatJsonOrRaw(application?.aiAnalysisData ?? application?.aiAnalysis ?? '') - ) - ]) - ); - setPromptToolsOutput( - '#scoringOutput', - formatOutputBody('APPLICATION SCORING', [ - formatSectionBody('SCORESHEET', formatJsonOrRaw(getScoresheetSchemaJson())), - formatSectionBody('DATA', getPromptDataPayload()), - attachmentSection, - formatSectionBody( - 'OUTPUT', - formatJsonOrRaw(application?.aiScoresheetAnswers ?? application?.aIScoresheetAnswers ?? '') - ) - ]) - ); - setPromptToolsOutput( - '#attachmentOutput', - formatOutputBody('ATTACHMENT SUMMARY', [formatAttachmentAiOutput(attachments)]) - ); - }) - .fail(function() { - setPromptToolsOutput('#analysisOutput', ''); - setPromptToolsOutput('#scoringOutput', ''); - setPromptToolsOutput('#attachmentOutput', ''); - setPromptToolsTimestamp('#analysisOutputTimestamp', ''); - setPromptToolsTimestamp('#scoringOutputTimestamp', ''); - setPromptToolsTimestamp('#attachmentOutputTimestamp', ''); - }); - } - - let aiGenerationPollTimeoutId = null; - const aiGenerationPollIntervalMs = 15000; - - function stopAIGenerationPolling() { - if (aiGenerationPollTimeoutId) { - clearTimeout(aiGenerationPollTimeoutId); - aiGenerationPollTimeoutId = null; - } - } - - function pollAIGenerationStatus(applicationId, operationType, promptVersion, restoreButton, originalHtml) { - const poll = function() { - unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(applicationId, operationType, promptVersion) - .done(function(request) { - const statusText = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - - if (statusText === 'Failed') { - stopAIGenerationPolling(); - globalThis.AIGenerationButtonState?.restore(restoreButton); - restoreButton.html(originalHtml).prop('disabled', false); - loadPromptToolsOutputs(); - abp.message.error(request?.failureReason || getAIGenerationFailureMessage(operationType)); - return; - } - - if (!request || request.isActive === false || statusText === 'Completed') { - stopAIGenerationPolling(); - setPromptToolsTimestamp('#analysisOutputTimestamp', request?.completedAt || request?.startedAt || null); - setPromptToolsTimestamp('#scoringOutputTimestamp', request?.completedAt || request?.startedAt || null); - loadPromptToolsOutputs(); - globalThis.AIGenerationButtonState?.setCompleted(restoreButton); - restoreButton.html('Completed').prop('disabled', true); - return; - } - - aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); - }) - .fail(function() { - aiGenerationPollTimeoutId = setTimeout(poll, aiGenerationPollIntervalMs); - }); - }; - - stopAIGenerationPolling(); - aiGenerationPollTimeoutId = setTimeout(poll, 500); - } - - function queueAIGenerationOperation(queueAction, operationType, failureMessage, restoreButton, originalHtml) { - queueAction() - .done(function(request) { - pollAIGenerationStatus( - $('#DetailsViewApplicationId').val(), - operationType, - globalThis.getSelectedPromptVersion?.() || null, - restoreButton, - originalHtml - ); - }) - .fail(function() { - abp.message.error(failureMessage); - globalThis.AIGenerationButtonState?.restore(restoreButton); - 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('Generating...') - .prop('disabled', true); - globalThis.AIGenerationButtonState?.setGenerating($button); - - queueAIGenerationOperation( - () => unity.grantManager.grantApplications.grantApplication.queueAttachmentSummary(applicationId, promptVersion), - 'attachment-summary', - '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('Generating...') - .prop('disabled', true); - globalThis.AIGenerationButtonState?.setGenerating($button); - - queueAIGenerationOperation( - () => unity.grantManager.grantApplications.grantApplication.queueApplicationScoring(applicationId, promptVersion), - 'application-scoring', - 'Failed to queue AI application scoring. Please try again.', - $button, - existingHtml - ); - }; - - globalThis.refreshPromptToolsOutputs = loadPromptToolsOutputs; - - $(document).on('click', '.prompt-tools-output-copy-btn', async function () { - const targetSelector = $(this).data('target'); - const text = $(targetSelector).val(); - - if (!targetSelector || !text) { - return; - } - - try { - await navigator.clipboard.writeText(text); - abp.notify.success('Copied AI output.'); - } catch { - const output = $(targetSelector); - output.trigger('focus'); - output.trigger('select'); - } - }); - let selectedReviewDetails = null; let renderFormIoToHtml = document.getElementById('RenderFormIoToHtml').value; @@ -531,7 +31,6 @@ $(function () { updateLinksCounters(); renderSubmission(); loadAIAnalysis(); - loadPromptToolsOutputs(); applyTabHeightOffset(); } @@ -819,11 +318,6 @@ $(function () { PubSub.subscribe('refresh_assessment_scores', (msg, data) => { assessmentScoresWidgetManager.refresh(); updateSubtotal(); - loadPromptToolsOutputs(); - }); - - PubSub.subscribe('refresh_chefs_attachment_list', () => { - loadPromptToolsOutputs(); }); PubSub.subscribe('select_application_review', (msg, data) => { 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 8bd0df421a..885a705ca9 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 @@ -415,7 +415,6 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { const applicationId = $('#DetailsViewApplicationId').val(); const $button = triggerButton ? $(triggerButton) : $('#regenerateApplicationAnalysis'); const existingHtml = $button.html(); - const promptVersion = globalThis.getSelectedPromptVersion?.() || null; const aiAnalysisPollIntervalMs = 15000; const aiAnalysisMaxPollFailures = 3; @@ -439,7 +438,7 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { const poll = function() { unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(applicationId, 'application-analysis', promptVersion) + .getAIGenerationStatus(applicationId, 'application-analysis') .done(function(request) { aiAnalysisPollFailures = 0; const statusText = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; @@ -479,7 +478,7 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { }; unity.grantManager.grantApplications.grantApplication - .queueApplicationAnalysis(applicationId, promptVersion) + .queueApplicationAnalysis(applicationId) .done(function(request) { aiAnalysisPollFailures = 0; stopAIAnalysisPolling(); 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 1e7dd26748..e163dcf415 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 @@ -584,7 +584,6 @@ function queueApplicationScoring(triggerButton = null) { const applicationId = $('#DetailsViewApplicationId').val(); const $button = triggerButton ? $(triggerButton) : $('#regenerateAiScoresheetBtn'); const existingHtml = $button.html(); - const promptVersion = globalThis.getSelectedPromptVersion?.() || null; const aiGenerationPollIntervalMs = 15000; let aiGenerationPollTimeoutId = null; @@ -608,7 +607,7 @@ function queueApplicationScoring(triggerButton = null) { const poll = function () { unity.grantManager.grantApplications.grantApplication - .getAIGenerationStatus(applicationId, 'application-scoring', promptVersion) + .getAIGenerationStatus(applicationId, 'application-scoring') .done(function (request) { const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; @@ -636,7 +635,7 @@ function queueApplicationScoring(triggerButton = null) { }; unity.grantManager.grantApplications.applicationScoring - .queueApplicationScoring(applicationId, promptVersion) + .queueApplicationScoring(applicationId) .done(function (request) { const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; 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 03191486c5..4775432398 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 @@ -170,7 +170,6 @@ $(function () { const rowsToProcess = triggerButton ? chefsDataTable.rows().data() : chefsDataTable.rows({ selected: true }).data(); - const promptVersion = globalThis.getSelectedPromptVersion?.() || null; $button.removeData('trigger-button'); @@ -189,10 +188,7 @@ $(function () { // Call the backend API $.ajax({ - url: - '/api/app/attachment-summary/generate-attachment-summaries' + - '?promptVersion=' + - encodeURIComponent(promptVersion || ''), + url: '/api/app/attachment-summary/generate-attachment-summaries', data: JSON.stringify(attachmentIds), contentType: 'application/json', type: 'POST', @@ -627,4 +623,4 @@ function showChefsAPIAccessError() { confirmButton: 'btn btn-primary', }, }); -} \ No newline at end of file +} 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 257407605c..0da7f980ce 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 @@ -455,98 +455,97 @@ function unityWorkflowButtonAction(e, dt, button, config) { } } -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 = globalThis.AIGenerationButtonState?.resolveStatus(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) { - const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; - - if (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'); +function generateAiButtonAction(e, dt, button, config) { + const $button = button?.node ? $(button.node) : 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') + .done(function (request) { + const status = globalThis.AIGenerationButtonState?.resolveStatus(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) + .done(function (request) { + const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; + + if (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( "Completed Successfully", From e97cbc59b032bc7648d2e4a234b2e936c9edc7f0 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 7 May 2026 08:50:29 -0700 Subject: [PATCH 33/34] AB#32452 fix scoring queue proxy --- .../Views/Shared/Components/AssessmentScoresWidget/Default.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e163dcf415..b697de046a 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 @@ -634,7 +634,7 @@ function queueApplicationScoring(triggerButton = null) { }); }; - unity.grantManager.grantApplications.applicationScoring + unity.grantManager.grantApplications.grantApplication .queueApplicationScoring(applicationId) .done(function (request) { const status = globalThis.AIGenerationButtonState?.resolveStatus(request?.status) ?? ''; From ec9b4eaa720c870f1f7c017bf3bb039c08d26572 Mon Sep 17 00:00:00 2001 From: aurelio-aot Date: Thu, 7 May 2026 12:25:55 -0700 Subject: [PATCH 34/34] AB#32684: Fix sonarqube issues --- .../NotificationsApplicationModule.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs index 7ed6ea1f97..828e253693 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/NotificationsApplicationModule.cs @@ -1,5 +1,4 @@ using Amazon.S3; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Mapperly; using Volo.Abp.Modularity;