diff --git a/api/src/Feature.Form.Submissions/FormSubmissionsInstaller.cs b/api/src/Feature.Form.Submissions/FormSubmissionsInstaller.cs index e53fc0751..4075d94b1 100644 --- a/api/src/Feature.Form.Submissions/FormSubmissionsInstaller.cs +++ b/api/src/Feature.Form.Submissions/FormSubmissionsInstaller.cs @@ -13,6 +13,7 @@ public static IServiceCollection AddFormSubmissionsFeature(this IServiceCollecti SqlMapper.AddTypeHandler(typeof(BaseQuestionModel[]), new JsonToObjectConverter()); SqlMapper.AddTypeHandler(typeof(BaseAnswerModel[]), new JsonToObjectConverter()); SqlMapper.AddTypeHandler(typeof(NoteModel[]), new JsonToObjectConverter()); + SqlMapper.AddTypeHandler(typeof(AggregatedSubmissionsAttachmentModel[]), new JsonToObjectConverter()); SqlMapper.AddTypeHandler(typeof(AttachmentModel[]), new JsonToObjectConverter()); SqlMapper.AddTypeHandler(typeof(ObservationBreakModel[]), new JsonToObjectConverter()); diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs index a1b98c7ea..a9346e2bb 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs @@ -432,7 +432,17 @@ @HASATTACHMENTS IS NULL .ToList(); var attachments = submissions - .SelectMany(x => x.Attachments.Select(attachment => attachment with { SubmissionId = x.SubmissionId })); + .SelectMany(submission => submission.Attachments.Select(attachment => new AggregatedSubmissionsAttachmentModel() + { + SubmissionId = submission.SubmissionId, + QuestionId = attachment.QuestionId, + TimeSubmitted = attachment.TimeSubmitted, + MimeType = attachment.MimeType, + MonitoringObserverId = submission.MonitoringObserverId, + FilePath = attachment.FilePath, + UploadedFileName = attachment.UploadedFileName, + FileName = attachment.FileName, + } )); attachments = await Task.WhenAll( attachments.Select(async attachment => diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Response.cs b/api/src/Feature.Form.Submissions/GetAggregated/Response.cs index 763ab8005..049f2dee2 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Response.cs +++ b/api/src/Feature.Form.Submissions/GetAggregated/Response.cs @@ -8,7 +8,7 @@ public class Response { public SubmissionsFilterModel SubmissionsFilter { get; set; } public FormSubmissionsAggregate SubmissionsAggregate { get; set; } - public List Attachments { get; set; } + public List Attachments { get; set; } public List Notes { get; set; } } diff --git a/api/src/Feature.Form.Submissions/GetById/Endpoint.cs b/api/src/Feature.Form.Submissions/GetById/Endpoint.cs index 5ddef5729..f548cc937 100644 --- a/api/src/Feature.Form.Submissions/GetById/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/GetById/Endpoint.cs @@ -33,21 +33,14 @@ WITH submissions AS (SELECT psi."Id" AS "SubmissionId", 'PSI' AS "FormType", 'PSI' AS "FormCode", + psif."Name" as "FormName", psi."PollingStationId", psi."MonitoringObserverId", psi."Answers", - (SELECT "Questions" - FROM "PollingStationInformationForms" - WHERE "ElectionRoundId" = @electionRoundId) AS "Questions", - (SELECT "DefaultLanguage" - FROM "PollingStationInformationForms" - WHERE "ElectionRoundId" = @electionRoundId) AS "DefaultLanguage", - (SELECT "Languages" - FROM "PollingStationInformationForms" - WHERE "ElectionRoundId" = @electionRoundId) AS "Languages", - (SELECT "Id" - FROM "PollingStationInformationForms" - WHERE "ElectionRoundId" = @electionRoundId) AS "FormId", + psif."Questions" AS "Questions", + psif."DefaultLanguage" AS "DefaultLanguage", + psif."Languages" AS "Languages", + psif."Id" AS "FormId", psi."FollowUpStatus" as "FollowUpStatus", '[]'::jsonb AS "Attachments", '[]'::jsonb AS "Notes", @@ -58,12 +51,14 @@ WITH submissions AS psi."IsCompleted" FROM "PollingStationInformation" psi INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = psi."MonitoringObserverId" + INNER JOIN "PollingStationInformationForms" psif on psif."ElectionRoundId" = psi."ElectionRoundId" WHERE psi."Id" = @submissionId and psi."ElectionRoundId" = @electionRoundId UNION ALL SELECT fs."Id" AS "SubmissionId", f."FormType" AS "FormType", f."Code" AS "FormCode", + f."Name" AS "FormName", fs."PollingStationId", fs."MonitoringObserverId", fs."Answers", @@ -105,6 +100,7 @@ UNION ALL s."FormId", s."TimeSubmitted", s."FormCode", + s."FormName", s."FormType", ps."Id" AS "PollingStationId", ps."Level1", diff --git a/api/src/Feature.Form.Submissions/GetByIdV2/Endpoint.cs b/api/src/Feature.Form.Submissions/GetByIdV2/Endpoint.cs index 1309e14fd..e179c2667 100644 --- a/api/src/Feature.Form.Submissions/GetByIdV2/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/GetByIdV2/Endpoint.cs @@ -6,7 +6,7 @@ namespace Feature.Form.Submissions.GetByIdV2; public class Endpoint( IAuthorizationService authorizationService, INpgsqlConnectionFactory dbConnectionFactory, - IFileStorageService fileStorageService) : Endpoint, NotFound>> + IFileStorageService fileStorageService) : Endpoint, NotFound>> { public override void Configure() { @@ -18,7 +18,7 @@ public override void Configure() Policies(PolicyNames.NgoAdminsOnly); } - public override async Task, NotFound>> ExecuteAsync(Request req, + public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) { var authorizationResult = @@ -30,70 +30,63 @@ public override async Task, NotFound>> ExecuteAsy var sql = """ WITH submissions AS - (SELECT psi."Id" AS "SubmissionId", - 'PSI' AS "FormType", - 'PSI' AS "FormCode", - psi."PollingStationId", - psi."MonitoringObserverId", - psi."Answers", - (SELECT "Id" - FROM "PollingStationInformationForms" - WHERE "ElectionRoundId" = @electionRoundId) AS "FormId", - psi."FollowUpStatus" as "FollowUpStatus", - '[]'::jsonb AS "Attachments", - '[]'::jsonb AS "Notes", - "LastUpdatedAt" AS "TimeSubmitted", - psi."ArrivalTime", - psi."DepartureTime", - psi."Breaks", - psi."IsCompleted" - FROM "PollingStationInformation" psi - INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = psi."MonitoringObserverId" - WHERE psi."Id" = @submissionId and psi."ElectionRoundId" = @electionRoundId - UNION ALL - SELECT - fs."Id" AS "SubmissionId", - f."FormType" AS "FormType", - f."Code" AS "FormCode", - fs."PollingStationId", - fs."MonitoringObserverId", - fs."Answers", - f."Id" AS "FormId", - fs."FollowUpStatus", - COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'FileName', "FileName", 'MimeType', "MimeType", 'FilePath', "FilePath", 'UploadedFileName', "UploadedFileName", 'TimeSubmitted', "LastUpdatedAt")) - FROM "Attachments" a - WHERE - ( - (A."FormId" = FS."FormId" AND FS."PollingStationId" = A."PollingStationId") -- backwards compatibility - OR A."SubmissionId" = FS."Id" - ) - AND a."MonitoringObserverId" = fs."MonitoringObserverId" - AND a."IsDeleted" = false - AND a."IsCompleted" = true),'[]'::JSONB) AS "Attachments", + (SELECT psi."Id" AS "SubmissionId", + psi."PollingStationId", + psi."MonitoringObserverId", + psi."Answers", + psif."Id" AS "FormId", + psi."FollowUpStatus" as "FollowUpStatus", + '[]'::jsonb AS "Attachments", + '[]'::jsonb AS "Notes", + "LastUpdatedAt" AS "TimeSubmitted", + psi."ArrivalTime", + psi."DepartureTime", + psi."Breaks", + psi."IsCompleted" + FROM "PollingStationInformation" psi + INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = psi."MonitoringObserverId" + INNER JOIN "PollingStationInformationForms" psif on psi."ElectionRoundId" = psif."ElectionRoundId" + WHERE psi."Id" = @submissionId and psi."ElectionRoundId" = @electionRoundId + UNION ALL + SELECT + fs."Id" AS "SubmissionId", + fs."PollingStationId", + fs."MonitoringObserverId", + fs."Answers", + f."Id" AS "FormId", + fs."FollowUpStatus", + COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'FileName', "FileName", 'MimeType', "MimeType", 'FilePath', "FilePath", 'UploadedFileName', "UploadedFileName", 'TimeSubmitted', "LastUpdatedAt")) + FROM "Attachments" a + WHERE + ( + (A."FormId" = FS."FormId" AND FS."PollingStationId" = A."PollingStationId") -- backwards compatibility + OR A."SubmissionId" = FS."Id" + ) + AND a."MonitoringObserverId" = fs."MonitoringObserverId" + AND a."IsDeleted" = false + AND a."IsCompleted" = true),'[]'::JSONB) AS "Attachments", - COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'Text', "Text", 'TimeSubmitted', "LastUpdatedAt")) - FROM "Notes" n - WHERE - ( - (N."FormId" = FS."FormId" AND FS."PollingStationId" = N."PollingStationId") -- backwards compatibility - OR N."SubmissionId" = FS."Id" - ) - AND n."MonitoringObserverId" = fs."MonitoringObserverId"), '[]'::JSONB) AS "Notes", - - "LastUpdatedAt" AS "TimeSubmitted", - NULL AS "ArrivalTime", - NULL AS "DepartureTime", - '[]'::jsonb AS "Breaks", - fs."IsCompleted" - FROM "FormSubmissions" fs - INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = FS."MonitoringObserverId" - INNER JOIN "Forms" f ON f."Id" = fs."FormId" - WHERE fs."Id" = @submissionId and fs."ElectionRoundId" = @electionRoundId) + COALESCE((select jsonb_agg(jsonb_build_object('QuestionId', "QuestionId", 'Text', "Text", 'TimeSubmitted', "LastUpdatedAt")) + FROM "Notes" n + WHERE + ( + (N."FormId" = FS."FormId" AND FS."PollingStationId" = N."PollingStationId") -- backwards compatibility + OR N."SubmissionId" = FS."Id" + ) + AND n."MonitoringObserverId" = fs."MonitoringObserverId"), '[]'::JSONB) AS "Notes", + + "LastUpdatedAt" AS "TimeSubmitted", + NULL AS "ArrivalTime", + NULL AS "DepartureTime", + '[]'::jsonb AS "Breaks", + fs."IsCompleted" + FROM "FormSubmissions" fs + INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = FS."MonitoringObserverId" + INNER JOIN "Forms" f ON f."Id" = fs."FormId" + WHERE fs."Id" = @submissionId and fs."ElectionRoundId" = @electionRoundId) SELECT s."SubmissionId", s."FormId", s."TimeSubmitted", - s."FormCode", - s."FormType", ps."Id" AS "PollingStationId", ps."Level1", ps."Level2", @@ -117,8 +110,8 @@ UNION ALL s."Breaks", s."IsCompleted" FROM submissions s - INNER JOIN "PollingStations" ps ON ps."Id" = s."PollingStationId" - INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = s."MonitoringObserverId" + INNER JOIN "PollingStations" ps ON ps."Id" = s."PollingStationId" + INNER JOIN "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, 'Coalition') AMO on AMO."MonitoringObserverId" = s."MonitoringObserverId" """; var queryArgs = new @@ -126,11 +119,11 @@ FROM submissions s electionRoundId = req.ElectionRoundId, ngoId = req.NgoId, submissionId = req.SubmissionId }; - FormSubmissionView submission = null; + FormSubmissionViewV2 submission = null; using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct)) { - submission = await dbConnection.QueryFirstOrDefaultAsync(sql, queryArgs); + submission = await dbConnection.QueryFirstOrDefaultAsync(sql, queryArgs); } if (submission is null) diff --git a/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs b/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs index fd1c333b4..0d994b4a9 100644 --- a/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs @@ -147,7 +147,7 @@ WITH polling_station_submissions AS (SELECT psi."Id" AS psi."LastUpdatedAt" AS "TimeSubmitted", psi."FollowUpStatus", psif."DefaultLanguage", - psif."Name", + psif."Name" as "FormName", psi."IsCompleted" FROM "PollingStationInformation" psi INNER JOIN "PollingStationInformationForms" psif @@ -202,7 +202,7 @@ form_submissions AS (SELECT fs."Id" fs."LastUpdatedAt" AS "TimeSubmitted", fs."FollowUpStatus", f."DefaultLanguage", - f."Name", + f."Name" as "FormName", fs."IsCompleted" FROM "FormSubmissions" fs INNER JOIN "Forms" f ON f."Id" = fs."FormId" @@ -233,7 +233,7 @@ OR mo."PhoneNumber" ILIKE @searchText s."FormCode", s."FormType", s."DefaultLanguage", - s."Name" as "FormName", + s."FormName", ps."Id" AS "PollingStationId", ps."Level1", ps."Level2", diff --git a/api/src/Module.Answers/Models/FormSubmissionView.cs b/api/src/Module.Answers/Models/FormSubmissionView.cs index 3f5a7f0c7..9d06275c2 100644 --- a/api/src/Module.Answers/Models/FormSubmissionView.cs +++ b/api/src/Module.Answers/Models/FormSubmissionView.cs @@ -5,6 +5,7 @@ namespace Module.Answers.Models; +[Obsolete("Will be removed in future version")] public record FormSubmissionView { public Guid SubmissionId { get; init; } @@ -14,6 +15,7 @@ public record FormSubmissionView public string DefaultLanguage { get; init; } public string[] Languages { get; init; } = []; public FormType FormType { get; init; } = null!; + public TranslatedString FormName { get; init; } = null!; public SubmissionFollowUpStatus FollowUpStatus { get; init; } = null!; diff --git a/api/src/Module.Answers/Models/FormSubmissionViewV2.cs b/api/src/Module.Answers/Models/FormSubmissionViewV2.cs new file mode 100644 index 000000000..72d4bf7c2 --- /dev/null +++ b/api/src/Module.Answers/Models/FormSubmissionViewV2.cs @@ -0,0 +1,41 @@ +using Module.Forms.Models; +using Vote.Monitor.Core.Models; +using Vote.Monitor.Domain.Entities.FormSubmissionAggregate; + +namespace Module.Answers.Models; + +public record FormSubmissionViewV2 +{ + public Guid SubmissionId { get; init; } + public DateTime TimeSubmitted { get; init; } + public Guid FormId { get; init; } + + public SubmissionFollowUpStatus FollowUpStatus { get; init; } = null!; + + public Guid PollingStationId { get; init; } + public string Level1 { get; init; } = null!; + public string Level2 { get; init; } = null!; + public string Level3 { get; init; } = null!; + public string Level4 { get; init; } = null!; + public string Level5 { get; init; } = null!; + public string Number { get; init; } = null!; + public Guid MonitoringObserverId { get; init; } + public bool IsOwnObserver { get; init; } + public string ObserverName { get; init; } = null!; + public string Email { get; init; } = null!; + public string? PhoneNumber { get; init; } = null!; + public string[] Tags { get; init; } = []; + public string NgoName { get; init; } = null!; + public int NumberOfFlaggedAnswers { get; init; } + public int NumberOfQuestionsAnswered { get; init; } + + public BaseQuestionModel[] Questions { get; init; } + public BaseAnswerModel[] Answers { get; init; } = []; + public NoteModel[] Notes { get; init; } = []; + public AttachmentModel[] Attachments { get; init; } = []; + + public DateTime? ArrivalTime { get; init; } + public DateTime? DepartureTime { get; init; } + public ObservationBreakModel[] Breaks { get; init; } = []; + public bool IsCompleted { get; init; } +} diff --git a/api/src/Vote.Monitor.Core/Models/AggregatedSubmissionsAttachmentModel.cs b/api/src/Vote.Monitor.Core/Models/AggregatedSubmissionsAttachmentModel.cs new file mode 100644 index 000000000..4cf207e72 --- /dev/null +++ b/api/src/Vote.Monitor.Core/Models/AggregatedSubmissionsAttachmentModel.cs @@ -0,0 +1,16 @@ +namespace Vote.Monitor.Core.Models; + +public record AggregatedSubmissionsAttachmentModel +{ + public Guid SubmissionId { get; init; } + public Guid QuestionId { get; init; } + public Guid MonitoringObserverId { get; init; } + public string FilePath { get; init; } + public string UploadedFileName { get; init; } + public string FileName { get; init; } + public string MimeType { get; init; } + public string PresignedUrl { get; init; } + public int UrlValidityInSeconds { get; init; } + + public DateTime TimeSubmitted { get; init; } +} diff --git a/api/src/Vote.Monitor.Core/Models/AttachmentModel.cs b/api/src/Vote.Monitor.Core/Models/AttachmentModel.cs index 9f115ceed..3592d528b 100644 --- a/api/src/Vote.Monitor.Core/Models/AttachmentModel.cs +++ b/api/src/Vote.Monitor.Core/Models/AttachmentModel.cs @@ -2,15 +2,12 @@ public record AttachmentModel { - public Guid SubmissionId { get; init; } public Guid QuestionId { get; init; } - public Guid MonitoringObserverId { get; init; } public string FilePath { get; init; } public string UploadedFileName { get; init; } public string FileName { get; init; } public string MimeType { get; init; } public string PresignedUrl { get; init; } public int UrlValidityInSeconds { get; init; } - public DateTime TimeSubmitted { get; init; } }