diff --git a/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminAuthorizationHandler.cs b/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminAuthorizationHandler.cs index c077ed4cc..6bb32f7cd 100644 --- a/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminAuthorizationHandler.cs +++ b/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminAuthorizationHandler.cs @@ -1,6 +1,5 @@ using Authorization.Policies.Requirements; using Authorization.Policies.Specifications; -using Vote.Monitor.Domain.Entities.ElectionRoundAggregate; namespace Authorization.Policies.RequirementHandlers; @@ -35,8 +34,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (result.ElectionRoundStatus == ElectionRoundStatus.Archived - || result.NgoStatus == NgoStatus.Deactivated + if (result.NgoStatus == NgoStatus.Deactivated || result.MonitoringNgoStatus == MonitoringNgoStatus.Suspended) { context.Fail(); diff --git a/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminOrObserverAuthorizationHandler.cs b/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminOrObserverAuthorizationHandler.cs index 1ddec46ed..4dea606cf 100644 --- a/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminOrObserverAuthorizationHandler.cs +++ b/api/src/Authorization.Policies/RequirementHandlers/MonitoringNgoAdminOrObserverAuthorizationHandler.cs @@ -28,8 +28,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext if (result is not null) { - if (result.ElectionRoundStatus == ElectionRoundStatus.Archived || - result.NgoStatus == NgoStatus.Deactivated || + if (result.NgoStatus == NgoStatus.Deactivated || result.MonitoringNgoStatus == MonitoringNgoStatus.Suspended || result.UserStatus == UserStatus.Deactivated || result.MonitoringObserverStatus == MonitoringObserverStatus.Suspended) diff --git a/api/src/Feature.Citizen.Guides/Update/Validator.cs b/api/src/Feature.Citizen.Guides/Update/Validator.cs index 01b49f4d6..4724de65f 100644 --- a/api/src/Feature.Citizen.Guides/Update/Validator.cs +++ b/api/src/Feature.Citizen.Guides/Update/Validator.cs @@ -1,6 +1,4 @@ -using Vote.Monitor.Core.Validators; - -namespace Feature.Citizen.Guides.Update; +namespace Feature.Citizen.Guides.Update; public class Validator : Validator { diff --git a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs index b083fbf89..5ffd4d149 100644 --- a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs +++ b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs @@ -1,4 +1,5 @@ using Feature.CitizenReports.Models; +using Feature.CitizenReports.Requests; using Vote.Monitor.Answer.Module.Aggregators; using Vote.Monitor.Core.Services.FileStorage.Contracts; @@ -8,7 +9,7 @@ public class Endpoint( VoteMonitorContext context, IAuthorizationService authorizationService, IFileStorageService fileStorageService) - : Endpoint, NotFound>> + : Endpoint, NotFound>> { public override void Configure() { @@ -23,7 +24,7 @@ public override void Configure() }); } - public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, NotFound>> ExecuteAsync(CitizenReportsAggregateFilter req, CancellationToken ct) { var authorizationResult = await authorizationService.AuthorizeAsync(User, new CitizenReportingNgoAdminRequirement(req.ElectionRoundId)); @@ -49,7 +50,7 @@ public override async Task, NotFound>> ExecuteAsync(Request } private async Task, NotFound>> AggregateCitizenReportsAsync(FormAggregate form, - Request req, + CitizenReportsAggregateFilter req, CancellationToken ct) { var citizenReports = await context.CitizenReports @@ -118,7 +119,20 @@ private async Task, NotFound>> AggregateCitizenReportsAsync { SubmissionsAggregate = formSubmissionsAggregate, Notes = citizenReports.SelectMany(x => x.Notes).Select(NoteModel.FromEntity).ToArray(), - Attachments = attachments + Attachments = attachments, + SubmissionsFilter = new SubmissionsFilterModel + { + HasAttachments = req.HasAttachments, + HasNotes = req.HasNotes, + Level1Filter = req.Level1Filter, + Level2Filter = req.Level2Filter, + Level3Filter = req.Level3Filter, + Level4Filter = req.Level4Filter, + Level5Filter = req.Level5Filter, + HasFlaggedAnswers = req.HasFlaggedAnswers, + QuestionsAnswered = req.QuestionsAnswered, + FollowUpStatus = req.FollowUpStatus, + } }); } } \ No newline at end of file diff --git a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs index 121c564c8..70b405b95 100644 --- a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs +++ b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs @@ -1,5 +1,6 @@ using Feature.CitizenReports.Models; using Vote.Monitor.Answer.Module.Aggregators; +using Vote.Monitor.Domain.Entities.CitizenReportAggregate; namespace Feature.CitizenReports.GetSubmissionsAggregated; @@ -8,4 +9,27 @@ public class Response public CitizenReportFormSubmissionsAggregate SubmissionsAggregate { get; set; } public AttachmentModel[] Attachments { get; set; } = []; public NoteModel[] Notes { get; set; } = []; + + public SubmissionsFilterModel SubmissionsFilter { get; set; } +} + +public class SubmissionsFilterModel +{ + public string? Level1Filter { get; set; } + + public string? Level2Filter { get; set; } + + public string? Level3Filter { get; set; } + + public string? Level4Filter { get; set; } + + public string? Level5Filter { get; set; } + + public bool? HasFlaggedAnswers { get; set; } + + public CitizenReportFollowUpStatus? FollowUpStatus { get; set; } + + public bool? HasNotes { get; set; } + public bool? HasAttachments { get; set; } + public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Validator.cs b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Validator.cs deleted file mode 100644 index c50435c1f..000000000 --- a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Validator.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Feature.CitizenReports.GetSubmissionsAggregated; - -public class Validator : Validator -{ - public Validator() - { - RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); - RuleFor(x => x.FormId).NotEmpty(); - } -} \ No newline at end of file diff --git a/api/src/Feature.CitizenReports/ListFormsOverview/Endpoint.cs b/api/src/Feature.CitizenReports/ListFormsOverview/Endpoint.cs index f8f875d13..9bf1367d7 100644 --- a/api/src/Feature.CitizenReports/ListFormsOverview/Endpoint.cs +++ b/api/src/Feature.CitizenReports/ListFormsOverview/Endpoint.cs @@ -1,7 +1,9 @@ -namespace Feature.CitizenReports.ListFormsOverview; +using Feature.CitizenReports.Requests; + +namespace Feature.CitizenReports.ListFormsOverview; public class Endpoint(VoteMonitorContext context, IAuthorizationService authorizationService) - : Endpoint, NotFound>> + : Endpoint, NotFound>> { public override void Configure() { @@ -13,7 +15,7 @@ public override void Configure() Summary(x => { x.Summary = "Citizen report submissions aggregated by form"; }); } - public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, NotFound>> ExecuteAsync(CitizenReportsAggregateFilter req, CancellationToken ct) { var authorizationResult = await authorizationService.AuthorizeAsync(User, diff --git a/api/src/Feature.CitizenReports/ListFormsOverview/Request.cs b/api/src/Feature.CitizenReports/ListFormsOverview/Request.cs deleted file mode 100644 index 194e94930..000000000 --- a/api/src/Feature.CitizenReports/ListFormsOverview/Request.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Vote.Monitor.Core.Security; -using Vote.Monitor.Domain.Entities.CitizenReportAggregate; - -namespace Feature.CitizenReports.ListFormsOverview; - -public class Request -{ - public Guid ElectionRoundId { get; set; } - - [FromClaim(ApplicationClaimTypes.NgoId)] - public Guid NgoId { get; set; } - - [QueryParam] public string? Level1Filter { get; set; } - [QueryParam] public string? Level2Filter { get; set; } - [QueryParam] public string? Level3Filter { get; set; } - [QueryParam] public string? Level4Filter { get; set; } - [QueryParam] public string? Level5Filter { get; set; } - [QueryParam] public bool? HasFlaggedAnswers { get; set; } - [QueryParam] public CitizenReportFollowUpStatus? FollowUpStatus { get; set; } - [QueryParam] public bool? HasNotes { get; set; } - [QueryParam] public bool? HasAttachments { get; set; } - [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } -} \ No newline at end of file diff --git a/api/src/Feature.CitizenReports/ListFormsOverview/Validator.cs b/api/src/Feature.CitizenReports/ListFormsOverview/Validator.cs deleted file mode 100644 index 78eb6352c..000000000 --- a/api/src/Feature.CitizenReports/ListFormsOverview/Validator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Feature.CitizenReports.ListFormsOverview; - -public class Validator : Validator -{ - public Validator() - { - RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); - } -} \ No newline at end of file diff --git a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Request.cs b/api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs similarity index 87% rename from api/src/Feature.CitizenReports/GetSubmissionsAggregated/Request.cs rename to api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs index 72964ddc4..b61f30119 100644 --- a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Request.cs +++ b/api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs @@ -1,16 +1,16 @@ using Vote.Monitor.Core.Security; using Vote.Monitor.Domain.Entities.CitizenReportAggregate; -namespace Feature.CitizenReports.GetSubmissionsAggregated; +namespace Feature.CitizenReports.Requests; -public class Request +public class CitizenReportsAggregateFilter { public Guid ElectionRoundId { get; set; } [FromClaim(ApplicationClaimTypes.NgoId)] public Guid NgoId { get; set; } - public Guid FormId { get; set; } + public Guid? FormId { get; set; } [QueryParam] public string? Level1Filter { get; set; } diff --git a/api/src/Feature.CitizenReports/SendCopy/Validator.cs b/api/src/Feature.CitizenReports/SendCopy/Validator.cs index 3f2514867..8fccf1ca5 100644 --- a/api/src/Feature.CitizenReports/SendCopy/Validator.cs +++ b/api/src/Feature.CitizenReports/SendCopy/Validator.cs @@ -1,6 +1,4 @@ -using Vote.Monitor.Core.Validators; - -namespace Feature.CitizenReports.SendCopy; +namespace Feature.CitizenReports.SendCopy; public class Validator : Validator { diff --git a/api/src/Feature.CitizenReports/Validators/CitizenReportsAggregateFilterValidator.cs b/api/src/Feature.CitizenReports/Validators/CitizenReportsAggregateFilterValidator.cs new file mode 100644 index 000000000..c4210a45a --- /dev/null +++ b/api/src/Feature.CitizenReports/Validators/CitizenReportsAggregateFilterValidator.cs @@ -0,0 +1,12 @@ +using Feature.CitizenReports.Requests; + +namespace Feature.CitizenReports.Validators; + +public class CitizenReportsAggregateFilterValidator : Validator +{ + public CitizenReportsAggregateFilterValidator() + { + RuleFor(x => x.ElectionRoundId).NotEmpty(); + RuleFor(x => x.NgoId).NotEmpty(); + } +} \ No newline at end of file diff --git a/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs b/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs index 2c4f79585..86aefe940 100644 --- a/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs +++ b/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs @@ -39,6 +39,8 @@ public class FormSubmissionsFilters public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } public DateTime? FromDateFilter { get; set; } public DateTime? ToDateFilter { get; set; } + + public bool? IsCompletedFilter { get; set; } public ExportFormSubmissionsFilters ToFilter() { @@ -62,7 +64,8 @@ public ExportFormSubmissionsFilters ToFilter() HasAttachments = HasAttachments, QuestionsAnswered = QuestionsAnswered, FromDateFilter = FromDateFilter, - ToDateFilter = ToDateFilter + ToDateFilter = ToDateFilter, + IsCompletedFilter = IsCompletedFilter }; } } \ No newline at end of file diff --git a/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs b/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs index 3637508ac..e5d1ed7b7 100644 --- a/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs +++ b/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs @@ -37,6 +37,7 @@ public class IncidentReportsFilters public DateTime? FromDateFilter { get; set; } public DateTime? ToDateFilter { get; set; } + public bool? IsCompletedFilter { get; set; } public ExportIncidentReportsFilters ToFilter() { @@ -60,7 +61,8 @@ public ExportIncidentReportsFilters ToFilter() FollowUpStatus = FollowUpStatus, LocationType = LocationType, FromDateFilter = FromDateFilter, - ToDateFilter = ToDateFilter + ToDateFilter = ToDateFilter, + IsCompletedFilter = IsCompletedFilter }; } } \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs index 1ae226a28..ef0e8f1fc 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs @@ -1,9 +1,11 @@ using Feature.Form.Submissions.Models; +using Feature.Form.Submissions.Requests; using Microsoft.EntityFrameworkCore; using Vote.Monitor.Answer.Module.Aggregators; using Vote.Monitor.Core.Models; using Vote.Monitor.Core.Services.FileStorage.Contracts; using Vote.Monitor.Domain; +using Vote.Monitor.Domain.Entities.FormAggregate; using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate; namespace Feature.Form.Submissions.GetAggregated; @@ -12,7 +14,7 @@ public class Endpoint( IAuthorizationService authorizationService, VoteMonitorContext context, INpgsqlConnectionFactory connectionFactory, - IFileStorageService fileStorageService) : Endpoint, NotFound>> + IFileStorageService fileStorageService) : Endpoint, NotFound>> { public override void Configure() { @@ -23,7 +25,8 @@ public override void Configure() Policies(PolicyNames.NgoAdminsOnly); } - public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, NotFound>> ExecuteAsync(FormSubmissionsAggregateFilter req, + CancellationToken ct) { var authorizationResult = await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); @@ -31,12 +34,14 @@ public override async Task, NotFound>> ExecuteAsync(Request { return TypedResults.NotFound(); } - + var form = await context .Forms .Where(x => x.ElectionRoundId == req.ElectionRoundId && x.MonitoringNgo.NgoId == req.NgoId && x.Id == req.FormId) + .Where(x => x.Status == FormStatus.Published) + .Where(x => x.FormType != FormType.CitizenReporting && x.FormType != FormType.IncidentReporting) .AsNoTracking() .FirstOrDefaultAsync(ct); @@ -59,7 +64,7 @@ public override async Task, NotFound>> ExecuteAsync(Request } private async Task, NotFound>> AggregateNgoFormSubmissionsAsync(FormAggregate form, - Request req, + FormSubmissionsAggregateFilter req, CancellationToken ct) { var tags = req.TagsFilter ?? []; @@ -118,6 +123,7 @@ private async Task, NotFound>> AggregateNgoFormSubmissionsA && a.FormId == x.FormId && a.PollingStationId == x.PollingStationId && a.ElectionRoundId == x.ElectionRoundId) == 0)) + .Where(x => req.IsCompletedFilter == null || x.IsCompleted == req.IsCompletedFilter) .AsNoTracking() .AsSplitQuery() .ToListAsync(ct); @@ -219,13 +225,30 @@ private async Task, NotFound>> AggregateNgoFormSubmissionsA { SubmissionsAggregate = formSubmissionsAggregate, Notes = notes, - Attachments = attachments + Attachments = attachments, + SubmissionsFilter = new SubmissionsFilterModel + { + HasAttachments = req.HasAttachments, + HasNotes = req.HasNotes, + Level1Filter = req.Level1Filter, + Level2Filter = req.Level2Filter, + Level3Filter = req.Level3Filter, + Level4Filter = req.Level4Filter, + Level5Filter = req.Level5Filter, + QuestionsAnswered = req.QuestionsAnswered, + TagsFilter = req.TagsFilter, + FollowUpStatus = req.FollowUpStatus, + HasFlaggedAnswers = req.HasFlaggedAnswers, + IsCompletedFilter = req.IsCompletedFilter, + MonitoringObserverStatus = req.MonitoringObserverStatus, + PollingStationNumberFilter = req.PollingStationNumberFilter, + } }); } private async Task, NotFound>> AggregatePSIFormSubmissionsAsync( PollingStationInformationForm form, - Request req, + FormSubmissionsAggregateFilter req, CancellationToken ct) { var tags = req.TagsFilter ?? []; diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Response.cs b/api/src/Feature.Form.Submissions/GetAggregated/Response.cs index 051d913ef..6314a7054 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Response.cs +++ b/api/src/Feature.Form.Submissions/GetAggregated/Response.cs @@ -1,11 +1,41 @@ using Feature.Form.Submissions.Models; using Vote.Monitor.Answer.Module.Aggregators; +using Vote.Monitor.Core.Models; +using Vote.Monitor.Domain.Entities.MonitoringObserverAggregate; namespace Feature.Form.Submissions.GetAggregated; public class Response { + public SubmissionsFilterModel SubmissionsFilter { get; set; } public FormSubmissionsAggregate SubmissionsAggregate { get; set; } public List Attachments { get; set; } public List Notes { get; set; } } + +public class SubmissionsFilterModel +{ + public string? Level1Filter { get; set; } + + public string? Level2Filter { get; set; } + + public string? Level3Filter { get; set; } + + public string? Level4Filter { get; set; } + + public string? Level5Filter { get; set; } + + public string? PollingStationNumberFilter { get; set; } + + public bool? HasFlaggedAnswers { get; set; } + + public SubmissionFollowUpStatus? FollowUpStatus { get; set; } + + public string[]? TagsFilter { get; set; } = []; + + public MonitoringObserverStatus? MonitoringObserverStatus { get; set; } + public bool? HasNotes { get; set; } + public bool? HasAttachments { get; set; } + public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + public bool? IsCompletedFilter { get; set; } +} \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Validator.cs b/api/src/Feature.Form.Submissions/GetAggregated/Validator.cs deleted file mode 100644 index 6bd6f1b88..000000000 --- a/api/src/Feature.Form.Submissions/GetAggregated/Validator.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Feature.Form.Submissions.GetAggregated; - -public class Validator : Validator -{ - public Validator() - { - RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); - RuleFor(x => x.FormId).NotEmpty(); - } -} diff --git a/api/src/Feature.Form.Submissions/ListByForm/Endpoint.cs b/api/src/Feature.Form.Submissions/ListByForm/Endpoint.cs index 7f675ba91..ec89f14aa 100644 --- a/api/src/Feature.Form.Submissions/ListByForm/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/ListByForm/Endpoint.cs @@ -1,7 +1,9 @@ -namespace Feature.Form.Submissions.ListByForm; +using Feature.Form.Submissions.Requests; + +namespace Feature.Form.Submissions.ListByForm; public class Endpoint(IAuthorizationService authorizationService, INpgsqlConnectionFactory dbConnectionFactory) - : Endpoint, NotFound>> + : Endpoint, NotFound>> { public override void Configure() { @@ -13,7 +15,7 @@ public override void Configure() Summary(x => { x.Summary = "Form submissions aggregated by observer"; }); } - public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, NotFound>> ExecuteAsync(FormSubmissionsAggregateFilter req, CancellationToken ct) { var authorizationResult = await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); @@ -27,6 +29,8 @@ public override async Task, NotFound>> ExecuteAsync(Request F."Id" AS "FormId", 'PSI' AS "FormCode", 'PSI' AS "FormType", + F."Name" as "FormName", + F."DefaultLanguage", COUNT(DISTINCT PSI."Id") "NumberOfSubmissions", SUM(PSI."NumberOfFlaggedAnswers") "NumberOfFlaggedAnswers", 0 AS "NumberOfMediaFiles", @@ -56,6 +60,7 @@ 0 AS "NumberOfNotes" OR (@questionsAnswered = 'None' AND psi."NumberOfQuestionsAnswered" = 0)) AND (@hasNotes is NULL OR (TRUE AND @hasNotes = false) OR (FALSE AND @hasNotes = true)) AND (@hasAttachments is NULL OR (TRUE AND @hasAttachments = false) OR (FALSE AND @hasAttachments = true)) + AND (@isCompleted is NULL OR psi."IsCompleted" = @isCompleted) GROUP BY F."Id" UNION ALL @@ -63,6 +68,8 @@ UNION ALL F."Id" AS "FormId", F."Code" AS "FormCode", F."FormType" AS "FormType", + F."Name" as "FormName", + F."DefaultLanguage", COUNT(DISTINCT FS."Id") "NumberOfSubmissions", SUM(FS."NumberOfFlaggedAnswers") "NumberOfFlaggedAnswers", ( @@ -93,6 +100,7 @@ UNION ALL F."ElectionRoundId" = @electionRoundId AND MN."NgoId" = @ngoId AND F."Status" = 'Published' + AND F."FormType" NOT IN ('CitizenReporting', 'IncidentReporting') AND (@level1 IS NULL OR ps."Level1" = @level1) AND (@level2 IS NULL OR ps."Level2" = @level2) AND (@level3 IS NULL OR ps."Level3" = @level3) @@ -104,6 +112,7 @@ UNION ALL AND (@tagsFilter IS NULL OR cardinality(@tagsFilter) = 0 OR mo."Tags" && @tagsFilter) AND (@monitoringObserverStatus IS NULL OR mo."Status" = @monitoringObserverStatus) AND (@formId IS NULL OR fs."FormId" = @formId) + AND (@isCompleted is NULL OR FS."IsCompleted" = @isCompleted) AND (@questionsAnswered is null OR (@questionsAnswered = 'All' AND f."NumberOfQuestions" = fs."NumberOfQuestionsAnswered") OR (@questionsAnswered = 'Some' AND f."NumberOfQuestions" <> fs."NumberOfQuestionsAnswered") @@ -115,9 +124,7 @@ UNION ALL OR ((SELECT COUNT(1) FROM "Notes" WHERE "FormId" = fs."FormId" AND "MonitoringObserverId" = fs."MonitoringObserverId" AND fs."PollingStationId" = "PollingStationId") = 0 AND @hasNotes = false) OR ((SELECT COUNT(1) FROM "Notes" WHERE "FormId" = fs."FormId" AND "MonitoringObserverId" = fs."MonitoringObserverId" AND fs."PollingStationId" = "PollingStationId") > 0 AND @hasNotes = true)) GROUP BY - F."Id", - F."Code", - F."FormType"; + F."Id" """; var queryArgs = new @@ -138,9 +145,10 @@ GROUP BY hasNotes = req.HasNotes, hasAttachments = req.HasAttachments, questionsAnswered = req.QuestionsAnswered?.ToString(), + isCompleted = req.IsCompletedFilter }; - IEnumerable aggregatedFormOverviews = []; + IEnumerable aggregatedFormOverviews; using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct)) { diff --git a/api/src/Feature.Form.Submissions/ListByForm/Request.cs b/api/src/Feature.Form.Submissions/ListByForm/Request.cs deleted file mode 100644 index 3d6dd7b0a..000000000 --- a/api/src/Feature.Form.Submissions/ListByForm/Request.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Vote.Monitor.Core.Models; -using Vote.Monitor.Core.Security; -using Vote.Monitor.Domain.Entities.MonitoringObserverAggregate; - -namespace Feature.Form.Submissions.ListByForm; - -public class Request -{ - public Guid ElectionRoundId { get; set; } - - [FromClaim(ApplicationClaimTypes.NgoId)] - public Guid NgoId { get; set; } - - [QueryParam] public string? Level1Filter { get; set; } - - [QueryParam] public string? Level2Filter { get; set; } - - [QueryParam] public string? Level3Filter { get; set; } - - [QueryParam] public string? Level4Filter { get; set; } - - [QueryParam] public string? Level5Filter { get; set; } - - [QueryParam] public string? PollingStationNumberFilter { get; set; } - - [QueryParam] public bool? HasFlaggedAnswers { get; set; } - - [QueryParam] public SubmissionFollowUpStatus? FollowUpStatus { get; set; } - - [QueryParam] public string[]? TagsFilter { get; set; } = []; - - [QueryParam] public MonitoringObserverStatus? MonitoringObserverStatus { get; set; } - [QueryParam] public Guid? FormId { get; set; } - [QueryParam] public bool? HasNotes { get; set; } - [QueryParam] public bool? HasAttachments { get; set; } - [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } -} \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/ListByForm/Response.cs b/api/src/Feature.Form.Submissions/ListByForm/Response.cs index d3ad6c73f..086c034b6 100644 --- a/api/src/Feature.Form.Submissions/ListByForm/Response.cs +++ b/api/src/Feature.Form.Submissions/ListByForm/Response.cs @@ -1,4 +1,6 @@ -namespace Feature.Form.Submissions.ListByForm; +using Vote.Monitor.Core.Models; + +namespace Feature.Form.Submissions.ListByForm; public record Response { @@ -10,8 +12,11 @@ public class AggregatedFormOverview public Guid FormId { get; set; } public string FormCode { get; set; } public string FormType { get; set; } + public string DefaultLanguage { get; set; } + public TranslatedString FormName { get; set; } + public int NumberOfSubmissions { get; set; } public int NumberOfFlaggedAnswers { get; set; } public int NumberOfNotes { get; set; } public int NumberOfMediaFiles { get; set; } -} +} \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/ListByForm/Validator.cs b/api/src/Feature.Form.Submissions/ListByForm/Validator.cs deleted file mode 100644 index 700d01037..000000000 --- a/api/src/Feature.Form.Submissions/ListByForm/Validator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Feature.Form.Submissions.ListByForm; - -public class Validator : Validator -{ - public Validator() - { - RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); - } -} diff --git a/api/src/Feature.Form.Submissions/ListByObserver/Endpoint.cs b/api/src/Feature.Form.Submissions/ListByObserver/Endpoint.cs index 66b39101a..516c1ba59 100644 --- a/api/src/Feature.Form.Submissions/ListByObserver/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/ListByObserver/Endpoint.cs @@ -46,6 +46,7 @@ SELECT COUNT(*) count "PhoneNumber", "Email", "Tags", + "NumberOfCompletedForms", "NumberOfFlaggedAnswers", "NumberOfLocations", "NumberOfFormsSubmitted", @@ -57,6 +58,17 @@ SELECT COUNT(*) count U."PhoneNumber", U."Email", MO."Tags", + COALESCE( + ( + SELECT + count(*) + FROM + "FormSubmissions" FS + WHERE + FS."MonitoringObserverId" = MO."Id" and fs."IsCompleted" = true + ), + 0 + ) AS "NumberOfCompletedForms", COALESCE( ( SELECT @@ -160,7 +172,10 @@ ORDER BY CASE WHEN @sortExpression = 'NumberOfLocations DESC' THEN "NumberOfLocations" END DESC, CASE WHEN @sortExpression = 'NumberOfFormsSubmitted ASC' THEN "NumberOfFormsSubmitted" END ASC, - CASE WHEN @sortExpression = 'NumberOfFormsSubmitted DESC' THEN "NumberOfFormsSubmitted" END DESC + CASE WHEN @sortExpression = 'NumberOfFormsSubmitted DESC' THEN "NumberOfFormsSubmitted" END DESC, + + CASE WHEN @sortExpression = 'NumberOfCompletedForms ASC' THEN "NumberOfCompletedForms" END ASC, + CASE WHEN @sortExpression = 'NumberOfCompletedForms DESC' THEN "NumberOfCompletedForms" END DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY; @@ -243,6 +258,12 @@ private static string GetSortExpression(string? sortColumnName, bool isAscending return $"{nameof(ObserverSubmissionOverview.NumberOfFormsSubmitted)} {sortOrder}"; } + if (string.Equals(sortColumnName, nameof(ObserverSubmissionOverview.NumberOfCompletedForms), + StringComparison.InvariantCultureIgnoreCase)) + { + return $"{nameof(ObserverSubmissionOverview.NumberOfCompletedForms)} {sortOrder}"; + } + return $"{nameof(ObserverSubmissionOverview.ObserverName)} ASC"; } } \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/ListByObserver/ObserverSubmissionOverview.cs b/api/src/Feature.Form.Submissions/ListByObserver/ObserverSubmissionOverview.cs index d31cb8f65..5da5cfe43 100644 --- a/api/src/Feature.Form.Submissions/ListByObserver/ObserverSubmissionOverview.cs +++ b/api/src/Feature.Form.Submissions/ListByObserver/ObserverSubmissionOverview.cs @@ -13,6 +13,7 @@ public record ObserverSubmissionOverview public int NumberOfFlaggedAnswers { get; init; } public int NumberOfLocations { get; init; } public int NumberOfFormsSubmitted { get; init; } + public int NumberOfCompletedForms { get; init; } [JsonConverter(typeof(SmartEnumNameConverter))] public SubmissionFollowUpStatus? FollowUpStatus { get; init; } diff --git a/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs b/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs index 848a8ce4a..b8f77232e 100644 --- a/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs @@ -59,6 +59,7 @@ SELECT SUM(count) AND (@hasAttachments is NULL OR (TRUE AND @hasAttachments = false) OR (FALSE AND @hasAttachments = true)) AND (@fromDate is NULL OR COALESCE(PSI."LastModifiedOn", PSI."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(PSI."LastModifiedOn", PSI."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR PSI."IsCompleted" = @isCompleted) UNION ALL SELECT count(*) AS count FROM "FormSubmissions" fs INNER JOIN "Forms" f ON f."Id" = fs."FormId" @@ -95,6 +96,7 @@ UNION ALL SELECT count(*) AS count OR ((SELECT COUNT(1) FROM "Notes" N WHERE N."FormId" = fs."FormId" AND N."MonitoringObserverId" = fs."MonitoringObserverId" AND fs."PollingStationId" = N."PollingStationId") > 0 AND @hasNotes = true)) AND (@fromDate is NULL OR COALESCE(FS."LastModifiedOn", FS."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(FS."LastModifiedOn", FS."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR FS."IsCompleted" = @isCompleted) ) c; WITH polling_station_submissions AS ( @@ -108,7 +110,10 @@ WITH polling_station_submissions AS ( 0 AS "MediaFilesCount", 0 AS "NotesCount", COALESCE(psi."LastModifiedOn", psi."CreatedOn") "TimeSubmitted", - psi."FollowUpStatus" + psi."FollowUpStatus", + psif."DefaultLanguage", + psif."Name", + psi."IsCompleted" FROM "PollingStationInformation" psi INNER JOIN "PollingStationInformationForms" psif ON psif."Id" = psi."PollingStationInformationFormId" INNER JOIN "MonitoringObservers" mo ON mo."Id" = psi."MonitoringObserverId" @@ -120,6 +125,7 @@ WITH polling_station_submissions AS ( AND (@formId IS NULL OR psi."PollingStationInformationFormId" = @formId) AND (@fromDate is NULL OR COALESCE(PSI."LastModifiedOn", PSI."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(PSI."LastModifiedOn", PSI."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR psi."IsCompleted" = @isCompleted) AND (@questionsAnswered IS NULL OR (@questionsAnswered = 'All' AND psif."NumberOfQuestions" = psi."NumberOfQuestionsAnswered") OR (@questionsAnswered = 'Some' AND psif."NumberOfQuestions" <> psi."NumberOfQuestionsAnswered") @@ -149,7 +155,10 @@ SELECT COUNT(1) AND fs."PollingStationId" = N."PollingStationId" ) AS "NotesCount", COALESCE(fs."LastModifiedOn", fs."CreatedOn") AS "TimeSubmitted", - fs."FollowUpStatus" + fs."FollowUpStatus", + f."DefaultLanguage", + f."Name", + fs."IsCompleted" FROM "FormSubmissions" fs INNER JOIN "Forms" f ON f."Id" = fs."FormId" INNER JOIN "MonitoringObservers" mo ON fs."MonitoringObserverId" = mo."Id" @@ -161,6 +170,7 @@ SELECT COUNT(1) AND (@formId IS NULL OR fs."FormId" = @formId) AND (@fromDate is NULL OR COALESCE(FS."LastModifiedOn", FS."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(FS."LastModifiedOn", FS."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR FS."IsCompleted" = @isCompleted) AND (@questionsAnswered IS NULL OR (@questionsAnswered = 'All' AND f."NumberOfQuestions" = fs."NumberOfQuestionsAnswered") OR (@questionsAnswered = 'Some' AND f."NumberOfQuestions" <> fs."NumberOfQuestionsAnswered") @@ -170,6 +180,8 @@ SELECT COUNT(1) s."TimeSubmitted", s."FormCode", s."FormType", + s."DefaultLanguage", + s."Name" as "FormName", ps."Id" AS "PollingStationId", ps."Level1", ps."Level2", @@ -187,7 +199,9 @@ SELECT COUNT(1) s."NumberOfFlaggedAnswers", s."MediaFilesCount", s."NotesCount", - s."FollowUpStatus" + s."FollowUpStatus", + s."IsCompleted", + mo."Status" "MonitoringObserverStatus" FROM ( SELECT * FROM polling_station_submissions UNION ALL @@ -244,7 +258,17 @@ ORDER BY CASE WHEN @sortExpression = 'Number ASC' THEN ps."Number" END ASC, CASE WHEN @sortExpression = 'Number DESC' THEN ps."Number" END DESC, CASE WHEN @sortExpression = 'ObserverName ASC' THEN u."FirstName" || ' ' || u."LastName" END ASC, - CASE WHEN @sortExpression = 'ObserverName DESC' THEN u."FirstName" || ' ' || u."LastName" END DESC + CASE WHEN @sortExpression = 'ObserverName DESC' THEN u."FirstName" || ' ' || u."LastName" END DESC, + CASE WHEN @sortExpression = 'NumberOfFlaggedAnswers ASC' THEN s."NumberOfFlaggedAnswers" END ASC, + CASE WHEN @sortExpression = 'NumberOfFlaggedAnswers DESC' THEN s."NumberOfFlaggedAnswers" END DESC, + CASE WHEN @sortExpression = 'NumberOfQuestionsAnswered ASC' THEN s."NumberOfQuestionsAnswered" END ASC, + CASE WHEN @sortExpression = 'NumberOfQuestionsAnswered DESC' THEN s."NumberOfQuestionsAnswered" END DESC, + CASE WHEN @sortExpression = 'MediaFilesCount ASC' THEN s."MediaFilesCount" END ASC, + CASE WHEN @sortExpression = 'MediaFilesCount DESC' THEN s."MediaFilesCount" END DESC, + CASE WHEN @sortExpression = 'NotesCount ASC' THEN s."NotesCount" END ASC, + CASE WHEN @sortExpression = 'NotesCount DESC' THEN s."NotesCount" END DESC, + CASE WHEN @sortExpression = 'MonitoringObserverStatus ASC' THEN mo."Status" END ASC, + CASE WHEN @sortExpression = 'MonitoringObserverStatus DESC' THEN mo."Status" END DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY; """; @@ -274,6 +298,7 @@ OFFSET @offset ROWS questionsAnswered = req.QuestionsAnswered?.ToString(), fromDate = req.FromDateFilter?.ToString("O"), toDate = req.ToDateFilter?.ToString("O"), + isCompleted = req.IsCompletedFilter, sortExpression = GetSortExpression(req.SortColumnName, req.IsAscendingSorting) }; diff --git a/api/src/Feature.Form.Submissions/ListEntries/FormSubmissionEntry.cs b/api/src/Feature.Form.Submissions/ListEntries/FormSubmissionEntry.cs index 8cee29765..e8017dd5d 100644 --- a/api/src/Feature.Form.Submissions/ListEntries/FormSubmissionEntry.cs +++ b/api/src/Feature.Form.Submissions/ListEntries/FormSubmissionEntry.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Ardalis.SmartEnum.SystemTextJson; +using Vote.Monitor.Core.Models; using Vote.Monitor.Domain.Entities.FormAggregate; using Vote.Monitor.Domain.Entities.MonitoringObserverAggregate; @@ -11,10 +12,12 @@ public record FormSubmissionEntry public DateTime TimeSubmitted { get; init; } public string FormCode { get; init; } = default!; + public TranslatedString FormName { get; init; } = default!; [JsonConverter(typeof(SmartEnumNameConverter))] public FormType FormType { get; init; } = default!; + public string DefaultLanguage { get; set; } public Guid PollingStationId { get; init; } public string Level1 { get; init; } = default!; public string Level2 { get; init; } = default!; @@ -37,4 +40,6 @@ public record FormSubmissionEntry [JsonConverter(typeof(SmartEnumNameConverter))] public MonitoringObserverStatus MonitoringObserverStatus { get; init; } + + public bool IsCompleted { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/ListEntries/Request.cs b/api/src/Feature.Form.Submissions/ListEntries/Request.cs index b1fc848bc..32c5dec3b 100644 --- a/api/src/Feature.Form.Submissions/ListEntries/Request.cs +++ b/api/src/Feature.Form.Submissions/ListEntries/Request.cs @@ -44,4 +44,5 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public DateTime? FromDateFilter { get; set; } [QueryParam] public DateTime? ToDateFilter { get; set; } + [QueryParam] public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Request.cs b/api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs similarity index 85% rename from api/src/Feature.Form.Submissions/GetAggregated/Request.cs rename to api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs index f5e6eeae9..4a6580ae8 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Request.cs +++ b/api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs @@ -2,16 +2,16 @@ using Vote.Monitor.Core.Security; using Vote.Monitor.Domain.Entities.MonitoringObserverAggregate; -namespace Feature.Form.Submissions.GetAggregated; +namespace Feature.Form.Submissions.Requests; -public class Request +public class FormSubmissionsAggregateFilter { public Guid ElectionRoundId { get; set; } [FromClaim(ApplicationClaimTypes.NgoId)] public Guid NgoId { get; set; } - public Guid FormId { get; set; } + public Guid? FormId { get; set; } [QueryParam] public string? Level1Filter { get; set; } @@ -35,4 +35,5 @@ public class Request [QueryParam] public bool? HasNotes { get; set; } [QueryParam] public bool? HasAttachments { get; set; } [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + [QueryParam] public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.Form.Submissions/Validators/FormSubmissionsAggregateFilterValidator.cs b/api/src/Feature.Form.Submissions/Validators/FormSubmissionsAggregateFilterValidator.cs new file mode 100644 index 000000000..3e2641d5f --- /dev/null +++ b/api/src/Feature.Form.Submissions/Validators/FormSubmissionsAggregateFilterValidator.cs @@ -0,0 +1,12 @@ +using Feature.Form.Submissions.Requests; + +namespace Feature.Form.Submissions.Validators; + +public class FormSubmissionsAggregateFilterValidator : Validator +{ + public FormSubmissionsAggregateFilterValidator() + { + RuleFor(x => x.ElectionRoundId).NotEmpty(); + RuleFor(x => x.NgoId).NotEmpty(); + } +} diff --git a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs index c1ea4efa3..f0f81f91f 100644 --- a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs +++ b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs @@ -1,4 +1,5 @@ -using Vote.Monitor.Answer.Module.Aggregators; +using Feature.IncidentReports.Requests; +using Vote.Monitor.Answer.Module.Aggregators; namespace Feature.IncidentReports.GetSubmissionsAggregated; @@ -6,7 +7,7 @@ public class Endpoint( IAuthorizationService authorizationService, VoteMonitorContext context, IFileStorageService fileStorageService) - : Endpoint, NotFound>> + : Endpoint, NotFound>> { public override void Configure() { @@ -20,7 +21,7 @@ public override void Configure() }); } - public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, NotFound>> ExecuteAsync(IncidentReportsAggregateFilter req, CancellationToken ct) { var authorizationResult = await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); @@ -46,7 +47,7 @@ public override async Task, NotFound>> ExecuteAsync(Request } private async Task, NotFound>> AggregateIncidentReportsAsync(FormAggregate form, - Request req, + IncidentReportsAggregateFilter req, CancellationToken ct) { var incidentReports = await context.IncidentReports @@ -92,6 +93,7 @@ private async Task, NotFound>> AggregateIncidentReportsAsyn : !x.Attachments.Any())) .Where(x => req.FollowUpStatusFilter == null || x.FollowUpStatus == req.FollowUpStatusFilter) .Where(x => req.LocationTypeFilter == null || x.LocationType == req.LocationTypeFilter) + .Where(x => req.IsCompletedFilter == null || x.IsCompleted == req.IsCompletedFilter) .AsSplitQuery() .AsNoTracking() .ToListAsync(ct); @@ -125,7 +127,23 @@ private async Task, NotFound>> AggregateIncidentReportsAsyn { SubmissionsAggregate = formSubmissionsAggregate, Notes = incidentReports.SelectMany(x => x.Notes).Select(NoteModel.FromEntity).ToArray(), - Attachments = attachments + Attachments = attachments, + SubmissionsFilter = new SubmissionsFilterModel + { + HasAttachments = req.HasAttachments, + HasNotes = req.HasNotes, + Level1Filter = req.Level1Filter, + Level2Filter = req.Level2Filter, + Level3Filter = req.Level3Filter, + Level4Filter = req.Level4Filter, + Level5Filter = req.Level5Filter, + QuestionsAnswered = req.QuestionsAnswered, + HasFlaggedAnswers = req.HasFlaggedAnswers, + IsCompletedFilter = req.IsCompletedFilter, + LocationTypeFilter = req.LocationTypeFilter, + FollowUpStatusFilter = req.FollowUpStatusFilter, + PollingStationNumberFilter = req.PollingStationNumberFilter + } }); } } \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs index cc6f3316c..ea7329251 100644 --- a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs +++ b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs @@ -1,10 +1,36 @@ using Vote.Monitor.Answer.Module.Aggregators; +using Vote.Monitor.Domain.Entities.IncidentReportAggregate; namespace Feature.IncidentReports.GetSubmissionsAggregated; public class Response { + public SubmissionsFilterModel SubmissionsFilter { get; set; } public FormSubmissionsAggregate SubmissionsAggregate { get; set; } public AttachmentModel[] Attachments { get; set; } = []; public NoteModel[] Notes { get; set; } = []; + +} + +public class SubmissionsFilterModel +{ + public string? Level1Filter { get; set; } + + public string? Level2Filter { get; set; } + + public string? Level3Filter { get; set; } + + public string? Level4Filter { get; set; } + + public string? Level5Filter { get; set; } + public string? PollingStationNumberFilter { get; set; } + + public bool? HasFlaggedAnswers { get; set; } + + public IncidentReportFollowUpStatus? FollowUpStatusFilter { get; set; } + public IncidentReportLocationType? LocationTypeFilter { get; set; } + public bool? HasNotes { get; set; } + public bool? HasAttachments { get; set; } + public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Validator.cs b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Validator.cs deleted file mode 100644 index 0d7d51be5..000000000 --- a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Validator.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Feature.IncidentReports.GetSubmissionsAggregated; - -public class Validator : Validator -{ - public Validator() - { - RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); - RuleFor(x => x.FormId).NotEmpty(); - } -} \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/ListByObserver/Endpoint.cs b/api/src/Feature.IncidentReports/ListByObserver/Endpoint.cs index b1e02194e..0e848bc1d 100644 --- a/api/src/Feature.IncidentReports/ListByObserver/Endpoint.cs +++ b/api/src/Feature.IncidentReports/ListByObserver/Endpoint.cs @@ -48,6 +48,7 @@ SELECT COUNT(*) count "Tags", "NumberOfFlaggedAnswers", "NumberOfIncidentsSubmitted", + "NumberOfCompletedForms" "FollowUpStatus" FROM (SELECT MO."Id" AS "MonitoringObserverId", U."FirstName" || ' ' || U."LastName" AS "ObserverName", @@ -60,6 +61,12 @@ SELECT COUNT(*) count WHERE IR."MonitoringObserverId" = MO."Id"), 0 ) AS "NumberOfFlaggedAnswers", + COALESCE( + (SELECT COUNT(*) + FROM "IncidentReports" IR + WHERE IR."MonitoringObserverId" = MO."Id"), + 0 + ) AS "NumberOfCompletedForms", (SELECT COUNT(1) FROM "IncidentReports" IR WHERE IR."MonitoringObserverId" = MO."Id" AND IR."ElectionRoundId" = @electionRoundId) AS "NumberOfIncidentsSubmitted", @@ -73,7 +80,7 @@ WHEN EXISTS (SELECT 1 THEN 'NeedsFollowUp' ELSE NULL END - ) AS "FollowUpStatus" + ) AS "FollowUpStatus" FROM "MonitoringObservers" MO INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" INNER JOIN "Observers" O ON O."Id" = MO."ObserverId" @@ -93,6 +100,10 @@ ORDER BY CASE WHEN @sortExpression = 'Email DESC' THEN "Email" END DESC, CASE WHEN @sortExpression = 'Tags ASC' THEN "Tags" END ASC, CASE WHEN @sortExpression = 'Tags DESC' THEN "Tags" END DESC, + + CASE WHEN @sortExpression = 'NumberOfCompletedForms ASC' THEN "NumberOfCompletedForms" END ASC, + CASE WHEN @sortExpression = 'NumberOfCompletedForms DESC' THEN "NumberOfCompletedForms" END DESC, + CASE WHEN @sortExpression = 'NumberOfFlaggedAnswers ASC' THEN "NumberOfFlaggedAnswers" END ASC, CASE WHEN @sortExpression = 'NumberOfFlaggedAnswers DESC' THEN "NumberOfFlaggedAnswers" END DESC, @@ -162,6 +173,12 @@ private static string GetSortExpression(string? sortColumnName, bool isAscending return $"{nameof(ObserverIncidentReportsOverview.Tags)} {sortOrder}"; } + if (string.Equals(sortColumnName, nameof(ObserverIncidentReportsOverview.NumberOfCompletedForms), + StringComparison.InvariantCultureIgnoreCase)) + { + return $"{nameof(ObserverIncidentReportsOverview.NumberOfCompletedForms)} {sortOrder}"; + } + if (string.Equals(sortColumnName, nameof(ObserverIncidentReportsOverview.NumberOfFlaggedAnswers), StringComparison.InvariantCultureIgnoreCase)) { diff --git a/api/src/Feature.IncidentReports/ListByObserver/ObserverIncidentReportsOverview.cs b/api/src/Feature.IncidentReports/ListByObserver/ObserverIncidentReportsOverview.cs index fe8e7825d..18b192654 100644 --- a/api/src/Feature.IncidentReports/ListByObserver/ObserverIncidentReportsOverview.cs +++ b/api/src/Feature.IncidentReports/ListByObserver/ObserverIncidentReportsOverview.cs @@ -13,6 +13,7 @@ public record ObserverIncidentReportsOverview public string[] Tags { get; init; } = []; public int NumberOfFlaggedAnswers { get; init; } public int NumberOfIncidentsSubmitted { get; init; } + public int NumberOfCompletedForms { get; init; } [JsonConverter(typeof(SmartEnumNameConverter))] public IncidentReportFollowUpStatus? FollowUpStatus { get; init; } diff --git a/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs b/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs index 77624dc5e..94b16f705 100644 --- a/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs +++ b/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs @@ -100,7 +100,8 @@ OR U."PhoneNumber" ILIKE @searchText OR (@hasNotes = TRUE AND (SELECT COUNT(1) FROM "IncidentReportNotes" N WHERE N."IncidentReportId" = IR."Id") > 0 ) )) AND (@fromDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") >= @fromDate::timestamp) - AND (@toDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") <= @toDate::timestamp); + AND (@toDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR IR."IsCompleted" = @isCompleted); WITH @@ -128,7 +129,8 @@ INCIDENT_REPORTS AS ( ) AS "MediaFilesCount", ( SELECT COUNT(1) FROM "IncidentReportNotes" N WHERE N."IncidentReportId" = IR."Id") AS "NotesCount", COALESCE(IR."LastModifiedOn", IR."CreatedOn") AS "TimeSubmitted", - IR."FollowUpStatus" + IR."FollowUpStatus", + IR."IsCompleted" FROM "IncidentReports" IR INNER JOIN "Forms" F ON F."Id" = IR."FormId" @@ -148,6 +150,7 @@ INCIDENT_REPORTS AS ( ) AND (@fromDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR IR."IsCompleted" = @isCompleted) ) SELECT IR."IncidentReportId", @@ -174,7 +177,8 @@ INCIDENT_REPORTS AS ( IR."NumberOfFlaggedAnswers", IR."MediaFilesCount", IR."NotesCount", - IR."FollowUpStatus" + IR."FollowUpStatus", + IR."IsCompleted" FROM INCIDENT_REPORTS IR INNER JOIN "MonitoringObservers" MO ON MO."Id" = IR."MonitoringObserverId" @@ -263,6 +267,7 @@ OFFSET @offset ROWS fromDate = req.FromDateFilter?.ToString("O"), toDate = req.ToDateFilter?.ToString("O"), sortExpression = GetSortExpression(req.SortColumnName, req.IsAscendingSorting), + iscompleted = req.IsCompletedFilter }; int totalRowCount; diff --git a/api/src/Feature.IncidentReports/ListEntries/Request.cs b/api/src/Feature.IncidentReports/ListEntries/Request.cs index 4b2447800..acf4a9f85 100644 --- a/api/src/Feature.IncidentReports/ListEntries/Request.cs +++ b/api/src/Feature.IncidentReports/ListEntries/Request.cs @@ -38,7 +38,8 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } [QueryParam] public IncidentReportFollowUpStatus? FollowUpStatus { get; set; } [QueryParam] public IncidentReportLocationType? LocationType { get; set; } - + [QueryParam] public DateTime? FromDateFilter { get; set; } [QueryParam] public DateTime? ToDateFilter { get; set; } + [QueryParam] public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/ListFormsOverview/Endpoint.cs b/api/src/Feature.IncidentReports/ListFormsOverview/Endpoint.cs index 2fac8f29d..e20b9b223 100644 --- a/api/src/Feature.IncidentReports/ListFormsOverview/Endpoint.cs +++ b/api/src/Feature.IncidentReports/ListFormsOverview/Endpoint.cs @@ -1,7 +1,9 @@ -namespace Feature.IncidentReports.ListFormsOverview; +using Feature.IncidentReports.Requests; + +namespace Feature.IncidentReports.ListFormsOverview; public class Endpoint(IAuthorizationService authorizationService, VoteMonitorContext context) - : Endpoint, NotFound>> + : Endpoint, NotFound>> { public override void Configure() { @@ -13,7 +15,7 @@ public override void Configure() Summary(x => { x.Summary = "Incident reports aggregated by form"; }); } - public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, NotFound>> ExecuteAsync(IncidentReportsAggregateFilter req, CancellationToken ct) { var authorizationResult = await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); @@ -51,6 +53,7 @@ public override async Task, NotFound>> ExecuteAsync(Request : !x.Attachments.Any())) .Where(x => req.FollowUpStatusFilter == null || x.FollowUpStatus == req.FollowUpStatusFilter) .Where(x => req.LocationTypeFilter == null || x.LocationType == req.LocationTypeFilter) + .Where(x => req.IsCompletedFilter == null || x.IsCompleted == req.IsCompletedFilter) .GroupBy(cr => new { cr.FormId, cr.Form.Code, cr.Form.Name, cr.Form.DefaultLanguage }) .Select(cr => new AggregatedFormOverview { diff --git a/api/src/Feature.IncidentReports/ListFormsOverview/Request.cs b/api/src/Feature.IncidentReports/ListFormsOverview/Request.cs index 317c9f6b3..38cb36498 100644 --- a/api/src/Feature.IncidentReports/ListFormsOverview/Request.cs +++ b/api/src/Feature.IncidentReports/ListFormsOverview/Request.cs @@ -23,4 +23,5 @@ public class Request [QueryParam] public bool? HasNotes { get; set; } [QueryParam] public bool? HasAttachments { get; set; } [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + [QueryParam] public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/ListFormsOverview/Validator.cs b/api/src/Feature.IncidentReports/ListFormsOverview/Validator.cs deleted file mode 100644 index 52dacec1f..000000000 --- a/api/src/Feature.IncidentReports/ListFormsOverview/Validator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Feature.IncidentReports.ListFormsOverview; - -public class Validator : Validator -{ - public Validator() - { - RuleFor(x => x.ElectionRoundId).NotEmpty(); - RuleFor(x => x.NgoId).NotEmpty(); - } -} \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/Models/IncidentReportEntryModel.cs b/api/src/Feature.IncidentReports/Models/IncidentReportEntryModel.cs index ae32cad74..90cae86f5 100644 --- a/api/src/Feature.IncidentReports/Models/IncidentReportEntryModel.cs +++ b/api/src/Feature.IncidentReports/Models/IncidentReportEntryModel.cs @@ -35,4 +35,6 @@ public record IncidentReportEntryModel [JsonConverter(typeof(SmartEnumNameConverter))] public IncidentReportFollowUpStatus FollowUpStatus { get; set; } + + public bool IsCompleted { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Request.cs b/api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs similarity index 81% rename from api/src/Feature.IncidentReports/GetSubmissionsAggregated/Request.cs rename to api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs index d50620c88..737e200e0 100644 --- a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Request.cs +++ b/api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs @@ -1,16 +1,16 @@ -using Vote.Monitor.Core.Security; +using Vote.Monitor.Core.Security; using Vote.Monitor.Domain.Entities.IncidentReportAggregate; -namespace Feature.IncidentReports.GetSubmissionsAggregated; +namespace Feature.IncidentReports.Requests; -public class Request +public class IncidentReportsAggregateFilter { public Guid ElectionRoundId { get; set; } [FromClaim(ApplicationClaimTypes.NgoId)] public Guid NgoId { get; set; } - public Guid FormId { get; set; } + public Guid? FormId { get; set; } [QueryParam] public string? Level1Filter { get; set; } @@ -30,4 +30,5 @@ public class Request [QueryParam] public bool? HasNotes { get; set; } [QueryParam] public bool? HasAttachments { get; set; } [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + [QueryParam] public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Feature.IncidentReports/Validators/IncidentReportsAggregateFilterValidator.cs b/api/src/Feature.IncidentReports/Validators/IncidentReportsAggregateFilterValidator.cs new file mode 100644 index 000000000..83957239e --- /dev/null +++ b/api/src/Feature.IncidentReports/Validators/IncidentReportsAggregateFilterValidator.cs @@ -0,0 +1,12 @@ +using Feature.IncidentReports.Requests; + +namespace Feature.IncidentReports.Validators; + +public class IncidentReportsAggregateFilterValidator : Validator +{ + public IncidentReportsAggregateFilterValidator() + { + RuleFor(x => x.ElectionRoundId).NotEmpty(); + RuleFor(x => x.NgoId).NotEmpty(); + } +} \ No newline at end of file diff --git a/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs b/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs index 70f894611..b137f5680 100644 --- a/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs +++ b/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs @@ -6,7 +6,7 @@ public class MonitoringObserverImportModel public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } - public string[] Tags { get; set; } + public string[] Tags { get; set; } = []; public override int GetHashCode() { diff --git a/api/src/Feature.MonitoringObservers/Services/ObserverImportService.cs b/api/src/Feature.MonitoringObservers/Services/ObserverImportService.cs index 48d73d1ac..63c939c88 100644 --- a/api/src/Feature.MonitoringObservers/Services/ObserverImportService.cs +++ b/api/src/Feature.MonitoringObservers/Services/ObserverImportService.cs @@ -101,7 +101,7 @@ public async Task ImportAsync(Guid electionRoundId, Guid ngoId, existingAccount.NewInvite(); var newObserver = ObserverAggregate.Create(existingAccount); var newMonitoringObserver = MonitoringObserverAggregate.CreateForExisting(electionRoundId, - monitoringNgo.Id, newObserver.Id, observer.Tags); + monitoringNgo.Id, newObserver.Id, observer.Tags ?? []); await context.Observers.AddAsync(newObserver, ct); await context.MonitoringObservers.AddAsync(newMonitoringObserver, ct); @@ -123,7 +123,7 @@ public async Task ImportAsync(Guid electionRoundId, Guid ngoId, var newMonitoringObserver = MonitoringObserverAggregate.CreateForExisting(electionRoundId, monitoringNgo.Id, existingObserver.Id, - observer.Tags); + observer.Tags ?? []); await context.MonitoringObservers.AddAsync(newMonitoringObserver, ct); var invitationExistingUserEmailProps = new InvitationExistingUserEmailProps(FullName: fullName, @@ -148,7 +148,7 @@ public async Task ImportAsync(Guid electionRoundId, Guid ngoId, var newObserver = ObserverAggregate.Create(user); var newMonitoringObserver = MonitoringObserverAggregate.Create(electionRoundId, monitoringNgo.Id, - newObserver.Id, observer.Tags); + newObserver.Id, observer.Tags ?? []); await context.Observers.AddAsync(newObserver, ct); await context.MonitoringObservers.AddAsync(newMonitoringObserver, ct); var endpointUri = new Uri(Path.Combine($"{_apiConfig.WebAppUrl}", "accept-invite")); diff --git a/api/src/Feature.Notifications/ListRecipients/Endpoint.cs b/api/src/Feature.Notifications/ListRecipients/Endpoint.cs index 96d934e2e..60df9ec7f 100644 --- a/api/src/Feature.Notifications/ListRecipients/Endpoint.cs +++ b/api/src/Feature.Notifications/ListRecipients/Endpoint.cs @@ -2,12 +2,13 @@ using Dapper; using Vote.Monitor.Core.Models; using Vote.Monitor.Domain.ConnectionFactory; +using Vote.Monitor.Domain.Migrations; using Vote.Monitor.Domain.Specifications; namespace Feature.Notifications.ListRecipients; public class Endpoint(INpgsqlConnectionFactory dbConnectionFactory) : - Endpoint> + Endpoint> { public override void Configure() { @@ -17,241 +18,326 @@ public override void Configure() Policies(PolicyNames.NgoAdminsOnly); } - public override async Task> ExecuteAsync(Request req, CancellationToken ct) + public override async Task> ExecuteAsync(Request req, + CancellationToken ct) { var sql = """ - SELECT COUNT(*) count - FROM - "MonitoringObservers" MO - INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" - INNER JOIN "Observers" O ON O."Id" = MO."ObserverId" - INNER JOIN "AspNetUsers" U ON U."Id" = O."ApplicationUserId" - WHERE - MN."ElectionRoundId" = @electionRoundId - AND MN."NgoId" = @ngoId - AND (@searchText IS NULL OR @searchText = '' OR (U."FirstName" || ' ' || U."LastName") ILIKE @searchText OR U."Email" ILIKE @searchText OR u."PhoneNumber" ILIKE @searchText) - AND (@tagsFilter IS NULL OR cardinality(@tagsFilter) = 0 OR mo."Tags" && @tagsFilter) - AND (@status IS NULL OR mo."Status" = @status) - AND (@level1 IS NULL OR EXISTS ( - SELECT - 1 - FROM - ( - SELECT - PSI."PollingStationId" "PollingStationId" - FROM - "PollingStationInformation" PSI - INNER JOIN "PollingStations" PS ON PS."Id" = PSI."PollingStationId" - WHERE - PSI."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND PSI."ElectionRoundId" = @electionRoundId - UNION - SELECT - N."PollingStationId" "PollingStationId" - FROM - "Notes" N - INNER JOIN "PollingStations" PS ON PS."Id" = N."PollingStationId" - WHERE - N."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND N."ElectionRoundId" = @electionRoundId - UNION - SELECT - A."PollingStationId" "PollingStationId" - FROM - "Attachments" A - INNER JOIN "PollingStations" PS ON PS."Id" = A."PollingStationId" - WHERE - A."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND A."ElectionRoundId" = @electionRoundId - AND A."IsDeleted" = false AND A."IsCompleted" = true - UNION - SELECT - QR."PollingStationId" "PollingStationId" - FROM - "QuickReports" QR - INNER JOIN "PollingStations" PS ON PS."Id" = QR."PollingStationId" - WHERE - QR."PollingStationId" IS NOT NULL - AND QR."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND QR."ElectionRoundId" = @electionRoundId - UNION - SELECT - FS."PollingStationId" "PollingStationId" - FROM - "FormSubmissions" FS - INNER JOIN "PollingStations" PS ON PS."Id" = FS."PollingStationId" - WHERE - FS."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND FS."ElectionRoundId" = @electionRoundId - ) psVisits - INNER JOIN "PollingStations" PS ON psVisits."PollingStationId" = PS."Id" - WHERE - "ElectionRoundId" = @electionRoundId - AND ( - @level1 IS NULL - OR PS."Level1" = @level1 - ) - AND ( - @level2 IS NULL - OR PS."Level2" = @level2 - ) - AND ( - @level3 IS NULL - OR PS."Level3" = @level3 - ) - AND ( - @level4 IS NULL - OR PS."Level4" = @level4 - ) - AND ( - @level5 IS NULL - OR PS."Level5" = @level5 - ))); - - SELECT - "MonitoringObserverId", - "ObserverName", - "PhoneNumber", - "Email", - "Tags", - "Status" - FROM ( - SELECT - MO."Id" "MonitoringObserverId", - U."FirstName" || ' ' || U."LastName" "ObserverName", - U."PhoneNumber", - U."Email", - MO."Tags", - MO."Status" - FROM - "MonitoringObservers" MO - INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" - INNER JOIN "Observers" O ON O."Id" = MO."ObserverId" - INNER JOIN "AspNetUsers" U ON U."Id" = O."ApplicationUserId" - WHERE - MN."ElectionRoundId" = @electionRoundId - AND MN."NgoId" = @ngoId - AND (@searchText IS NULL OR @searchText = '' OR (U."FirstName" || ' ' || U."LastName") ILIKE @searchText OR u."Email" ILIKE @searchText OR u."PhoneNumber" ILIKE @searchText) - AND (@tagsFilter IS NULL OR cardinality(@tagsFilter) = 0 OR mo."Tags" && @tagsFilter) - AND (@status IS NULL OR mo."Status" = @status) - AND (@level1 IS NULL OR EXISTS ( - SELECT - 1 - FROM - ( - SELECT - PSI."PollingStationId" "PollingStationId" - FROM - "PollingStationInformation" PSI - INNER JOIN "PollingStations" PS ON PS."Id" = PSI."PollingStationId" - WHERE - PSI."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND PSI."ElectionRoundId" = @electionRoundId - UNION - SELECT - N."PollingStationId" "PollingStationId" - FROM - "Notes" N - INNER JOIN "PollingStations" PS ON PS."Id" = N."PollingStationId" - WHERE - N."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND N."ElectionRoundId" = @electionRoundId - UNION - SELECT - A."PollingStationId" "PollingStationId" - FROM - "Attachments" A - INNER JOIN "PollingStations" PS ON PS."Id" = A."PollingStationId" - WHERE - A."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND A."ElectionRoundId" = @electionRoundId - AND a."IsDeleted" = false AND a."IsCompleted" = true - UNION - SELECT - QR."PollingStationId" "PollingStationId" - FROM - "QuickReports" QR - INNER JOIN "PollingStations" PS ON PS."Id" = QR."PollingStationId" - WHERE - QR."PollingStationId" IS NOT NULL - AND QR."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND QR."ElectionRoundId" = @electionRoundId - UNION - SELECT - FS."PollingStationId" "PollingStationId" - FROM - "FormSubmissions" FS - INNER JOIN "PollingStations" PS ON PS."Id" = FS."PollingStationId" - WHERE - FS."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND FS."ElectionRoundId" = @electionRoundId - ) psVisits - INNER JOIN "PollingStations" PS ON psVisits."PollingStationId" = PS."Id" - WHERE - "ElectionRoundId" = @electionRoundId - AND ( - @level1 IS NULL - OR PS."Level1" = @level1 - ) - AND ( - @level2 IS NULL - OR PS."Level2" = @level2 - ) - AND ( - @level3 IS NULL - OR PS."Level3" = @level3 - ) - AND ( - @level4 IS NULL - OR PS."Level4" = @level4 - ) - AND ( - @level5 IS NULL - OR PS."Level5" = @level5 - ))) - ) T - - ORDER BY - CASE WHEN @sortExpression = 'ObserverName ASC' THEN "ObserverName" END ASC, - CASE WHEN @sortExpression = 'ObserverName DESC' THEN "ObserverName" END DESC, - - CASE WHEN @sortExpression = 'PhoneNumber ASC' THEN "PhoneNumber" END ASC, - CASE WHEN @sortExpression = 'PhoneNumber DESC' THEN "PhoneNumber" END DESC, - - CASE WHEN @sortExpression = 'Email ASC' THEN "Email" END ASC, - CASE WHEN @sortExpression = 'Email DESC' THEN "Email" END DESC, - - CASE WHEN @sortExpression = 'Tags ASC' THEN "Tags" END ASC, - CASE WHEN @sortExpression = 'Tags DESC' THEN "Tags" END DESC - - OFFSET @offset ROWS - FETCH NEXT @pageSize ROWS ONLY; - """; + WITH "ObserverPSI" AS + (SELECT f."Id" AS "FormId", + f."FormType" AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + fs."PollingStationId" AS "PollingStationId", + fs."FollowUpStatus" AS "FollowUpStatus", + COALESCE(FS."LastModifiedOn", FS."CreatedOn") AS "LastModifiedOn", + fs."IsCompleted" AS "IsCompleted", + CAST(NULL AS bigint) AS "MediaFilesCount", + CAST(NULL AS bigint) AS "NotesCount", + (CASE + WHEN FS."NumberOfQuestionsAnswered" = F."NumberOfQuestions" THEN 'All' + WHEN fs."NumberOfQuestionsAnswered" > 0 THEN 'Some' + WHEN fs."NumberOfQuestionsAnswered" = 0 THEN 'None' + END) "QuestionsAnswered", + (CASE + WHEN FS."NumberOfFlaggedAnswers" > 0 THEN TRUE + ELSE FALSE + END) "HasFlaggedAnswers", + CAST(NULL AS UUID) AS "QuickReportId", + NULL AS "IncidentCategory", + NULL AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "PollingStationInformation" FS ON MO."Id" = FS."MonitoringObserverId" + INNER JOIN "PollingStationInformationForms" F ON f."ElectionRoundId" = @electionRoundId + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversFormSubmissions" AS + (SELECT f."Id" AS "FormId", + f."FormType" AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + fs."PollingStationId" AS "PollingStationId", + fs."FollowUpStatus" AS "FollowUpStatus", + COALESCE(FS."LastModifiedOn", FS."CreatedOn") AS "LastModifiedOn", + fs."IsCompleted" AS "IsCompleted", + + (SELECT COUNT(*) + FROM "Attachments" A + WHERE A."FormId" = fs."FormId" + AND a."MonitoringObserverId" = fs."MonitoringObserverId" + AND fs."PollingStationId" = A."PollingStationId" + AND A."IsDeleted" = FALSE + AND A."IsCompleted" = TRUE) AS "MediaFilesCount", + + (SELECT COUNT(*) + FROM "Notes" N + WHERE N."FormId" = fs."FormId" + AND N."MonitoringObserverId" = fs."MonitoringObserverId" + AND fs."PollingStationId" = N."PollingStationId") AS "NotesCount", + (CASE + WHEN FS."NumberOfQuestionsAnswered" = F."NumberOfQuestions" THEN 'All' + WHEN fs."NumberOfQuestionsAnswered" > 0 THEN 'Some' + WHEN fs."NumberOfQuestionsAnswered" = 0 THEN 'None' + END) "QuestionsAnswered", + (CASE + WHEN FS."NumberOfFlaggedAnswers" > 0 THEN TRUE + ELSE FALSE + END) "HasFlaggedAnswers", + CAST(NULL AS UUID) AS "QuickReportId", + NULL AS "IncidentCategory", + NULL AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "FormSubmissions" FS ON MO."Id" = FS."MonitoringObserverId" + INNER JOIN "Forms" F ON FS."FormId" = F."Id" + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversQuickReports" AS + (SELECT CAST(NULL AS UUID) AS "FormId", + NULL AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + qr."PollingStationId" AS "PollingStationId", + NULL AS "FollowUpStatus", + COALESCE(qr."LastModifiedOn", qr."CreatedOn") AS "LastModifiedOn", + CAST(NULL AS boolean) AS "IsCompleted", + CAST(NULL AS bigint) AS "MediaFilesCount", + CAST(NULL AS bigint) AS "NotesCount", + NULL AS "QuestionsAnswered", + CAST(NULL AS boolean) AS "HasFlaggedAnswers", + qr."Id" AS "QuickReportId", + qr."IncidentCategory" AS "IncidentCategory", + qr."FollowUpStatus" AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "QuickReports" QR ON MO."Id" = QR."MonitoringObserverId" + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversActivity" AS + (SELECT * + FROM "ObserversFormSubmissions" + UNION ALL SELECT * + FROM "ObserversQuickReports" + UNION ALL SELECT * + FROM "ObserverPSI") + SELECT COUNT(DISTINCT OA."ObserverId") COUNT + FROM "ObserversActivity" OA + INNER JOIN "MonitoringObservers" mo ON mo."Id" = OA."MonitoringObserverId" + INNER JOIN "AspNetUsers" U ON U."Id" = OA."ObserverId" + LEFT JOIN "PollingStations" ps ON OA."PollingStationId" = ps."Id" + WHERE (@searchText IS NULL + OR @searchText = '' + OR (U."FirstName" || ' ' || U."LastName") ILIKE @searchText + OR U."Email" ILIKE @searchText + OR u."PhoneNumber" ILIKE @searchText + OR mo."Id"::text ILIKE @searchText) + AND (@tagsFilter IS NULL OR cardinality(@tagsFilter) = 0 OR mo."Tags" && @tagsFilter) + AND (@monitoringObserverStatus IS NULL OR mo."Status" = @monitoringObserverStatus) + AND (@formType IS NULL OR OA."FormType" = @formType) + AND (@level1 IS NULL OR ps."Level1" = @level1) + AND (@level2 IS NULL OR ps."Level2" = @level2) + AND (@level3 IS NULL OR ps."Level3" = @level3) + AND (@level4 IS NULL OR ps."Level4" = @level4) + AND (@level5 IS NULL OR ps."Level5" = @level5) + AND (@pollingStationNumber IS NULL OR ps."Number" = @pollingStationNumber) + AND (@hasFlaggedAnswers IS NULL OR OA."HasFlaggedAnswers" = @hasFlaggedAnswers) + AND (@submissionsFollowUpStatus IS NULL OR OA."FollowUpStatus" = @submissionsFollowUpStatus) + AND (@formId IS NULL OR OA."FormId" = @formId) + AND (@questionsAnswered IS NULL OR OA."QuestionsAnswered" = @questionsAnswered) + AND (@hasAttachments IS NULL OR (@hasAttachments = TRUE AND OA."MediaFilesCount" > 0) OR (@hasAttachments = FALSE AND OA."MediaFilesCount" = 0)) + AND (@hasNotes IS NULL OR (OA."NotesCount" = 0 AND @hasNotes = FALSE) OR (OA."NotesCount" > 0 AND @hasNotes = TRUE)) + AND (@fromDate IS NULL OR OA."LastModifiedOn" >= @fromDate::timestamp) + AND (@toDate IS NULL OR OA."LastModifiedOn" <= @toDate::timestamp) + AND (@isCompleted IS NULL OR OA."IsCompleted" = @isCompleted) + AND (@hasQuickReports IS NULL OR (@hasQuickReports = TRUE AND OA."QuickReportId" IS NOT NULL) OR (@hasQuickReports = FALSE AND OA."QuickReportId" IS NULL)) + AND (@quickReportFollowUpStatus IS NULL OR OA."QuickReportFollowUpStatus" = @quickReportFollowUpStatus) + AND (@quickReportIncidentCategory IS NULL OR OA."IncidentCategory" = @quickReportIncidentCategory); + + ------------------------------------------------------------------------------------------------- + WITH "ObserverPSI" AS + (SELECT f."Id" AS "FormId", + f."FormType" AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + fs."PollingStationId" AS "PollingStationId", + fs."FollowUpStatus" AS "FollowUpStatus", + COALESCE(FS."LastModifiedOn", FS."CreatedOn") AS "LastModifiedOn", + fs."IsCompleted" AS "IsCompleted", + CAST(NULL AS bigint) AS "MediaFilesCount", + CAST(NULL AS bigint) AS "NotesCount", + (CASE + WHEN FS."NumberOfQuestionsAnswered" = F."NumberOfQuestions" THEN 'All' + WHEN fs."NumberOfQuestionsAnswered" > 0 THEN 'Some' + WHEN fs."NumberOfQuestionsAnswered" = 0 THEN 'None' + END) "QuestionsAnswered", + (CASE + WHEN FS."NumberOfFlaggedAnswers" > 0 THEN TRUE + ELSE FALSE + END) "HasFlaggedAnswers", + CAST(NULL AS UUID) AS "QuickReportId", + NULL AS "IncidentCategory", + NULL AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "PollingStationInformation" FS ON MO."Id" = FS."MonitoringObserverId" + INNER JOIN "PollingStationInformationForms" F ON f."ElectionRoundId" = @electionRoundId + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversFormSubmissions" AS + (SELECT f."Id" AS "FormId", + f."FormType" AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + fs."PollingStationId" AS "PollingStationId", + fs."FollowUpStatus" AS "FollowUpStatus", + COALESCE(FS."LastModifiedOn", FS."CreatedOn") AS "LastModifiedOn", + fs."IsCompleted" AS "IsCompleted", + + (SELECT COUNT(*) + FROM "Attachments" A + WHERE A."FormId" = fs."FormId" + AND a."MonitoringObserverId" = fs."MonitoringObserverId" + AND fs."PollingStationId" = A."PollingStationId" + AND A."IsDeleted" = FALSE + AND A."IsCompleted" = TRUE) AS "MediaFilesCount", + + (SELECT COUNT(*) + FROM "Notes" N + WHERE N."FormId" = fs."FormId" + AND N."MonitoringObserverId" = fs."MonitoringObserverId" + AND fs."PollingStationId" = N."PollingStationId") AS "NotesCount", + (CASE + WHEN FS."NumberOfQuestionsAnswered" = F."NumberOfQuestions" THEN 'ALL' + WHEN fs."NumberOfQuestionsAnswered" > 0 THEN 'SOME' + WHEN fs."NumberOfQuestionsAnswered" = 0 THEN 'NONE' + END) "QuestionsAnswered", + (CASE + WHEN FS."NumberOfFlaggedAnswers" > 0 THEN TRUE + ELSE FALSE + END) "HasFlaggedAnswers", + CAST(NULL AS UUID) AS "QuickReportId", + NULL AS "IncidentCategory", + NULL AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "FormSubmissions" FS ON MO."Id" = FS."MonitoringObserverId" + INNER JOIN "Forms" F ON FS."FormId" = F."Id" + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversQuickReports" AS + (SELECT CAST(NULL AS UUID) AS "FormId", + NULL AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + qr."PollingStationId" AS "PollingStationId", + NULL AS "FollowUpStatus", + COALESCE(qr."LastModifiedOn", qr."CreatedOn") AS "LastModifiedOn", + CAST(NULL AS boolean) AS "IsCompleted", + CAST(NULL AS bigint) AS "MediaFilesCount", + CAST(NULL AS bigint) AS "NotesCount", + NULL AS "QuestionsAnswered", + CAST(NULL AS boolean) AS "HasFlaggedAnswers", + qr."Id" AS "QuickReportId", + qr."IncidentCategory" AS "IncidentCategory", + qr."FollowUpStatus" AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "QuickReports" QR ON MO."Id" = QR."MonitoringObserverId" + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversActivity" AS + (SELECT * + FROM "ObserversFormSubmissions" + UNION ALL SELECT * + FROM "ObserversQuickReports" + UNION ALL SELECT * + FROM "ObserverPSI"), + "FilteredObservers" AS + (SELECT DISTINCT OA."MonitoringObserverId", + U."FirstName" || ' ' || U."LastName" "ObserverName", + U."PhoneNumber", + U."Email", + MO."Tags", + MO."Status" + FROM "ObserversActivity" OA + INNER JOIN "MonitoringObservers" mo ON mo."Id" = OA."MonitoringObserverId" + INNER JOIN "AspNetUsers" U ON U."Id" = OA."ObserverId" + LEFT JOIN "PollingStations" ps ON OA."PollingStationId" = ps."Id" + WHERE (@searchText IS NULL + OR @searchText = '' + OR (U."FirstName" || ' ' || U."LastName") ILIKE @searchText + OR U."Email" ILIKE @searchText + OR u."PhoneNumber" ILIKE @searchText + OR mo."Id"::text ILIKE @searchText) + AND (@tagsFilter IS NULL OR cardinality(@tagsFilter) = 0 OR mo."Tags" && @tagsFilter) + AND (@monitoringObserverStatus IS NULL OR mo."Status" = @monitoringObserverStatus) + AND (@formType IS NULL OR OA."FormType" = @formType) + AND (@level1 IS NULL OR ps."Level1" = @level1) + AND (@level2 IS NULL OR ps."Level2" = @level2) + AND (@level3 IS NULL OR ps."Level3" = @level3) + AND (@level4 IS NULL OR ps."Level4" = @level4) + AND (@level5 IS NULL OR ps."Level5" = @level5) + AND (@pollingStationNumber IS NULL OR ps."Number" = @pollingStationNumber) + AND (@hasFlaggedAnswers IS NULL OR OA."HasFlaggedAnswers" = @hasFlaggedAnswers) + AND (@submissionsFollowUpStatus IS NULL OR OA."FollowUpStatus" = @submissionsFollowUpStatus) + AND (@formId IS NULL OR OA."FormId" = @formId) + AND (@questionsAnswered IS NULL OR OA."QuestionsAnswered" = @questionsAnswered) + AND (@hasAttachments IS NULL OR (@hasAttachments = TRUE AND OA."MediaFilesCount" > 0) OR (@hasAttachments = FALSE AND OA."MediaFilesCount" = 0)) + AND (@hasNotes IS NULL OR (OA."NotesCount" = 0 AND @hasNotes = FALSE) OR (OA."NotesCount" > 0 AND @hasNotes = TRUE)) + AND (@fromDate IS NULL OR OA."LastModifiedOn" >= @fromDate::timestamp) + AND (@toDate IS NULL OR OA."LastModifiedOn" <= @toDate::timestamp) + AND (@isCompleted IS NULL OR OA."IsCompleted" = @isCompleted) + AND (@hasQuickReports IS NULL OR (@hasQuickReports = TRUE AND OA."QuickReportId" IS NOT NULL) + OR (@hasQuickReports = FALSE AND OA."QuickReportId" IS NULL)) + AND (@quickReportFollowUpStatus IS NULL OR OA."QuickReportFollowUpStatus" = @quickReportFollowUpStatus) + AND (@quickReportIncidentCategory IS NULL OR OA."IncidentCategory" = @quickReportIncidentCategory)) + SELECT * + FROM "FilteredObservers" + ORDER BY CASE + WHEN @sortExpression = 'ObserverName ASC' THEN "ObserverName" END ASC, + CASE WHEN @sortExpression = 'ObserverName DESC' THEN "ObserverName" END DESC, + CASE WHEN @sortExpression = 'PhoneNumber ASC' THEN "PhoneNumber" END ASC, + CASE WHEN @sortExpression = 'PhoneNumber DESC' THEN "PhoneNumber" END DESC, + CASE WHEN @sortExpression = 'Email ASC' THEN "Email" END ASC, + CASE WHEN @sortExpression = 'Email DESC' THEN "Email" END DESC, + CASE WHEN @sortExpression = 'Tags ASC' THEN "Tags" END ASC, + CASE WHEN @sortExpression = 'Tags DESC' THEN "Tags" END DESC, + CASE WHEN @sortExpression = 'Status ASC' THEN "Status" END ASC, + CASE WHEN @sortExpression = 'Status DESC' THEN "Status" END DESC + OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY; + """; var queryArgs = new { electionRoundId = req.ElectionRoundId, ngoId = req.NgoId, - offset = PaginationHelper.CalculateSkip(req.PageSize, req.PageNumber), - pageSize = req.PageSize, - tagsFilter = req.TagsFilter ?? [], searchText = $"%{req.SearchText?.Trim() ?? string.Empty}%", - status = req.StatusFilter?.ToString(), + formType = req.FormTypeFilter?.ToString(), level1 = req.Level1Filter, level2 = req.Level2Filter, level3 = req.Level3Filter, level4 = req.Level4Filter, level5 = req.Level5Filter, + pollingStationNumber = req.PollingStationNumberFilter, + hasFlaggedAnswers = req.HasFlaggedAnswers, + submissionsFollowUpStatus = req.SubmissionsFollowUpStatus?.ToString(), + tagsFilter = req.TagsFilter ?? [], + monitoringObserverStatus = req.MonitoringObserverStatus?.ToString(), + formId = req.FormId, + hasNotes = req.HasNotes, + hasAttachments = req.HasAttachments, + questionsAnswered = req.QuestionsAnswered?.ToString(), + fromDate = req.FromDateFilter?.ToString("O"), + toDate = req.ToDateFilter?.ToString("O"), + isCompleted = req.IsCompletedFilter, + + hasQuickReports = req.HasQuickReports, + quickReportFollowUpStatus = req.QuickReportFollowUpStatus?.ToString(), + quickReportIncidentCategory = req.QuickReportIncidentCategory?.ToString(), + + offset = PaginationHelper.CalculateSkip(req.PageSize, req.PageNumber), + pageSize = req.PageSize, + sortExpression = GetSortExpression(req.SortColumnName, req.IsAscendingSorting), }; @@ -276,25 +362,36 @@ private static string GetSortExpression(string? sortColumnName, bool isAscending var sortOrder = isAscendingSorting ? "ASC" : "DESC"; - if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.ObserverName), StringComparison.InvariantCultureIgnoreCase)) + if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.ObserverName), + StringComparison.InvariantCultureIgnoreCase)) { return $"{nameof(TargetedMonitoringObserverModel.ObserverName)} {sortOrder}"; } - if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.Email), StringComparison.InvariantCultureIgnoreCase)) + + if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.Email), + StringComparison.InvariantCultureIgnoreCase)) { return $"{nameof(TargetedMonitoringObserverModel.Email)} {sortOrder}"; } - if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.PhoneNumber), StringComparison.InvariantCultureIgnoreCase)) + if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.PhoneNumber), + StringComparison.InvariantCultureIgnoreCase)) { return $"{nameof(TargetedMonitoringObserverModel.PhoneNumber)} {sortOrder}"; } - if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.Tags), StringComparison.InvariantCultureIgnoreCase)) + if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.Tags), + StringComparison.InvariantCultureIgnoreCase)) { return $"{nameof(TargetedMonitoringObserverModel.Tags)} {sortOrder}"; } + if (string.Equals(sortColumnName, nameof(TargetedMonitoringObserverModel.Status), + StringComparison.InvariantCultureIgnoreCase)) + { + return $"{nameof(TargetedMonitoringObserverModel.Status)} {sortOrder}"; + } + return $"{nameof(TargetedMonitoringObserverModel.ObserverName)} ASC"; } -} +} \ No newline at end of file diff --git a/api/src/Feature.Notifications/ListRecipients/Request.cs b/api/src/Feature.Notifications/ListRecipients/Request.cs index 5cd90cc33..795b258cb 100644 --- a/api/src/Feature.Notifications/ListRecipients/Request.cs +++ b/api/src/Feature.Notifications/ListRecipients/Request.cs @@ -1,6 +1,9 @@ using Vote.Monitor.Core.Models; using Vote.Monitor.Core.Security; +using Vote.Monitor.Domain.Entities.FormAggregate; +using Vote.Monitor.Domain.Entities.FormSubmissionAggregate; using Vote.Monitor.Domain.Entities.MonitoringObserverAggregate; +using Vote.Monitor.Domain.Entities.QuickReportAggregate; namespace Feature.Notifications.ListRecipients; @@ -10,28 +13,39 @@ public class Request : BaseSortPaginatedRequest [FromClaim(ApplicationClaimTypes.NgoId)] public Guid NgoId { get; set; } + + [QueryParam] public string? SearchText { get; set; } - [QueryParam] - public string? SearchText { get; set; } + [QueryParam] public FormType? FormTypeFilter { get; set; } - [QueryParam] - public string? Level1Filter { get; set; } + [QueryParam] public string? Level1Filter { get; set; } - [QueryParam] - public string? Level2Filter { get; set; } + [QueryParam] public string? Level2Filter { get; set; } - [QueryParam] - public string? Level3Filter { get; set; } + [QueryParam] public string? Level3Filter { get; set; } - [QueryParam] - public string? Level4Filter { get; set; } + [QueryParam] public string? Level4Filter { get; set; } - [QueryParam] - public string? Level5Filter { get; set; } + [QueryParam] public string? Level5Filter { get; set; } - [QueryParam] - public MonitoringObserverStatus? StatusFilter { get; set; } + [QueryParam] public string? PollingStationNumberFilter { get; set; } - [QueryParam] - public string[]? TagsFilter { get; set; } -} + [QueryParam] public bool? HasFlaggedAnswers { get; set; } + + [QueryParam] public SubmissionFollowUpStatus? SubmissionsFollowUpStatus { get; set; } + + [QueryParam] public string[]? TagsFilter { get; set; } = []; + + [QueryParam] public MonitoringObserverStatus? MonitoringObserverStatus { get; set; } + [QueryParam] public Guid? FormId { get; set; } + [QueryParam] public bool? HasNotes { get; set; } + [QueryParam] public bool? HasAttachments { get; set; } + [QueryParam] public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + [QueryParam] public DateTime? FromDateFilter { get; set; } + [QueryParam] public DateTime? ToDateFilter { get; set; } + [QueryParam] public bool? IsCompletedFilter { get; set; } + [QueryParam] public QuickReportFollowUpStatus? QuickReportFollowUpStatus { get; set; } + [QueryParam] public IncidentCategory? QuickReportIncidentCategory { get; set; } + [QueryParam] public bool? HasQuickReports { get; set; } + +} \ No newline at end of file diff --git a/api/src/Feature.Notifications/Send/Endpoint.cs b/api/src/Feature.Notifications/Send/Endpoint.cs index e473fa77e..29cc57e25 100644 --- a/api/src/Feature.Notifications/Send/Endpoint.cs +++ b/api/src/Feature.Notifications/Send/Endpoint.cs @@ -27,115 +27,198 @@ public override void Configure() public override async Task, ProblemHttpResult>> ExecuteAsync(Request req, CancellationToken ct) { var sql = """ - SELECT - MO."Id", - NT."Token" - FROM - "MonitoringObservers" MO - INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" - INNER JOIN "Observers" O ON O."Id" = MO."ObserverId" - INNER JOIN "AspNetUsers" U ON U."Id" = O."ApplicationUserId" - LEFT JOIN "NotificationTokens" NT ON NT."ObserverId" = MO."ObserverId" - WHERE - MN."ElectionRoundId" = @electionRoundId - AND MN."NgoId" = @ngoId - AND (@searchText IS NULL OR @searchText = '' OR (U."FirstName" || ' ' || U."LastName") ILIKE @searchText OR u."Email" ILIKE @searchText OR u."PhoneNumber" ILIKE @searchText) - AND (@tagsFilter IS NULL OR cardinality(@tagsFilter) = 0 OR mo."Tags" && @tagsFilter) - AND (@status IS NULL OR mo."Status" = @status) - AND (@level1 IS NULL OR EXISTS ( - SELECT - 1 - FROM - ( - SELECT - PSI."PollingStationId" "PollingStationId" - FROM - "PollingStationInformation" PSI - INNER JOIN "PollingStations" PS ON PS."Id" = PSI."PollingStationId" - WHERE - PSI."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND PSI."ElectionRoundId" = @electionRoundId - UNION - SELECT - N."PollingStationId" "PollingStationId" - FROM - "Notes" N - INNER JOIN "PollingStations" PS ON PS."Id" = N."PollingStationId" - WHERE - N."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND N."ElectionRoundId" = @electionRoundId - UNION - SELECT - A."PollingStationId" "PollingStationId" - FROM - "Attachments" A - INNER JOIN "PollingStations" PS ON PS."Id" = A."PollingStationId" - WHERE - A."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND A."ElectionRoundId" = @electionRoundId - AND a."IsDeleted" = false AND a."IsCompleted" = true - UNION - SELECT - QR."PollingStationId" "PollingStationId" - FROM - "QuickReports" QR - INNER JOIN "PollingStations" PS ON PS."Id" = QR."PollingStationId" - WHERE - QR."PollingStationId" IS NOT NULL - AND QR."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND QR."ElectionRoundId" = @electionRoundId - UNION - SELECT - FS."PollingStationId" "PollingStationId" - FROM - "FormSubmissions" FS - INNER JOIN "PollingStations" PS ON PS."Id" = FS."PollingStationId" - WHERE - FS."MonitoringObserverId" = MO."Id" - AND PS."ElectionRoundId" = @electionRoundId - AND FS."ElectionRoundId" = @electionRoundId - ) psVisits - INNER JOIN "PollingStations" PS ON psVisits."PollingStationId" = PS."Id" - WHERE - "ElectionRoundId" = @electionRoundId - AND ( - @level1 IS NULL - OR PS."Level1" = @level1 - ) - AND ( - @level2 IS NULL - OR PS."Level2" = @level2 - ) - AND ( - @level3 IS NULL - OR PS."Level3" = @level3 - ) - AND ( - @level4 IS NULL - OR PS."Level4" = @level4 - ) - AND ( - @level5 IS NULL - OR PS."Level5" = @level5 - ))) + WITH "ObserverPSI" AS + (SELECT f."Id" AS "FormId", + f."FormType" AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + fs."PollingStationId" AS "PollingStationId", + fs."FollowUpStatus" AS "FollowUpStatus", + COALESCE(FS."LastModifiedOn", FS."CreatedOn") AS "LastModifiedOn", + fs."IsCompleted" AS "IsCompleted", + CAST(NULL AS bigint) AS "MediaFilesCount", + CAST(NULL AS bigint) AS "NotesCount", + (CASE + WHEN FS."NumberOfQuestionsAnswered" = F."NumberOfQuestions" THEN 'All' + WHEN fs."NumberOfQuestionsAnswered" > 0 THEN 'Some' + WHEN fs."NumberOfQuestionsAnswered" = 0 THEN 'None' + END) "QuestionsAnswered", + (CASE + WHEN FS."NumberOfFlaggedAnswers" > 0 THEN TRUE + ELSE FALSE + END) "HasFlaggedAnswers", + CAST(NULL AS UUID) AS "QuickReportId", + NULL AS "IncidentCategory", + NULL AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "PollingStationInformation" FS ON MO."Id" = FS."MonitoringObserverId" + INNER JOIN "PollingStationInformationForms" F ON f."ElectionRoundId" = @electionRoundId + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversFormSubmissions" AS + (SELECT f."Id" AS "FormId", + f."FormType" AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + fs."PollingStationId" AS "PollingStationId", + fs."FollowUpStatus" AS "FollowUpStatus", + COALESCE(FS."LastModifiedOn", FS."CreatedOn") AS "LastModifiedOn", + fs."IsCompleted" AS "IsCompleted", + + (SELECT COUNT(*) + FROM "Attachments" A + WHERE A."FormId" = fs."FormId" + AND a."MonitoringObserverId" = fs."MonitoringObserverId" + AND fs."PollingStationId" = A."PollingStationId" + AND A."IsDeleted" = FALSE + AND A."IsCompleted" = TRUE) AS "MediaFilesCount", + + (SELECT COUNT(*) + FROM "Notes" N + WHERE N."FormId" = fs."FormId" + AND N."MonitoringObserverId" = fs."MonitoringObserverId" + AND fs."PollingStationId" = N."PollingStationId") AS "NotesCount", + (CASE + WHEN FS."NumberOfQuestionsAnswered" = F."NumberOfQuestions" THEN 'ALL' + WHEN fs."NumberOfQuestionsAnswered" > 0 THEN 'SOME' + WHEN fs."NumberOfQuestionsAnswered" = 0 THEN 'NONE' + END) "QuestionsAnswered", + (CASE + WHEN FS."NumberOfFlaggedAnswers" > 0 THEN TRUE + ELSE FALSE + END) "HasFlaggedAnswers", + CAST(NULL AS UUID) AS "QuickReportId", + NULL AS "IncidentCategory", + NULL AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "FormSubmissions" FS ON MO."Id" = FS."MonitoringObserverId" + INNER JOIN "Forms" F ON FS."FormId" = F."Id" + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversQuickReports" AS + (SELECT CAST(NULL AS UUID) AS "FormId", + NULL AS "FormType", + mo."ObserverId" AS "ObserverId", + mo."Id" AS "MonitoringObserverId", + qr."PollingStationId" AS "PollingStationId", + NULL AS "FollowUpStatus", + COALESCE(qr."LastModifiedOn", qr."CreatedOn") AS "LastModifiedOn", + CAST(NULL AS boolean) AS "IsCompleted", + CAST(NULL AS bigint) AS "MediaFilesCount", + CAST(NULL AS bigint) AS "NotesCount", + NULL AS "QuestionsAnswered", + CAST(NULL AS boolean) AS "HasFlaggedAnswers", + qr."Id" AS "QuickReportId", + qr."IncidentCategory" AS "IncidentCategory", + qr."FollowUpStatus" AS "QuickReportFollowUpStatus" + FROM "MonitoringObservers" MO + INNER JOIN "MonitoringNgos" MN ON MN."Id" = MO."MonitoringNgoId" + INNER JOIN "QuickReports" QR ON MO."Id" = QR."MonitoringObserverId" + WHERE MN."ElectionRoundId" = @electionRoundId + AND MN."NgoId" = @ngoId), + "ObserversActivity" AS + (SELECT * + FROM "ObserversFormSubmissions" + UNION ALL SELECT * + FROM "ObserversQuickReports" + UNION ALL SELECT * + FROM "ObserverPSI") + SELECT DISTINCT OA."MonitoringObserverId", + NT."Token" + FROM "ObserversActivity" OA + INNER JOIN "MonitoringObservers" mo ON mo."Id" = OA."MonitoringObserverId" + INNER JOIN "AspNetUsers" U ON U."Id" = OA."ObserverId" + LEFT JOIN "PollingStations" ps ON OA."PollingStationId" = ps."Id" + LEFT JOIN "NotificationTokens" NT ON NT."ObserverId" = OA."ObserverId" + WHERE (@searchText IS NULL + OR @searchText = '' + OR (U."FirstName" || ' ' || U."LastName") ILIKE @searchText + OR U."Email" ILIKE @searchText + OR u."PhoneNumber" ILIKE @searchText + OR mo."Id"::text ILIKE @searchText) + AND (@tagsFilter IS NULL + OR cardinality(@tagsFilter) = 0 + OR mo."Tags" && @tagsFilter) + AND (@monitoringObserverStatus IS NULL + OR mo."Status" = @monitoringObserverStatus) + AND (@formType IS NULL + OR OA."FormType" = @formType) + AND (@level1 IS NULL + OR ps."Level1" = @level1) + AND (@level2 IS NULL + OR ps."Level2" = @level2) + AND (@level3 IS NULL + OR ps."Level3" = @level3) + AND (@level4 IS NULL + OR ps."Level4" = @level4) + AND (@level5 IS NULL + OR ps."Level5" = @level5) + AND (@pollingStationNumber IS NULL + OR ps."Number" = @pollingStationNumber) + AND (@hasFlaggedAnswers IS NULL + OR OA."HasFlaggedAnswers" = @hasFlaggedAnswers) + AND (@submissionsFollowUpStatus IS NULL + OR OA."FollowUpStatus" = @submissionsFollowUpStatus) + AND (@formId IS NULL + OR OA."FormId" = @formId) + AND (@questionsAnswered IS NULL + OR OA."QuestionsAnswered" = @questionsAnswered) + AND (@hasAttachments IS NULL + OR (@hasAttachments = TRUE + AND OA."MediaFilesCount" > 0) + OR (@hasAttachments = FALSE + AND OA."MediaFilesCount" = 0)) + AND (@hasNotes IS NULL + OR (OA."NotesCount" = 0 + AND @hasNotes = FALSE) + OR (OA."NotesCount" > 0 + AND @hasNotes = TRUE)) + AND (@fromDate IS NULL + OR OA."LastModifiedOn" >= @fromDate::timestamp) + AND (@toDate IS NULL + OR OA."LastModifiedOn" <= @toDate::timestamp) + AND (@isCompleted IS NULL + OR OA."IsCompleted" = @isCompleted) + AND (@hasQuickReports IS NULL + OR (@hasQuickReports = TRUE + AND OA."QuickReportId" IS NOT NULL) + OR (@hasQuickReports = FALSE + AND OA."QuickReportId" IS NULL)) + AND (@quickReportFollowUpStatus IS NULL + OR OA."QuickReportFollowUpStatus" = @quickReportFollowUpStatus) + AND (@quickReportIncidentCategory IS NULL + OR OA."IncidentCategory" = @quickReportIncidentCategory) """; var queryArgs = new { electionRoundId = req.ElectionRoundId, ngoId = req.NgoId, - tagsFilter = req.TagsFilter ?? [], searchText = $"%{req.SearchText?.Trim() ?? string.Empty}%", - status = req.StatusFilter?.ToString(), + formType = req.FormTypeFilter?.ToString(), level1 = req.Level1Filter, level2 = req.Level2Filter, level3 = req.Level3Filter, level4 = req.Level4Filter, - level5 = req.Level5Filter + level5 = req.Level5Filter, + pollingStationNumber = req.PollingStationNumberFilter, + hasFlaggedAnswers = req.HasFlaggedAnswers, + submissionsFollowUpStatus = req.SubmissionsFollowUpStatus?.ToString(), + tagsFilter = req.TagsFilter ?? [], + monitoringObserverStatus = req.MonitoringObserverStatus?.ToString(), + formId = req.FormId, + hasNotes = req.HasNotes, + hasAttachments = req.HasAttachments, + questionsAnswered = req.QuestionsAnswered?.ToString(), + fromDate = req.FromDateFilter?.ToString("O"), + toDate = req.ToDateFilter?.ToString("O"), + isCompleted = req.IsCompletedFilter, + + hasQuickReports = req.HasQuickReports, + quickReportFollowUpStatus = req.QuickReportFollowUpStatus?.ToString(), + quickReportIncidentCategory = req.QuickReportIncidentCategory?.ToString(), }; IEnumerable result = []; @@ -146,7 +229,7 @@ @level5 IS NULL var recipients = result.ToList(); - var monitoringObserverIds = recipients.Select(x => x.Id).Distinct().ToList(); + var monitoringObserverIds = recipients.Select(x => x.MonitoringObserverId).Distinct().ToList(); var pushNotificationTokens = recipients .Select(x => x.Token) .Where(x => !string.IsNullOrWhiteSpace(x)) @@ -167,7 +250,7 @@ @level5 IS NULL await repository.AddAsync(notification, ct); - jobService.EnqueueSendNotifications(Enumerable.Range(1,10).Select(x=> pushNotificationTokens.First()).ToList(), req.Title, sanitizedMessage); + jobService.EnqueueSendNotifications(pushNotificationTokens, req.Title, sanitizedMessage); return TypedResults.Ok(new Response { diff --git a/api/src/Feature.Notifications/Send/NotificationRecipient.cs b/api/src/Feature.Notifications/Send/NotificationRecipient.cs index 68895f9cf..deffadf11 100644 --- a/api/src/Feature.Notifications/Send/NotificationRecipient.cs +++ b/api/src/Feature.Notifications/Send/NotificationRecipient.cs @@ -2,6 +2,6 @@ public class NotificationRecipient { - public Guid Id { get; set; } + public Guid MonitoringObserverId { get; set; } public string? Token { get; set; } } diff --git a/api/src/Feature.Notifications/Send/Request.cs b/api/src/Feature.Notifications/Send/Request.cs index 918c85109..d7636af70 100644 --- a/api/src/Feature.Notifications/Send/Request.cs +++ b/api/src/Feature.Notifications/Send/Request.cs @@ -1,5 +1,9 @@ -using Vote.Monitor.Core.Security; +using Vote.Monitor.Core.Models; +using Vote.Monitor.Core.Security; +using Vote.Monitor.Domain.Entities.FormAggregate; +using Vote.Monitor.Domain.Entities.FormSubmissionAggregate; using Vote.Monitor.Domain.Entities.MonitoringObserverAggregate; +using Vote.Monitor.Domain.Entities.QuickReportAggregate; namespace Feature.Notifications.Send; @@ -15,12 +19,38 @@ public class Request public string Title { get; set; } public string Body { get; set; } + public string? SearchText { get; set; } + + public FormType? FormTypeFilter { get; set; } + public string? Level1Filter { get; set; } + public string? Level2Filter { get; set; } + public string? Level3Filter { get; set; } + public string? Level4Filter { get; set; } + public string? Level5Filter { get; set; } - public MonitoringObserverStatus? StatusFilter { get; set; } - public string[]? TagsFilter { get; set; } -} + + public string? PollingStationNumberFilter { get; set; } + + public bool? HasFlaggedAnswers { get; set; } + + public SubmissionFollowUpStatus? SubmissionsFollowUpStatus { get; set; } + + public string[]? TagsFilter { get; set; } = []; + + public MonitoringObserverStatus? MonitoringObserverStatus { get; set; } + public Guid? FormId { get; set; } + public bool? HasNotes { get; set; } + public bool? HasAttachments { get; set; } + public QuestionsAnsweredFilter? QuestionsAnswered { get; set; } + public DateTime? FromDateFilter { get; set; } + public DateTime? ToDateFilter { get; set; } + public bool? IsCompletedFilter { get; set; } + public QuickReportFollowUpStatus? QuickReportFollowUpStatus { get; set; } + public IncidentCategory? QuickReportIncidentCategory { get; set; } + public bool? HasQuickReports { get; set; } +} \ No newline at end of file diff --git a/api/src/Feature.ObserverGuide/Update/Validator.cs b/api/src/Feature.ObserverGuide/Update/Validator.cs index 6497e8b3d..b3ea38eae 100644 --- a/api/src/Feature.ObserverGuide/Update/Validator.cs +++ b/api/src/Feature.ObserverGuide/Update/Validator.cs @@ -1,6 +1,4 @@ -using Vote.Monitor.Core.Validators; - -namespace Feature.ObserverGuide.Update; +namespace Feature.ObserverGuide.Update; public class Validator : Validator { diff --git a/api/src/Vote.Monitor.Api.Feature.ElectionRound/Create/Endpoint.cs b/api/src/Vote.Monitor.Api.Feature.ElectionRound/Create/Endpoint.cs index 2b13a4a2f..27ed55945 100644 --- a/api/src/Vote.Monitor.Api.Feature.ElectionRound/Create/Endpoint.cs +++ b/api/src/Vote.Monitor.Api.Feature.ElectionRound/Create/Endpoint.cs @@ -1,6 +1,13 @@ -namespace Vote.Monitor.Api.Feature.ElectionRound.Create; +using Vote.Monitor.Core.Constants; +using Vote.Monitor.Core.Models; +using Vote.Monitor.Domain.Entities.FormBase.Questions; +using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate; -public class Endpoint(IRepository repository) +namespace Vote.Monitor.Api.Feature.ElectionRound.Create; + +public class Endpoint( + IRepository repository, + IRepository psiFormsRepository) : Endpoint, Conflict>> { public override void Configure() @@ -9,7 +16,8 @@ public override void Configure() Policies(PolicyNames.PlatformAdminsOnly); } - public override async Task, Conflict>> ExecuteAsync(Request req, CancellationToken ct) + public override async Task, Conflict>> ExecuteAsync(Request req, + CancellationToken ct) { var specification = new GetActiveElectionRoundSpecification(req.CountryId, req.Title); var hasElectionRoundWithSameTitle = await repository.AnyAsync(specification, ct); @@ -20,11 +28,22 @@ public override async Task, Conflict languages = new[] { LanguagesList.EN.Iso1 }; + var option1 = SelectOption.Create(Guid.NewGuid(), TranslatedString.New(languages, "Yes")); + var option2 = SelectOption.Create(Guid.NewGuid(), TranslatedString.New(languages, "Yes!")); + var questions = new BaseQuestion[] + { + SingleSelectQuestion.Create(Guid.NewGuid(), "PSI", + TranslatedString.New(languages, "Did you forgot to add PSI questions?"), [option1, option2]) + }; + var psiForm = PollingStationInformationForm.Create(electionRound, LanguagesList.EN.Iso1, languages, questions); + await psiFormsRepository.AddAsync(psiForm, ct); var country = CountriesList.Get(req.CountryId)!; - + return TypedResults.Ok(new ElectionRoundModel { Id = electionRound.Id, @@ -42,4 +61,4 @@ public override async Task, Conflict ExecuteAsync(CancellationToken ct) { var electionRounds = await context.ElectionRounds .Include(x => x.Country) - .Where(x => x.CitizenReportingEnabled && x.Status != ElectionRoundStatus.Archived) + .Where(x => x.CitizenReportingEnabled && x.Status == ElectionRoundStatus.Started) .Select(x => new ElectionRoundModel { Id = x.Id, diff --git a/api/src/Vote.Monitor.Api.Feature.ElectionRound/Specifications/GetObserverElectionSpecification.cs b/api/src/Vote.Monitor.Api.Feature.ElectionRound/Specifications/GetObserverElectionSpecification.cs index 98b70e692..c7b2bcc64 100644 --- a/api/src/Vote.Monitor.Api.Feature.ElectionRound/Specifications/GetObserverElectionSpecification.cs +++ b/api/src/Vote.Monitor.Api.Feature.ElectionRound/Specifications/GetObserverElectionSpecification.cs @@ -7,7 +7,7 @@ public GetObserverElectionSpecification(Guid observerId) Query .Include(x => x.MonitoringNgos) .ThenInclude(x => x.MonitoringObservers) - .Where(x => x.Status != ElectionRoundStatus.Archived) + .Where(x => x.Status == ElectionRoundStatus.Started) .Where(x => x.MonitoringNgos.Any(ngo => ngo.MonitoringObservers.Any(o => o.ObserverId == observerId))); Query.Select(x => new ElectionRoundModel diff --git a/api/src/Vote.Monitor.Domain/DomainInstaller.cs b/api/src/Vote.Monitor.Domain/DomainInstaller.cs index f5751dd1c..c86dabb42 100644 --- a/api/src/Vote.Monitor.Domain/DomainInstaller.cs +++ b/api/src/Vote.Monitor.Domain/DomainInstaller.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Npgsql; diff --git a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportFormSubmissionsFilters.cs b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportFormSubmissionsFilters.cs index 78425b217..f9f5f38c1 100644 --- a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportFormSubmissionsFilters.cs +++ b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportFormSubmissionsFilters.cs @@ -32,4 +32,5 @@ public class ExportFormSubmissionsFilters public DateTime? FromDateFilter { get; set; } public DateTime? ToDateFilter { get; set; } + public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportIncidentReportsFilters.cs b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportIncidentReportsFilters.cs index 3ea249ac8..95cbc905a 100644 --- a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportIncidentReportsFilters.cs +++ b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportIncidentReportsFilters.cs @@ -31,4 +31,5 @@ public class ExportIncidentReportsFilters public DateTime? FromDateFilter { get; set; } public DateTime? ToDateFilter { get; set; } + public bool? IsCompletedFilter { get; set; } } \ No newline at end of file diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsJob.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsJob.cs index fa3c01414..3a899b02d 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsJob.cs @@ -134,7 +134,8 @@ WITH submissions AS psi."NumberOfFlaggedAnswers", '[]'::jsonb AS "Attachments", '[]'::jsonb AS "Notes", - COALESCE(psi."LastModifiedOn", psi."CreatedOn") "TimeSubmitted" + COALESCE(psi."LastModifiedOn", psi."CreatedOn") "TimeSubmitted", + psi."IsCompleted" FROM "PollingStationInformation" psi INNER JOIN "MonitoringObservers" mo ON mo."Id" = psi."MonitoringObserverId" INNER JOIN "MonitoringNgos" mn ON mn."Id" = mo."MonitoringNgoId" @@ -145,10 +146,12 @@ WITH submissions AS AND (@formId IS NULL OR psi."PollingStationInformationFormId" = @formId) AND (@fromDate is NULL OR COALESCE(PSI."LastModifiedOn", PSI."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(PSI."LastModifiedOn", PSI."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR psi."IsCompleted" = @isCompleted) AND (@questionsAnswered IS NULL OR (@questionsAnswered = 'All' AND psif."NumberOfQuestions" = psi."NumberOfQuestionsAnswered") OR (@questionsAnswered = 'Some' AND psif."NumberOfQuestions" <> psi."NumberOfQuestionsAnswered") OR (@questionsAnswered = 'None' AND psi."NumberOfQuestionsAnswered" = 0)) + UNION ALL SELECT fs."Id" AS "SubmissionId", f."Id" AS "FormId", @@ -175,7 +178,7 @@ UNION ALL AND fs."PollingStationId" = n."PollingStationId"), '[]'::JSONB) AS "Notes", COALESCE(fs."LastModifiedOn", fs."CreatedOn") "TimeSubmitted" - + FS."IsCompleted" FROM "FormSubmissions" fs INNER JOIN "MonitoringObservers" mo ON fs."MonitoringObserverId" = mo."Id" INNER JOIN "MonitoringNgos" mn ON mn."Id" = mo."MonitoringNgoId" @@ -186,6 +189,7 @@ UNION ALL AND (@formId IS NULL OR fs."FormId" = @formId) AND (@fromDate is NULL OR COALESCE(FS."LastModifiedOn", FS."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(FS."LastModifiedOn", FS."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR FS."IsCompleted" = @isCompleted) AND (@questionsAnswered IS NULL OR (@questionsAnswered = 'All' AND f."NumberOfQuestions" = fs."NumberOfQuestionsAnswered") OR (@questionsAnswered = 'Some' AND f."NumberOfQuestions" <> fs."NumberOfQuestionsAnswered") @@ -211,7 +215,8 @@ UNION ALL s."Notes", s."Answers", s."Questions", - s."FollowUpStatus" + s."FollowUpStatus", + s."IsCompleted" FROM submissions s INNER JOIN "PollingStations" ps on ps."Id" = s."PollingStationId" INNER JOIN "MonitoringObservers" mo on mo."Id" = s."MonitoringObserverId" @@ -267,6 +272,7 @@ OR u."Email" ILIKE @searchText questionsAnswered = filters?.QuestionsAnswered?.ToString(), fromDate = filters?.FromDateFilter?.ToString("O"), toDate = filters?.ToDateFilter?.ToString("O"), + isCompleted = filters?.IsCompletedFilter }; IEnumerable submissions = []; diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ReadModels/SubmissionModel.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ReadModels/SubmissionModel.cs index 1bbe0dd10..199cbfb77 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ReadModels/SubmissionModel.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ReadModels/SubmissionModel.cs @@ -25,6 +25,7 @@ public class SubmissionModel public string LastName { get; init; } = default!; public string Email { get; init; } = default!; public string PhoneNumber { get; init; } = default!; + public bool IsCompleted { get; init; } = default!; public BaseAnswer[] Answers { get; init; } public SubmissionNoteModel[] Notes { get; init; } public SubmissionAttachmentModel[] Attachments { get; init; } diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ExportIncidentReportsJob.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ExportIncidentReportsJob.cs index 8e6a62a5e..f1ff33713 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ExportIncidentReportsJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ExportIncidentReportsJob.cs @@ -37,7 +37,8 @@ public async Task Run(Guid electionRoundId, Guid ngoId, Guid exportedDataId, Can try { - if (exportedData.ExportStatus == ExportedDataStatus.Completed || exportedData.ExportStatus == ExportedDataStatus.Failed) + if (exportedData.ExportStatus == ExportedDataStatus.Completed || + exportedData.ExportStatus == ExportedDataStatus.Failed) { logger.LogWarning("ExportData was completed or failed for {electionRoundId} {ngoId} {exportedDataId}", electionRoundId, ngoId, exportedDataId); @@ -119,6 +120,7 @@ INCIDENT_REPORTS AS ( IR."NumberOfQuestionsAnswered", IR."NumberOfFlaggedAnswers", IR."Answers", + IR."IsCompleted", COALESCE( ( SELECT @@ -176,6 +178,7 @@ INCIDENT_REPORTS AS ( ) AND (@fromDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") >= @fromDate::timestamp) AND (@toDate is NULL OR COALESCE(IR."LastModifiedOn", IR."CreatedOn") <= @toDate::timestamp) + AND (@isCompleted is NULL OR IR."IsCompleted" = @isCompleted) ) SELECT IR."IncidentReportId", @@ -202,7 +205,8 @@ INCIDENT_REPORTS AS ( IR."Attachments", IR."Notes", IR."FollowUpStatus", - IR."NumberOfFlaggedAnswers" + IR."NumberOfFlaggedAnswers", + IR."IsCompleted" FROM INCIDENT_REPORTS IR INNER JOIN "MonitoringObservers" MO ON MO."Id" = IR."MonitoringObserverId" @@ -266,8 +270,9 @@ OR U."PhoneNumber" ILIKE @searchText questionsAnswered = filters?.QuestionsAnswered?.ToString(), fromDate = filters?.FromDateFilter?.ToString("O"), toDate = filters?.ToDateFilter?.ToString("O"), + isCompleted = filters?.IsCompletedFilter }; - + IEnumerable incidentReports; using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct)) { diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ReadModels/IncidentReportModel.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ReadModels/IncidentReportModel.cs index b28008388..9cba03071 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ReadModels/IncidentReportModel.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ReadModels/IncidentReportModel.cs @@ -31,6 +31,7 @@ public class IncidentReportModel public string LastName { get; set; } = default!; public string Email { get; set; } = default!; public string PhoneNumber { get; set; } = default!; + public bool IsCompleted { get; set; } = default!; public BaseAnswer[] Answers { get; set; } = []; public SubmissionNoteModel[] Notes { get; set; } = []; public SubmissionAttachmentModel[] Attachments { get; set; } = []; diff --git a/api/src/Vote.Monitor.Hangfire/appsettings.json b/api/src/Vote.Monitor.Hangfire/appsettings.json index 24c4ce632..26949ccab 100644 --- a/api/src/Vote.Monitor.Hangfire/appsettings.json +++ b/api/src/Vote.Monitor.Hangfire/appsettings.json @@ -59,7 +59,7 @@ }, "S3": { "BucketName": "", - "PresignedUrlValidityInSeconds": 3600, + "PresignedUrlValidityInSeconds": 432000, "AWSRegion": "eu-central-1", "AWSAccessKey": "youraccesskey", "AWSSecretKey": "yoursecretkey" diff --git a/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminAuthorizationHandlerTests.cs b/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminAuthorizationHandlerTests.cs index 63b57dee7..461e9d159 100644 --- a/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminAuthorizationHandlerTests.cs +++ b/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminAuthorizationHandlerTests.cs @@ -55,26 +55,6 @@ public async Task HandleRequirementAsync_MonitoringNgoNotFound_Failure() _context.HasSucceeded.Should().BeFalse(); } - [Fact] - public async Task HandleRequirementAsync_ElectionRoundArchived_Failure() - { - // Arrange - _currentUserRoleProvider.IsNgoAdmin().Returns(true); - _currentUserProvider.GetNgoId().Returns(_ngoId); - - _monitoringNgoRepository - .FirstOrDefaultAsync(Arg.Any()) - .Returns(CreateMonitoringNgoView.With().ArchivedElectionRound()); - - var handler = new MonitoringNgoAdminAuthorizationHandler(_currentUserProvider, _currentUserRoleProvider, _monitoringNgoRepository); - - // Act - await handler.HandleAsync(_context); - - // Assert - _context.HasSucceeded.Should().BeFalse(); - } - [Fact] public async Task HandleRequirementAsync_NgoIsDeactivated_Failure() { diff --git a/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminOrObserverAuthorizationHandlerTests.cs b/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminOrObserverAuthorizationHandlerTests.cs index bedde1d60..e0ba11fb6 100644 --- a/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminOrObserverAuthorizationHandlerTests.cs +++ b/api/tests/Authorization.Policies.UnitTests/RequirementsHandlers/MonitoringNgoAdminOrObserverAuthorizationHandlerTests.cs @@ -161,25 +161,6 @@ public async Task HandleRequirementAsync_MonitoringObserverNotFound_Failure() _context.HasSucceeded.Should().BeFalse(); } - [Fact] - public async Task HandleRequirementAsync_ElectionRoundIsArchived_Failure() - { - // Arrange - _currentUserRoleProvider.IsObserver().Returns(true); - _currentUserRoleProvider.IsNgoAdmin().Returns(false); - _currentUserProvider.GetUserId().Returns(_observerId); - - _monitoringObserverRepository - .FirstOrDefaultAsync(Arg.Any()) - .Returns(CreateMonitoringObserverView.With().ArchivedElectionRound()); - - // Act - await _handler.HandleAsync(_context); - - // Assert - _context.HasSucceeded.Should().BeFalse(); - } - [Fact] public async Task HandleRequirementAsync_ObserverNgoIsDeactivated_Failure() { diff --git a/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/ListFormsOverviewRequestValidatorTests.cs b/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/CitizenReportsAggregateFilterValidatorTests.cs similarity index 67% rename from api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/ListFormsOverviewRequestValidatorTests.cs rename to api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/CitizenReportsAggregateFilterValidatorTests.cs index f87ef433d..91673f4e5 100644 --- a/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/ListFormsOverviewRequestValidatorTests.cs +++ b/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/CitizenReportsAggregateFilterValidatorTests.cs @@ -1,14 +1,17 @@ +using Feature.CitizenReports.Requests; +using Feature.CitizenReports.Validators; + namespace Feature.CitizenReports.UnitTests.ValidatorTests; -public class ListFormsOverviewRequestValidatorTests +public class CitizenReportsAggregateFilterValidatorTests { - private readonly ListFormsOverview.Validator _validator = new(); - + private readonly CitizenReportsAggregateFilterValidator _validator = new(); + [Fact] public void Validation_ShouldFail_When_NgoId_Empty() { // Arrange - var request = new ListFormsOverview.Request { NgoId = Guid.Empty }; + var request = new CitizenReportsAggregateFilter { NgoId = Guid.Empty }; // Act var result = _validator.TestValidate(request); @@ -21,7 +24,7 @@ public void Validation_ShouldFail_When_NgoId_Empty() public void Validation_ShouldFail_When_ElectionRoundId_Empty() { // Arrange - var request = new ListFormsOverview.Request { ElectionRoundId = Guid.Empty }; + var request = new CitizenReportsAggregateFilter { ElectionRoundId = Guid.Empty }; // Act var result = _validator.TestValidate(request); @@ -34,7 +37,7 @@ public void Validation_ShouldFail_When_ElectionRoundId_Empty() public void Validation_ShouldPass_When_ValidRequest() { // Arrange - var request = new ListFormsOverview.Request + var request = new CitizenReportsAggregateFilter { ElectionRoundId = Guid.NewGuid(), NgoId = Guid.NewGuid() diff --git a/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/GetSubmissionsAggregatedRequestValidatorTests.cs b/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/GetSubmissionsAggregatedRequestValidatorTests.cs deleted file mode 100644 index ac65f3808..000000000 --- a/api/tests/Feature.CitizenReports.UnitTests/ValidatorTests/GetSubmissionsAggregatedRequestValidatorTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Feature.CitizenReports.UnitTests.ValidatorTests; - -public class GetSubmissionsAggregatedRequestValidatorTests -{ - private readonly GetSubmissionsAggregated.Validator _validator = new(); - - [Fact] - public void Validation_ShouldFail_When_NgoId_Empty() - { - // Arrange - var request = new GetSubmissionsAggregated.Request { NgoId = Guid.Empty }; - - // Act - var result = _validator.TestValidate(request); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.NgoId); - } - - [Fact] - public void Validation_ShouldFail_When_ElectionRoundId_Empty() - { - // Arrange - var request = new GetSubmissionsAggregated.Request { ElectionRoundId = Guid.Empty }; - - // Act - var result = _validator.TestValidate(request); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.ElectionRoundId); - } - - [Fact] - public void Validation_ShouldFail_When_FormId_Empty() - { - // Arrange - var request = new GetSubmissionsAggregated.Request { FormId = Guid.Empty }; - - // Act - var result = _validator.TestValidate(request); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.FormId); - } - - [Fact] - public void Validation_ShouldPass_When_ValidRequest() - { - // Arrange - var request = new GetSubmissionsAggregated.Request - { - ElectionRoundId = Guid.NewGuid(), - NgoId = Guid.NewGuid(), - FormId = Guid.NewGuid() - }; - - // Act - var result = _validator.TestValidate(request); - - // Assert - result.ShouldNotHaveAnyValidationErrors(); - } -} diff --git a/api/tests/Feature.Form.Submissions.UnitTests/ValidatorTests/GetSubmissionsAggregatedValidatorTests.cs b/api/tests/Feature.Form.Submissions.UnitTests/ValidatorTests/FormSubmissionsAggregateFilterValidatorTests.cs similarity index 63% rename from api/tests/Feature.Form.Submissions.UnitTests/ValidatorTests/GetSubmissionsAggregatedValidatorTests.cs rename to api/tests/Feature.Form.Submissions.UnitTests/ValidatorTests/FormSubmissionsAggregateFilterValidatorTests.cs index 95502b21f..f3ca85322 100644 --- a/api/tests/Feature.Form.Submissions.UnitTests/ValidatorTests/GetSubmissionsAggregatedValidatorTests.cs +++ b/api/tests/Feature.Form.Submissions.UnitTests/ValidatorTests/FormSubmissionsAggregateFilterValidatorTests.cs @@ -1,14 +1,17 @@ +using Feature.Form.Submissions.Requests; +using Feature.Form.Submissions.Validators; + namespace Feature.Form.Submissions.UnitTests.ValidatorTests; -public class GetSubmissionsAggregatedValidatorTests +public class FormSubmissionsAggregateFilterValidatorTests { - private readonly GetAggregated.Validator _validator = new(); + private readonly FormSubmissionsAggregateFilterValidator _validator = new(); [Fact] public void Should_Have_Error_When_ElectionRoundId_Is_Empty() { // Arrange - var request = new GetAggregated.Request + var request = new FormSubmissionsAggregateFilter { ElectionRoundId = Guid.Empty }; @@ -24,7 +27,7 @@ public void Should_Have_Error_When_ElectionRoundId_Is_Empty() public void Should_Have_Error_When_NgoId_Is_Empty() { // Arrange - var request = new GetAggregated.Request + var request = new FormSubmissionsAggregateFilter { NgoId = Guid.Empty, }; @@ -36,27 +39,11 @@ public void Should_Have_Error_When_NgoId_Is_Empty() result.ShouldHaveValidationErrorFor(x => x.NgoId); } - [Fact] - public void Should_Have_Error_When_FormId_Is_Empty() - { - // Arrange - var request = new GetAggregated.Request - { - FormId = Guid.Empty - }; - - // Act - var result = _validator.TestValidate(request); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.FormId); - } - [Fact] public void Should_Not_Have_Error_When_All_Fields_Are_Valid() { // Arrange - var request = new GetAggregated.Request + var request = new FormSubmissionsAggregateFilter { ElectionRoundId = Guid.NewGuid(), NgoId = Guid.NewGuid(), diff --git a/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/GetSubmissionsAggregatedValidatorTests.cs b/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/IncidentReportsAggregateFilterValidatorTests.cs similarity index 65% rename from api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/GetSubmissionsAggregatedValidatorTests.cs rename to api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/IncidentReportsAggregateFilterValidatorTests.cs index d2783f9db..e34b517a2 100644 --- a/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/GetSubmissionsAggregatedValidatorTests.cs +++ b/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/IncidentReportsAggregateFilterValidatorTests.cs @@ -1,18 +1,19 @@ -using Feature.IncidentReports.GetSubmissionsAggregated; +using Feature.IncidentReports.Requests; +using Feature.IncidentReports.Validators; using FluentValidation.TestHelper; using Xunit; namespace Feature.IncidentReports.UnitTests.ValidatorTests; -public class GetSubmissionsAggregatedValidatorTests +public class IncidentReportsAggregateFilterValidatorTests { - private readonly Validator _validator = new(); + private readonly IncidentReportsAggregateFilterValidator _validator = new(); [Fact] public void Should_Have_Error_When_ElectionRoundId_Is_Empty() { // Arrange - var request = new Request + var request = new IncidentReportsAggregateFilter { ElectionRoundId = Guid.Empty }; @@ -28,7 +29,7 @@ public void Should_Have_Error_When_ElectionRoundId_Is_Empty() public void Should_Have_Error_When_NgoId_Is_Empty() { // Arrange - var request = new Request + var request = new IncidentReportsAggregateFilter { NgoId = Guid.Empty, }; @@ -40,27 +41,11 @@ public void Should_Have_Error_When_NgoId_Is_Empty() result.ShouldHaveValidationErrorFor(x => x.NgoId); } - [Fact] - public void Should_Have_Error_When_FormId_Is_Empty() - { - // Arrange - var request = new Request - { - FormId = Guid.Empty - }; - - // Act - var result = _validator.TestValidate(request); - - // Assert - result.ShouldHaveValidationErrorFor(x => x.FormId); - } - [Fact] public void Should_Not_Have_Error_When_All_Fields_Are_Valid() { // Arrange - var request = new Request + var request = new IncidentReportsAggregateFilter { ElectionRoundId = Guid.NewGuid(), NgoId = Guid.NewGuid(), diff --git a/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/ListFormsOverviewValidatorTests.cs b/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/ListFormsOverviewValidatorTests.cs index fa5e251d7..2b4a8e04b 100644 --- a/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/ListFormsOverviewValidatorTests.cs +++ b/api/tests/Feature.IncidentReports.UnitTests/ValidatorTests/ListFormsOverviewValidatorTests.cs @@ -1,4 +1,5 @@ -using Feature.IncidentReports.ListFormsOverview; +using Feature.IncidentReports.Requests; +using Feature.IncidentReports.Validators; using FluentValidation.TestHelper; using Xunit; @@ -6,13 +7,13 @@ namespace Feature.IncidentReports.UnitTests.ValidatorTests; public class ListFormsOverviewValidatorTests { - private readonly Validator _validator = new(); + private readonly IncidentReportsAggregateFilterValidator _validator = new(); [Fact] public void Should_Have_Error_When_ElectionRoundId_Is_Empty() { // Arrange - var request = new Request + var request = new IncidentReportsAggregateFilter { ElectionRoundId = Guid.Empty }; @@ -28,7 +29,7 @@ public void Should_Have_Error_When_ElectionRoundId_Is_Empty() public void Should_Have_Error_When_NgoId_Is_Empty() { // Arrange - var request = new Request + var request = new IncidentReportsAggregateFilter { NgoId = Guid.Empty, }; @@ -44,7 +45,7 @@ public void Should_Have_Error_When_NgoId_Is_Empty() public void Should_Not_Have_Error_When_All_Fields_Are_Valid() { // Arrange - var request = new Request + var request = new IncidentReportsAggregateFilter { ElectionRoundId = Guid.NewGuid(), NgoId = Guid.NewGuid(), diff --git a/api/tests/Feature.PollingStation.Information.UnitTests/ValidatorTests/UpsertValidatorTests.cs b/api/tests/Feature.PollingStation.Information.UnitTests/ValidatorTests/UpsertValidatorTests.cs index 5afd4a3bc..91075584b 100644 --- a/api/tests/Feature.PollingStation.Information.UnitTests/ValidatorTests/UpsertValidatorTests.cs +++ b/api/tests/Feature.PollingStation.Information.UnitTests/ValidatorTests/UpsertValidatorTests.cs @@ -77,12 +77,12 @@ public void Validation_ShouldFail_When_ObservationBreaks_ContainsInvalid() { // Arrange var request = new Request { Breaks = [ - new Request.BreakRequest() + new Request.BreakRequest { Start = DateTime.UtcNow.AddDays(-1), End = DateTime.UtcNow, }, - new Request.BreakRequest() + new Request.BreakRequest { Start = DateTime.UtcNow, End = DateTime.UtcNow.AddDays(-1), @@ -105,12 +105,12 @@ public void Validation_ShouldPass_When_BreakEndIsNull() ElectionRoundId = Guid.NewGuid(), PollingStationId = Guid.NewGuid(), Breaks = [ - new Request.BreakRequest() + new Request.BreakRequest { Start = DateTime.UtcNow.AddDays(-1), End = DateTime.UtcNow, }, - new Request.BreakRequest() + new Request.BreakRequest { Start = DateTime.UtcNow, End = null diff --git a/api/tests/Vote.Monitor.Api.Feature.ElectionRound.UnitTests/Endpoints/CreateEndpointTests.cs b/api/tests/Vote.Monitor.Api.Feature.ElectionRound.UnitTests/Endpoints/CreateEndpointTests.cs index c1b1990d5..8833ea8a7 100644 --- a/api/tests/Vote.Monitor.Api.Feature.ElectionRound.UnitTests/Endpoints/CreateEndpointTests.cs +++ b/api/tests/Vote.Monitor.Api.Feature.ElectionRound.UnitTests/Endpoints/CreateEndpointTests.cs @@ -1,4 +1,6 @@ -namespace Vote.Monitor.Api.Feature.ElectionRound.UnitTests.Endpoints; +using Vote.Monitor.Domain.Entities.PollingStationInfoFormAggregate; + +namespace Vote.Monitor.Api.Feature.ElectionRound.UnitTests.Endpoints; public class CreateEndpointTests { @@ -11,12 +13,13 @@ public async Task ShouldReturnOkWithElectionRoundModel_WhenNoConflict() var startDate = new DateOnly(2024, 01, 02); var repository = Substitute.For>(); + var psiFormRepository = Substitute.For>(); repository .AnyAsync(Arg.Any()) .Returns(false); - var endpoint = Factory.Create(repository); + var endpoint = Factory.Create(repository, psiFormRepository); // Act var request = new Create.Request @@ -47,15 +50,17 @@ public async Task ShouldReturnConflict_WhenElectionRoundWithSameTitleExists() { // Arrange var repository = Substitute.For>(); + var psiFormRepository = Substitute.For>(); repository .AnyAsync(Arg.Any()) .Returns(true); - var endpoint = Factory.Create(repository); + var endpoint = Factory.Create(repository, psiFormRepository); // Act - var request = new Create.Request { Title = "a title", EnglishTitle = "an english title", StartDate = DateOnly.MinValue }; + var request = new Create.Request + { Title = "a title", EnglishTitle = "an english title", StartDate = DateOnly.MinValue }; var result = await endpoint.ExecuteAsync(request, default); // Assert @@ -64,4 +69,29 @@ public async Task ShouldReturnConflict_WhenElectionRoundWithSameTitleExists() .Which .Result.Should().BeOfType>(); } -} + + + [Fact] + public async Task ShouldCreate_PSIForm() + { + // Arrange + var repository = Substitute.For>(); + var psiFormRepository = Substitute.For>(); + + var endpoint = Factory.Create(repository, psiFormRepository); + + // Act + var request = new Create.Request + { + Title = "a title", + EnglishTitle = "an english title", + StartDate = DateOnly.MinValue, + CountryId = CountriesList.MD.Id + }; + await endpoint.ExecuteAsync(request, default); + + // Assert + await psiFormRepository.Received(1) + .AddAsync(Arg.Is(f => f.Questions.Count == 1)); + } +} \ No newline at end of file diff --git a/api/tests/Vote.Monitor.Core.UnitTests/LanguagesTranslationStatusTests.cs b/api/tests/Vote.Monitor.Core.UnitTests/LanguagesTranslationStatusTests.cs index 2903e2b2b..679369ed2 100644 --- a/api/tests/Vote.Monitor.Core.UnitTests/LanguagesTranslationStatusTests.cs +++ b/api/tests/Vote.Monitor.Core.UnitTests/LanguagesTranslationStatusTests.cs @@ -4,13 +4,13 @@ namespace Vote.Monitor.Core.UnitTests; public class LanguagesTranslationStatusTests { - private readonly LanguagesTranslationStatus _first = new LanguagesTranslationStatus() + private readonly LanguagesTranslationStatus _first = new LanguagesTranslationStatus { ["Ro"] = TranslationStatus.Translated, ["En"] = TranslationStatus.MissingTranslations, }; - private readonly LanguagesTranslationStatus _second = new LanguagesTranslationStatus() + private readonly LanguagesTranslationStatus _second = new LanguagesTranslationStatus { ["Ro"] = TranslationStatus.Translated, ["En"] = TranslationStatus.MissingTranslations, diff --git a/web/src/components/table-tag-list/TableTagList.tsx b/web/src/components/table-tag-list/TableTagList.tsx index 3c7218ef5..d3247efe3 100644 --- a/web/src/components/table-tag-list/TableTagList.tsx +++ b/web/src/components/table-tag-list/TableTagList.tsx @@ -1,16 +1,46 @@ import { getTagColor } from '@/lib/utils'; +import { FC } from 'react'; import { Badge } from '../ui/badge'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; interface TableTagListProps { tags: string[]; } +const TableTag: FC<{ tag: string }> = ({ tag }) => { + return ( + + {tag} + + ); +}; + export default function TableTagList({ tags }: TableTagListProps) { + const firstTag = tags[0] as string; + const remainingTags = tags.slice(1); + + if (tags.length == 0) return; + + if (tags.length == 1) return ; + return ( -
- {tags?.map((tag) => ( - {tag} - ))} -
+ +
+ + + + +{tags.length - 1} + + + {remainingTags.map((tag, idx) => ( + + ))} + + +
+
); } diff --git a/web/src/components/ui/DataTable/DataTable.tsx b/web/src/components/ui/DataTable/DataTable.tsx index 3d53bb04e..e50487b6d 100644 --- a/web/src/components/ui/DataTable/DataTable.tsx +++ b/web/src/components/ui/DataTable/DataTable.tsx @@ -21,7 +21,6 @@ import { DataTablePagination } from './DataTablePagination'; export interface RowData { id: string; - defaultLanguage?: string; } export interface TableCellProps { @@ -118,7 +117,7 @@ export interface DataTableProps void; - onRowClick?: (id: string, defaultLanguage?: string) => void; + onRowClick?: (id: string) => void; getCellProps?: (context: CellContext) => TableCellProps | void; @@ -255,7 +254,7 @@ export function DataTable( data-state={row.getIsSelected() && 'selected'} className={getRowClassName ? getRowClassName(row) : ''} onClick={() => { - onRowClick?.(row.original.id, row.original.defaultLanguage); + onRowClick?.(row.original.id); }} style={{ cursor: onRowClick ? 'pointer' : undefined }}> {row.getVisibleCells().map((cell) => ( diff --git a/web/src/features/filtering/components/ActiveFilters.tsx b/web/src/features/filtering/components/ActiveFilters.tsx index 67bc1427f..13fe5ffb1 100644 --- a/web/src/features/filtering/components/ActiveFilters.tsx +++ b/web/src/features/filtering/components/ActiveFilters.tsx @@ -6,6 +6,14 @@ import { useNavigate } from '@tanstack/react-router'; import { format } from 'date-fns/format'; import { FC, useCallback } from 'react'; import { FILTER_KEY, FILTER_LABEL } from '../filtering-enums'; +import { isNotNilOrWhitespace, toBoolean } from '@/lib/utils'; +import { + mapFormSubmissionFollowUpStatus, + mapIncidentCategory, + mapQuickReportFollowUpStatus, + mapQuickReportLocationType, +} from '@/features/responses/utils/helpers'; +import { QuickReportFollowUpStatus } from '@/common/types'; interface ActiveFilterProps { filterId: string; @@ -47,6 +55,16 @@ const FILTER_LABELS = new Map([ [FILTER_KEY.FromDate, FILTER_LABEL.FromDate], [FILTER_KEY.ToDate, FILTER_LABEL.ToDate], [FILTER_KEY.SearchText, FILTER_LABEL.SearchText], + [FILTER_KEY.FormIsCompleted, FILTER_LABEL.FormCompleted], + [FILTER_KEY.QuickReportIncidentCategory, FILTER_LABEL.QuickReportIncidentCategory], + [FILTER_KEY.QuickReportFollowUpStatus, FILTER_LABEL.QuickReportFollowUpStatus], + [FILTER_KEY.HasQuickReports, FILTER_LABEL.HasQuickReports], +]); + +const FILTER_VALUE_LOCALIZATORS = new Map string>([ + [FILTER_KEY.QuickReportFollowUpStatus, mapQuickReportFollowUpStatus], + [FILTER_KEY.FormSubmissionFollowUpStatus, mapFormSubmissionFollowUpStatus], + [FILTER_KEY.QuickReportIncidentCategory, mapIncidentCategory], ]); const ActiveFilter: FC = ({ filterId, value, isArray }) => { @@ -71,13 +89,21 @@ const ActiveFilter: FC = ({ filterId, value, isArray }) => { }; interface ActiveFiltersProps { - queryParams: any; + queryParams: Record; } function isDateType(value: any): boolean { return value instanceof Date && !isNaN(value.getTime()); } +function isBooleanType(value: any): boolean { + const trimmedValue = value.toString().toLowerCase().trim(); + + return trimmedValue === 'true' || trimmedValue === 'false'; +} +function defaultLocalizator(value: any): string { + return (value ?? '').toString(); +} export const ActiveFilters: FC = ({ queryParams }) => { const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); @@ -85,39 +111,54 @@ export const ActiveFilters: FC = ({ queryParams }) => { return (
- {Object.keys(queryParams).map((filterId) => { - let key = ''; - const value = queryParams[filterId]; - const isArray = Array.isArray(value); - const isDate = isDateType(value); - - if (HIDDEN_FILTERS.includes(filterId)) return; - - if (filterId === FILTER_KEY.FormId) { - key = `active-filter-${filterId}`; - const form = formSubmissionsFilters?.formFilterOptions.find((f) => f.formId === value); + {Object.entries(queryParams) + .filter(([key, value]) => !!value) + .filter(([key, value]) => isNotNilOrWhitespace(value?.toString())) + .map(([filterId, value]) => { + let key = ''; + const isArray = Array.isArray(value); + const isDate = isDateType(value); + const isBoolean = isBooleanType(value); + const localizator = FILTER_VALUE_LOCALIZATORS.get(filterId) ?? defaultLocalizator; + + if (HIDDEN_FILTERS.includes(filterId)) return; + + if (filterId === FILTER_KEY.FormId) { + key = `active-filter-${filterId}`; + const form = formSubmissionsFilters?.formFilterOptions.find((f) => f.formId === value); + + if (form) { + return ; + } + } - if (form) { - return ; + if (!isArray && !isDate && !isBoolean) { + key = `active-filter-${filterId}`; + return ; } - } - if (!isArray && !isDate) { - key = `active-filter-${filterId}`; - return ; - } + if (isBoolean) { + key = `active-filter-${filterId}`; + return ( + + ); + } - if (isDate) { - key = `active-filter-${filterId}`; - return ; - } + if (isDate) { + key = `active-filter-${filterId}`; + return ; + } - return value.map((item: any) => { - key = `active-filter-${filterId}-${item}`; + return (value as unknown[]).map((item: any) => { + key = `active-filter-${filterId}-${item}`; - return ; - }); - })} + return ; + }); + })}
); }; diff --git a/web/src/features/forms/components/filtering/FormStatusSelect.tsx b/web/src/features/filtering/components/FormStatusFilter.tsx similarity index 91% rename from web/src/features/forms/components/filtering/FormStatusSelect.tsx rename to web/src/features/filtering/components/FormStatusFilter.tsx index cf619a2cb..1780a954c 100644 --- a/web/src/features/forms/components/filtering/FormStatusSelect.tsx +++ b/web/src/features/filtering/components/FormStatusFilter.tsx @@ -3,9 +3,9 @@ import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { mapFormStatus } from '@/lib/utils'; import { FC } from 'react'; -import { FormStatus } from '../../models/form'; +import { FormStatus } from '../../forms/models/form'; -export const FormStatusSelect: FC = () => { +export const FormStatusFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { diff --git a/web/src/features/filtering/components/FormSubmissionsCompletionFilter.tsx b/web/src/features/filtering/components/FormSubmissionsCompletionFilter.tsx new file mode 100644 index 000000000..03fe81068 --- /dev/null +++ b/web/src/features/filtering/components/FormSubmissionsCompletionFilter.tsx @@ -0,0 +1,20 @@ +import { BinarySelectFilter } from '@/features/filtering/components/SelectFilter'; +import { FILTER_KEY } from '@/features/filtering/filtering-enums'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { FC } from 'react'; + +export const FormSubmissionsCompletionFilter: FC = () => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onChange = (value: string) => { + navigateHandler({ [FILTER_KEY.FormIsCompleted]: value }); + }; + + return ( + + ); +}; diff --git a/web/src/features/responses/filtering/FormSubmissionsFlaggedAnswersSelect.tsx b/web/src/features/filtering/components/FormSubmissionsFlaggedAnswersFilter.tsx similarity index 90% rename from web/src/features/responses/filtering/FormSubmissionsFlaggedAnswersSelect.tsx rename to web/src/features/filtering/components/FormSubmissionsFlaggedAnswersFilter.tsx index 4007c4e36..85032edc6 100644 --- a/web/src/features/responses/filtering/FormSubmissionsFlaggedAnswersSelect.tsx +++ b/web/src/features/filtering/components/FormSubmissionsFlaggedAnswersFilter.tsx @@ -3,7 +3,7 @@ import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { FC } from 'react'; -export const FormSubmissionsFlaggedAnswersSelect: FC = () => { +export const FormSubmissionsFlaggedAnswersFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { diff --git a/web/src/features/responses/filtering/FormSubmissionsFollowUpSelect.tsx b/web/src/features/filtering/components/FormSubmissionsFollowUpFilter.tsx similarity index 76% rename from web/src/features/responses/filtering/FormSubmissionsFollowUpSelect.tsx rename to web/src/features/filtering/components/FormSubmissionsFollowUpFilter.tsx index 76473bbd5..b66d4ea25 100644 --- a/web/src/features/responses/filtering/FormSubmissionsFollowUpSelect.tsx +++ b/web/src/features/filtering/components/FormSubmissionsFollowUpFilter.tsx @@ -3,9 +3,13 @@ import { SelectFilter, SelectFilterOption } from '@/features/filtering/component import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { FC } from 'react'; -import { mapFormSubmissionFollowUpStatus } from '../utils/helpers'; +import { mapFormSubmissionFollowUpStatus } from '../../responses/utils/helpers'; -export const FormSubmissionsFollowUpSelect: FC = () => { +interface FormSubmissionsFollowUpFilterProps { + placeholder?: string; +} + +export const FormSubmissionsFollowUpFilter: FC = ({ placeholder = 'Follow-up status' }) => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { @@ -14,16 +18,16 @@ export const FormSubmissionsFollowUpSelect: FC = () => { const options: SelectFilterOption[] = [ { value: FormSubmissionFollowUpStatus.NotApplicable, - label: mapFormSubmissionFollowUpStatus(FormSubmissionFollowUpStatus.NotApplicable) + label: mapFormSubmissionFollowUpStatus(FormSubmissionFollowUpStatus.NotApplicable), }, { value: FormSubmissionFollowUpStatus.NeedsFollowUp, - label: mapFormSubmissionFollowUpStatus(FormSubmissionFollowUpStatus.NeedsFollowUp) + label: mapFormSubmissionFollowUpStatus(FormSubmissionFollowUpStatus.NeedsFollowUp), }, { value: FormSubmissionFollowUpStatus.Resolved, - label: mapFormSubmissionFollowUpStatus(FormSubmissionFollowUpStatus.Resolved) + label: mapFormSubmissionFollowUpStatus(FormSubmissionFollowUpStatus.Resolved), }, ]; diff --git a/web/src/features/responses/filtering/FormSubmissionsFormSelect.tsx b/web/src/features/filtering/components/FormSubmissionsFormFilter.tsx similarity index 87% rename from web/src/features/responses/filtering/FormSubmissionsFormSelect.tsx rename to web/src/features/filtering/components/FormSubmissionsFormFilter.tsx index c8f23c4af..7cc715a4e 100644 --- a/web/src/features/responses/filtering/FormSubmissionsFormSelect.tsx +++ b/web/src/features/filtering/components/FormSubmissionsFormFilter.tsx @@ -3,9 +3,9 @@ import { SelectFilter } from '@/features/filtering/components/SelectFilter'; import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { FC, useMemo } from 'react'; -import { useFormSubmissionsFilters } from '../hooks/form-submissions-queries'; +import { useFormSubmissionsFilters } from '../../responses/hooks/form-submissions-queries'; -export const FormSubmissionsFormSelect: FC = () => { +export const FormSubmissionsFormFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data } = useFormSubmissionsFilters(currentElectionRoundId); diff --git a/web/src/features/responses/filtering/FormSubmissionsFromDateFilter.tsx b/web/src/features/filtering/components/FormSubmissionsFromDateFilter.tsx similarity index 100% rename from web/src/features/responses/filtering/FormSubmissionsFromDateFilter.tsx rename to web/src/features/filtering/components/FormSubmissionsFromDateFilter.tsx diff --git a/web/src/features/responses/filtering/FormSubmissionsMediaFilesSelect.tsx b/web/src/features/filtering/components/FormSubmissionsMediaFilesFilter.tsx similarity index 91% rename from web/src/features/responses/filtering/FormSubmissionsMediaFilesSelect.tsx rename to web/src/features/filtering/components/FormSubmissionsMediaFilesFilter.tsx index 7e1730429..178f39722 100644 --- a/web/src/features/responses/filtering/FormSubmissionsMediaFilesSelect.tsx +++ b/web/src/features/filtering/components/FormSubmissionsMediaFilesFilter.tsx @@ -3,7 +3,7 @@ import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { FC } from 'react'; -export const FormSubmissionsMediaFilesSelect: FC = () => { +export const FormSubmissionsMediaFilesFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { diff --git a/web/src/features/responses/filtering/FormSubmissionsQuestionNotesSelect.tsx b/web/src/features/filtering/components/FormSubmissionsQuestionNotesFilter.tsx similarity index 90% rename from web/src/features/responses/filtering/FormSubmissionsQuestionNotesSelect.tsx rename to web/src/features/filtering/components/FormSubmissionsQuestionNotesFilter.tsx index 1a29455a6..64276ca9b 100644 --- a/web/src/features/responses/filtering/FormSubmissionsQuestionNotesSelect.tsx +++ b/web/src/features/filtering/components/FormSubmissionsQuestionNotesFilter.tsx @@ -3,7 +3,7 @@ import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { FC } from 'react'; -export const FormSubmissionsQuestionNotesSelect: FC = () => { +export const FormSubmissionsQuestionNotesFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { diff --git a/web/src/features/responses/filtering/FormSubmissionsQuestionsAnsweredSelect.tsx b/web/src/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter.tsx similarity index 94% rename from web/src/features/responses/filtering/FormSubmissionsQuestionsAnsweredSelect.tsx rename to web/src/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter.tsx index e95857ab6..38a8aa1ae 100644 --- a/web/src/features/responses/filtering/FormSubmissionsQuestionsAnsweredSelect.tsx +++ b/web/src/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter.tsx @@ -4,7 +4,7 @@ import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { FC } from 'react'; -export const FormSubmissionsQuestionsAnsweredSelect: FC = () => { +export const FormSubmissionsQuestionsAnsweredFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { diff --git a/web/src/features/responses/filtering/FormSubmissionsToDateFilter.tsx b/web/src/features/filtering/components/FormSubmissionsToDateFilter.tsx similarity index 100% rename from web/src/features/responses/filtering/FormSubmissionsToDateFilter.tsx rename to web/src/features/filtering/components/FormSubmissionsToDateFilter.tsx diff --git a/web/src/features/forms/components/filtering/FormTypeSelect.tsx b/web/src/features/filtering/components/FormTypeFilter.tsx similarity index 97% rename from web/src/features/forms/components/filtering/FormTypeSelect.tsx rename to web/src/features/filtering/components/FormTypeFilter.tsx index 307de0f02..d3e8d49a6 100644 --- a/web/src/features/forms/components/filtering/FormTypeSelect.tsx +++ b/web/src/features/filtering/components/FormTypeFilter.tsx @@ -6,7 +6,7 @@ import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringCo import { mapFormType } from '@/lib/utils'; import { FC, useMemo } from 'react'; -export const FormTypeSelect: FC = () => { +export const FormTypeFilter: FC = () => { const { queryParams, navigateHandler } = useFilteringContainer(); const onChange = (value: string) => { diff --git a/web/src/features/filtering/components/HasQuickReportsFilter.tsx b/web/src/features/filtering/components/HasQuickReportsFilter.tsx new file mode 100644 index 000000000..120b5a8dd --- /dev/null +++ b/web/src/features/filtering/components/HasQuickReportsFilter.tsx @@ -0,0 +1,20 @@ +import { FILTER_KEY } from '@/features/filtering/filtering-enums'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { FC } from 'react'; +import { BinarySelectFilter } from './SelectFilter'; + +export const HasQuickReportsFilter: FC = () => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onChange = (value: string) => { + navigateHandler({ [FILTER_KEY.HasQuickReports]: value }); + }; + + return ( + + ); +}; diff --git a/web/src/features/filtering/components/QuickReportsFollowUpFilter.tsx b/web/src/features/filtering/components/QuickReportsFollowUpFilter.tsx new file mode 100644 index 000000000..b0b69bd41 --- /dev/null +++ b/web/src/features/filtering/components/QuickReportsFollowUpFilter.tsx @@ -0,0 +1,42 @@ +import { QuickReportFollowUpStatus } from '@/common/types'; +import { SelectFilter, SelectFilterOption } from '@/features/filtering/components/SelectFilter'; +import { FILTER_KEY } from '@/features/filtering/filtering-enums'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { FC } from 'react'; +import { mapQuickReportFollowUpStatus } from '../../responses/utils/helpers'; + +interface QuickReportsFollowUpFilterProps { + placeholder?: string; +} + +export const QuickReportsFollowUpFilter: FC = ({ placeholder = 'Follow-up status' }) => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onChange = (value: string) => { + navigateHandler({ [FILTER_KEY.QuickReportFollowUpStatus]: value }); + }; + const options: SelectFilterOption[] = [ + { + value: QuickReportFollowUpStatus.NotApplicable, + label: mapQuickReportFollowUpStatus(QuickReportFollowUpStatus.NotApplicable), + }, + + { + value: QuickReportFollowUpStatus.NeedsFollowUp, + label: mapQuickReportFollowUpStatus(QuickReportFollowUpStatus.NeedsFollowUp), + }, + { + value: QuickReportFollowUpStatus.Resolved, + label: mapQuickReportFollowUpStatus(QuickReportFollowUpStatus.Resolved), + }, + ]; + + return ( + + ); +}; diff --git a/web/src/features/filtering/components/QuickReportsIncidentCategoryFilter.tsx b/web/src/features/filtering/components/QuickReportsIncidentCategoryFilter.tsx new file mode 100644 index 000000000..0c83913aa --- /dev/null +++ b/web/src/features/filtering/components/QuickReportsIncidentCategoryFilter.tsx @@ -0,0 +1,30 @@ +import { SelectFilter } from '@/features/filtering/components/SelectFilter'; +import { FILTER_KEY } from '@/features/filtering/filtering-enums'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { IncidentCategoryList } from '@/features/responses/models/quick-report'; +import { FC, useMemo } from 'react'; +import { mapIncidentCategory } from '../../responses/utils/helpers'; + +export const QuickReportsIncidentCategoryFilter: FC = () => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onChange = (value: string) => { + navigateHandler({ [FILTER_KEY.QuickReportIncidentCategory]: value }); + }; + + const options = useMemo(() => { + return IncidentCategoryList.map((incidentCategory) => ({ + value: incidentCategory, + label: mapIncidentCategory(incidentCategory), + })); + }, [IncidentCategoryList]); + + return ( + + ); +}; diff --git a/web/src/features/filtering/components/SelectFilter.tsx b/web/src/features/filtering/components/SelectFilter.tsx index 9f72bf7b6..a46193caa 100644 --- a/web/src/features/filtering/components/SelectFilter.tsx +++ b/web/src/features/filtering/components/SelectFilter.tsx @@ -20,7 +20,7 @@ export const SelectFilter: FC = (props) => { return (