diff --git a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs index b21043ad0..e8ad153f5 100644 --- a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs +++ b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Endpoint.cs @@ -73,6 +73,7 @@ private async Task, NotFound>> AggregateCitizenReportsAsync EF.Functions.ILike(x.Location.Level4, req.Level4Filter)) .Where(x => string.IsNullOrWhiteSpace(req.Level5Filter) || EF.Functions.ILike(x.Location.Level5, req.Level5Filter)) + .Where(x => req.LocationId == null || x.LocationId == req.LocationId) .Where(x => req.HasFlaggedAnswers == null || (req.HasFlaggedAnswers.Value ? x.NumberOfFlaggedAnswers > 0 : x.NumberOfFlaggedAnswers == 0)) @@ -133,6 +134,7 @@ private async Task, NotFound>> AggregateCitizenReportsAsync Level3Filter = req.Level3Filter, Level4Filter = req.Level4Filter, Level5Filter = req.Level5Filter, + LocationId = req.LocationId, HasFlaggedAnswers = req.HasFlaggedAnswers, QuestionsAnswered = req.QuestionsAnswered, FollowUpStatus = req.FollowUpStatus diff --git a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs index 6f9c1873b..82707e17c 100644 --- a/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs +++ b/api/src/Feature.CitizenReports/GetSubmissionsAggregated/Response.cs @@ -26,6 +26,8 @@ public class SubmissionsFilterModel public string? Level5Filter { get; set; } + public Guid? LocationId { get; set; } + public bool? HasFlaggedAnswers { get; set; } public CitizenReportFollowUpStatus? FollowUpStatus { get; set; } diff --git a/api/src/Feature.CitizenReports/ListEntries/Endpoint.cs b/api/src/Feature.CitizenReports/ListEntries/Endpoint.cs index 26f6f6226..63b8401a9 100644 --- a/api/src/Feature.CitizenReports/ListEntries/Endpoint.cs +++ b/api/src/Feature.CitizenReports/ListEntries/Endpoint.cs @@ -48,7 +48,8 @@ @followUpStatus IS NULL AND (@level2 IS NULL OR L."Level2" = @level2) AND (@level3 IS NULL OR L."Level3" = @level3) AND (@level4 IS NULL OR L."Level4" = @level4) - AND (@level5 IS NULL OR L."Level5" = @level5) + AND (@level5 IS NULL OR L."Level5" = @level5) + AND (@locationId IS NULL OR CR."LocationId" = @locationId) AND (@formId IS NULL OR CR."FormId" = @formId) AND (@questionsAnswered IS NULL OR (@questionsAnswered = 'All' AND F."NumberOfQuestions" = CR."NumberOfQuestionsAnswered") @@ -125,6 +126,7 @@ @followUpStatus IS NULL AND (@level3 IS NULL OR L."Level3" = @level3) AND (@level4 IS NULL OR L."Level4" = @level4) AND (@level5 IS NULL OR L."Level5" = @level5) + AND (@locationId IS NULL OR CR."LocationId" = @locationId) AND (@hasFlaggedAnswers is NULL OR @hasFlaggedAnswers = false OR 1 = 2) AND (@formId IS NULL OR CR."FormId" = @formId) AND (@questionsAnswered IS NULL @@ -225,6 +227,7 @@ OFFSET @offset ROWS level3 = req.Level3Filter, level4 = req.Level4Filter, level5 = req.Level5Filter, + locationId = req.LocationId, formId = req.FormId, hasAttachments = req.HasAttachments, hasNotes = req.HasNotes, diff --git a/api/src/Feature.CitizenReports/ListEntries/Request.cs b/api/src/Feature.CitizenReports/ListEntries/Request.cs index 74c092bfd..0b652e601 100644 --- a/api/src/Feature.CitizenReports/ListEntries/Request.cs +++ b/api/src/Feature.CitizenReports/ListEntries/Request.cs @@ -16,6 +16,7 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public string? Level3Filter { get; set; } [QueryParam] public string? Level4Filter { get; set; } [QueryParam] public string? Level5Filter { get; set; } + [QueryParam] public Guid? LocationId { get; set; } [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public CitizenReportFollowUpStatus? FollowUpStatus { get; set; } [QueryParam] public Guid? FormId { get; set; } diff --git a/api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs b/api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs index b61f30119..58cbeb7a8 100644 --- a/api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs +++ b/api/src/Feature.CitizenReports/Requests/CitizenReportsAggregateFilter.cs @@ -22,6 +22,8 @@ public class CitizenReportsAggregateFilter [QueryParam] public string? Level5Filter { get; set; } + [QueryParam] public Guid? LocationId { get; set; } + [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public CitizenReportFollowUpStatus? FollowUpStatus { get; set; } diff --git a/api/src/Feature.DataExport/Start/CitizenReportsFilters.cs b/api/src/Feature.DataExport/Start/CitizenReportsFilters.cs index 45c0b0518..ffa9ff8f7 100644 --- a/api/src/Feature.DataExport/Start/CitizenReportsFilters.cs +++ b/api/src/Feature.DataExport/Start/CitizenReportsFilters.cs @@ -13,6 +13,7 @@ public record CitizenReportsFilters public string? Level3Filter { get; init; } public string? Level4Filter { get; init; } public string? Level5Filter { get; init; } + public Guid? LocationId { get; init; } public bool? HasFlaggedAnswers { get; init; } public CitizenReportFollowUpStatus? FollowUpStatus { get; init; } public Guid? FormId { get; init; } @@ -34,6 +35,7 @@ public ExportCitizenReportsFilers ToFilter() Level3Filter = Level3Filter, Level4Filter = Level4Filter, Level5Filter = Level5Filter, + LocationId = LocationId, HasFlaggedAnswers = HasFlaggedAnswers, FollowUpStatus = FollowUpStatus, FormId = FormId, diff --git a/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs b/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs index 7762de5bb..05eca7e3a 100644 --- a/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs +++ b/api/src/Feature.DataExport/Start/FormSubmissionsFilters.cs @@ -26,6 +26,8 @@ public record FormSubmissionsFilters public string? PollingStationNumberFilter { get; init; } + public Guid? PollingStationId { get; init; } + public bool? HasFlaggedAnswers { get; init; } public SubmissionFollowUpStatus? FollowUpStatus { get; init; } @@ -57,6 +59,7 @@ public ExportFormSubmissionsFilters ToFilter() Level4Filter = Level4Filter, Level5Filter = Level5Filter, PollingStationNumberFilter = PollingStationNumberFilter, + PollingStationId = PollingStationId, HasFlaggedAnswers = HasFlaggedAnswers, FollowUpStatus = FollowUpStatus, MonitoringObserverId = MonitoringObserverId, diff --git a/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs b/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs index c62eb8afb..8211fdb11 100644 --- a/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs +++ b/api/src/Feature.DataExport/Start/IncidentReportsFilters.cs @@ -24,6 +24,8 @@ public record IncidentReportsFilters public string? PollingStationNumberFilter { get; init; } + public Guid? PollingStationId { get; init; } + public bool? HasFlaggedAnswers { get; init; } public Guid? MonitoringObserverId { get; init; } @@ -54,6 +56,7 @@ public ExportIncidentReportsFilters ToFilter() Level4Filter = Level4Filter, Level5Filter = Level5Filter, PollingStationNumberFilter = PollingStationNumberFilter, + PollingStationId = PollingStationId, HasFlaggedAnswers = HasFlaggedAnswers, MonitoringObserverId = MonitoringObserverId, TagsFilter = TagsFilter, diff --git a/api/src/Feature.DataExport/Start/QuickReportsFilters.cs b/api/src/Feature.DataExport/Start/QuickReportsFilters.cs index cfbe2c9d6..2ca326a6d 100644 --- a/api/src/Feature.DataExport/Start/QuickReportsFilters.cs +++ b/api/src/Feature.DataExport/Start/QuickReportsFilters.cs @@ -22,6 +22,7 @@ public record QuickReportsFilters public DateTime? ToDateFilter { get; init; } public bool? HasAttachments { get; init; } public Guid? MonitoringObserverId { get; init; } + public Guid? PollingStationId { get; init; } public string[]? TagsFilter { get; init; } = []; public ExportQuickReportsFilters ToFilter() @@ -43,6 +44,7 @@ public ExportQuickReportsFilters ToFilter() CoalitionMemberId = CoalitionMemberId, HasAttachments = HasAttachments, MonitoringObserverId = MonitoringObserverId, + PollingStationId = PollingStationId, TagsFilter = TagsFilter }; } diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs index a9346e2bb..134f592b4 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/GetAggregated/Endpoint.cs @@ -345,6 +345,10 @@ @LEVEL5 IS NULL @POLLINGSTATIONNUMBER IS NULL OR PS."Number" = @POLLINGSTATIONNUMBER ) + AND ( + @POLLINGSTATIONID IS NULL + OR PS."Id" = @POLLINGSTATIONID + ) AND ( @HASFLAGGEDANSWERS IS NULL OR ( @@ -400,6 +404,7 @@ @HASATTACHMENTS IS NULL level4 = req.Level4Filter, level5 = req.Level5Filter, pollingStationNumber = req.PollingStationNumberFilter, + pollingStationId = req.PollingStationId, hasFlaggedAnswers = req.HasFlaggedAnswers, followUpStatus = req.FollowUpStatus?.ToString(), tagsFilter = req.TagsFilter ?? [], @@ -474,6 +479,7 @@ @HASATTACHMENTS IS NULL HasFlaggedAnswers = req.HasFlaggedAnswers, MonitoringObserverStatus = req.MonitoringObserverStatus, PollingStationNumberFilter = req.PollingStationNumberFilter, + PollingStationId = req.PollingStationId, DataSource = req.DataSource!, CoalitionMemberId = req.CoalitionMemberId, } @@ -506,6 +512,7 @@ private async Task, NotFound>> AggregatePSIFormSubmissionsA EF.Functions.ILike(x.PollingStation.Level5, req.Level5Filter)) .Where(x => string.IsNullOrWhiteSpace(req.PollingStationNumberFilter) || EF.Functions.ILike(x.PollingStation.Number, req.PollingStationNumberFilter)) + .Where(x => req.PollingStationId == null || x.PollingStationId == req.PollingStationId) .Where(x => req.HasFlaggedAnswers == null || (req.HasFlaggedAnswers.Value ? x.NumberOfFlaggedAnswers > 0 : x.NumberOfFlaggedAnswers == 0)) diff --git a/api/src/Feature.Form.Submissions/GetAggregated/Response.cs b/api/src/Feature.Form.Submissions/GetAggregated/Response.cs index 049f2dee2..76171c5c2 100644 --- a/api/src/Feature.Form.Submissions/GetAggregated/Response.cs +++ b/api/src/Feature.Form.Submissions/GetAggregated/Response.cs @@ -26,6 +26,8 @@ public class SubmissionsFilterModel public string? PollingStationNumberFilter { get; set; } + public Guid? PollingStationId { get; set; } + public bool? HasFlaggedAnswers { get; set; } public SubmissionFollowUpStatus? FollowUpStatus { get; set; } diff --git a/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs b/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs index 36f46c93e..fa56f02b4 100644 --- a/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/ListEntries/Endpoint.cs @@ -48,6 +48,7 @@ OR mo."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR ps."Id" = @pollingStationId) AND (@hasFlaggedAnswers is NULL OR @hasFlaggedAnswers = false OR 1 = 2) AND (@followUpStatus is NULL OR psi."FollowUpStatus" = @followUpStatus) AND (@monitoringObserverStatus IS NULL OR mo."Status" = @monitoringObserverStatus) @@ -83,6 +84,7 @@ OR mo."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR ps."Id" = @pollingStationId) AND (@hasFlaggedAnswers is NULL OR (fs."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = false) OR ("NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = true)) AND (@followUpStatus is NULL OR fs."FollowUpStatus" = @followUpStatus) @@ -271,6 +273,7 @@ UNION ALL 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 (@pollingStationId IS NULL OR ps."Id" = @pollingStationId) AND (@hasFlaggedAnswers IS NULL OR (s."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = false) OR (s."NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = true)) @@ -331,6 +334,7 @@ UNION ALL level4 = req.Level4Filter, level5 = req.Level5Filter, pollingStationNumber = req.PollingStationNumberFilter, + pollingStationId = req.PollingStationId, hasFlaggedAnswers = req.HasFlaggedAnswers, followUpStatus = req.FollowUpStatus?.ToString(), tagsFilter = req.TagsFilter ?? [], diff --git a/api/src/Feature.Form.Submissions/ListEntries/Request.cs b/api/src/Feature.Form.Submissions/ListEntries/Request.cs index 040404943..7dacf6135 100644 --- a/api/src/Feature.Form.Submissions/ListEntries/Request.cs +++ b/api/src/Feature.Form.Submissions/ListEntries/Request.cs @@ -30,6 +30,8 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public string? PollingStationNumberFilter { get; set; } + [QueryParam] public Guid? PollingStationId { get; set; } + [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public SubmissionFollowUpStatus? FollowUpStatus { get; set; } diff --git a/api/src/Feature.Form.Submissions/ListEntriesDetailed/Endpoint.cs b/api/src/Feature.Form.Submissions/ListEntriesDetailed/Endpoint.cs index 50850ad75..b4f388631 100644 --- a/api/src/Feature.Form.Submissions/ListEntriesDetailed/Endpoint.cs +++ b/api/src/Feature.Form.Submissions/ListEntriesDetailed/Endpoint.cs @@ -52,6 +52,7 @@ OR mo."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR ps."Id" = @pollingStationId) AND (@hasFlaggedAnswers is NULL OR @hasFlaggedAnswers = false OR 1 = 2) AND (@followUpStatus is NULL OR psi."FollowUpStatus" = @followUpStatus) AND (@monitoringObserverStatus IS NULL OR mo."Status" = @monitoringObserverStatus) @@ -87,6 +88,7 @@ OR mo."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR ps."Id" = @pollingStationId) AND (@hasFlaggedAnswers is NULL OR (fs."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = false) OR ("NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = true)) AND (@followUpStatus is NULL OR fs."FollowUpStatus" = @followUpStatus) @@ -307,6 +309,7 @@ UNION ALL 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 (@pollingStationId IS NULL OR ps."Id" = @pollingStationId) AND (@hasFlaggedAnswers IS NULL OR (s."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = false) OR (s."NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = true)) @@ -369,6 +372,7 @@ UNION ALL level4 = req.Level4Filter, level5 = req.Level5Filter, pollingStationNumber = req.PollingStationNumberFilter, + pollingStationId = req.PollingStationId, hasFlaggedAnswers = req.HasFlaggedAnswers, followUpStatus = req.FollowUpStatus?.ToString(), tagsFilter = req.TagsFilter ?? [], diff --git a/api/src/Feature.Form.Submissions/ListEntriesDetailed/Request.cs b/api/src/Feature.Form.Submissions/ListEntriesDetailed/Request.cs index 0bc85e49f..1cac59611 100644 --- a/api/src/Feature.Form.Submissions/ListEntriesDetailed/Request.cs +++ b/api/src/Feature.Form.Submissions/ListEntriesDetailed/Request.cs @@ -30,6 +30,8 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public string? PollingStationNumberFilter { get; set; } + [QueryParam] public Guid? PollingStationId { get; set; } + [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public SubmissionFollowUpStatus? FollowUpStatus { get; set; } diff --git a/api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs b/api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs index f631b75fa..5170a31f0 100644 --- a/api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs +++ b/api/src/Feature.Form.Submissions/Requests/FormSubmissionsAggregateFilter.cs @@ -27,6 +27,8 @@ public class FormSubmissionsAggregateFilter [QueryParam] public string? PollingStationNumberFilter { get; set; } + [QueryParam] public Guid? PollingStationId { get; set; } + [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public SubmissionFollowUpStatus? FollowUpStatus { get; set; } diff --git a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs index 11bbded90..dd22266ce 100644 --- a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs +++ b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Endpoint.cs @@ -80,6 +80,7 @@ private async Task, NotFound>> AggregateIncidentReportsAsyn .Where(x => x.PollingStation != null && (string.IsNullOrWhiteSpace(req.PollingStationNumberFilter) || EF.Functions.ILike(x.PollingStation.Number, req.PollingStationNumberFilter))) + .Where(x => req.PollingStationId == null || x.PollingStationId == req.PollingStationId) .Where(x => req.HasFlaggedAnswers == null || (req.HasFlaggedAnswers.Value ? x.NumberOfFlaggedAnswers > 0 : x.NumberOfFlaggedAnswers == 0)) @@ -147,7 +148,8 @@ private async Task, NotFound>> AggregateIncidentReportsAsyn IsCompletedFilter = req.IsCompletedFilter, LocationTypeFilter = req.LocationTypeFilter, FollowUpStatusFilter = req.FollowUpStatusFilter, - PollingStationNumberFilter = req.PollingStationNumberFilter + PollingStationNumberFilter = req.PollingStationNumberFilter, + PollingStationId = req.PollingStationId } }); } diff --git a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs index faeed8898..eb46a1ee5 100644 --- a/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs +++ b/api/src/Feature.IncidentReports/GetSubmissionsAggregated/Response.cs @@ -26,6 +26,7 @@ public class SubmissionsFilterModel public string? Level5Filter { get; set; } public string? PollingStationNumberFilter { get; set; } + public Guid? PollingStationId { get; set; } public bool? HasFlaggedAnswers { get; set; } diff --git a/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs b/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs index 14a9db716..f92260502 100644 --- a/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs +++ b/api/src/Feature.IncidentReports/ListEntries/Endpoint.cs @@ -56,6 +56,7 @@ OR U."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR IR."PollingStationId" = @pollingStationId) AND (@hasFlaggedAnswers IS NULL OR (IR."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = FALSE) OR ("NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = TRUE) @@ -202,6 +203,7 @@ OR U."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR IR."PollingStationId" = @pollingStationId) AND (@hasFlaggedAnswers IS NULL OR (IR."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = FALSE) OR (IR."NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = TRUE) @@ -262,6 +264,7 @@ OFFSET @offset ROWS level4 = req.Level4Filter, level5 = req.Level5Filter, pollingStationNumber = req.PollingStationNumberFilter, + pollingStationId = req.PollingStationId, hasFlaggedAnswers = req.HasFlaggedAnswers, followUpStatus = req.FollowUpStatus?.ToString(), tagsFilter = req.TagsFilter ?? [], diff --git a/api/src/Feature.IncidentReports/ListEntries/Request.cs b/api/src/Feature.IncidentReports/ListEntries/Request.cs index 03a622a43..49521d645 100644 --- a/api/src/Feature.IncidentReports/ListEntries/Request.cs +++ b/api/src/Feature.IncidentReports/ListEntries/Request.cs @@ -25,6 +25,8 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public string? PollingStationNumberFilter { get; set; } + [QueryParam] public Guid? PollingStationId { get; set; } + [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public Guid? MonitoringObserverId { get; set; } diff --git a/api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs b/api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs index 737e200e0..b5c9fffeb 100644 --- a/api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs +++ b/api/src/Feature.IncidentReports/Requests/IncidentReportsAggregateFilter.cs @@ -23,6 +23,8 @@ public class IncidentReportsAggregateFilter [QueryParam] public string? Level5Filter { get; set; } [QueryParam] public string? PollingStationNumberFilter { get; set; } + [QueryParam] public Guid? PollingStationId { get; set; } + [QueryParam] public bool? HasFlaggedAnswers { get; set; } [QueryParam] public IncidentReportFollowUpStatus? FollowUpStatusFilter { get; set; } diff --git a/api/src/Feature.QuickReports/List/Endpoint.cs b/api/src/Feature.QuickReports/List/Endpoint.cs index 2038becdf..7499277bd 100644 --- a/api/src/Feature.QuickReports/List/Endpoint.cs +++ b/api/src/Feature.QuickReports/List/Endpoint.cs @@ -64,6 +64,7 @@ @level4 IS NULL @level5 IS NULL OR PS."Level5" = @level5 ) + AND (@pollingStationId IS NULL OR QR."PollingStationId" = @pollingStationId) AND (@fromDate is NULL OR QR."LastUpdatedAt" >= @fromDate::timestamp) AND (@toDate is NULL OR QR."LastUpdatedAt" <= @toDate::timestamp) AND (@hasAttachments IS NULL @@ -145,6 +146,7 @@ @level4 IS NULL @level5 IS NULL OR PS."Level5" = @level5 ) + AND (@pollingStationId IS NULL OR QR."PollingStationId" = @pollingStationId) AND (@fromDate is NULL OR QR."LastUpdatedAt" >= @fromDate::timestamp) AND (@toDate is NULL OR QR."LastUpdatedAt" <= @toDate::timestamp) AND (@hasAttachments IS NULL @@ -204,6 +206,7 @@ FETCH NEXT toDate = req.ToDateFilter?.ToString("O"), hasAttachments = req.HasAttachments, monitoringObserverId = req.MonitoringObserverId, + pollingStationId = req.PollingStationId, tagsFilter = req.TagsFilter ?? [], sortExpression = GetSortExpression(req.SortColumnName, req.IsAscendingSorting) }; diff --git a/api/src/Feature.QuickReports/List/Request.cs b/api/src/Feature.QuickReports/List/Request.cs index 29707b65e..b3a74097b 100644 --- a/api/src/Feature.QuickReports/List/Request.cs +++ b/api/src/Feature.QuickReports/List/Request.cs @@ -26,5 +26,6 @@ public class Request : BaseSortPaginatedRequest [QueryParam] public Guid? CoalitionMemberId { get; set; } [QueryParam] public bool? HasAttachments { get; set; } [QueryParam] public Guid? MonitoringObserverId { get; set; } + [QueryParam] public Guid? PollingStationId { get; set; } [QueryParam] public string[]? TagsFilter { get; set; } = []; } diff --git a/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Endpoint.cs b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Endpoint.cs new file mode 100644 index 000000000..b2a43f37a --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Endpoint.cs @@ -0,0 +1,291 @@ +using Authorization.Policies; +using Authorization.Policies.Requirements; +using Dapper; +using Feature.Statistics.GetNgoAdminStatistics.Models; +using Feature.Statistics.Options; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using Vote.Monitor.Domain.ConnectionFactory; +using ZiggyCreatures.Caching.Fusion; + +namespace Feature.Statistics.GetMonitoringObserverLevelStatistics; + +public class Endpoint( + IAuthorizationService authorizationService, + INpgsqlConnectionFactory dbConnectionFactory, + IFusionCache cache, + IOptions options) : Endpoint, NotFound>> +{ + private readonly StatisticsFeatureOptions _options = options.Value; + + public override void Configure() + { + Get("/api/election-rounds/{electionRoundId}/monitoring-observers/{monitoringObserverId}:statistics"); + DontAutoTag(); + Options(x => x.WithTags("statistics", "monitoring-observers")); + Summary(s => { s.Summary = "Location-level statistics for a specific monitoring observer"; }); + Policies(PolicyNames.NgoAdminsOnly); + } + + public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + { + var authorizationResult = + await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); + if (!authorizationResult.Succeeded) + { + return TypedResults.NotFound(); + } + + var cacheKey = + $"monitoring-observer-level-stats-{req.ElectionRoundId}-{req.NgoId}-{req.MonitoringObserverId}-{req.DataSource}"; + + var response = await cache.GetOrSetAsync( + cacheKey, + async _ => await GetStatisticsAsync(req, ct), + cacheOptions => cacheOptions.SetDuration(TimeSpan.FromMinutes(_options.CacheDurationInMinutes)), + token: ct); + + return TypedResults.Ok(response); + } + + private async Task GetStatisticsAsync(Request req, CancellationToken ct) + { + const string sql = + """ + WITH + "AvailableObservers" AS ( + SELECT "MonitoringObserverId" + FROM "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, @dataSource) + WHERE "MonitoringObserverId" = @monitoringObserverId + ), + "PollingStationsStatsRaw" AS ( + SELECT + FS."PollingStationId", + 0 AS "NumberOfIncidentReports", + 0 AS "NumberOfQuickReports", + COUNT(1) AS "NumberOfFormSubmissions", + 0::float AS "MinutesMonitoring", + SUM(FS."NumberOfFlaggedAnswers") AS "NumberOfFlaggedAnswers", + SUM(FS."NumberOfQuestionsAnswered") AS "NumberOfQuestionsAnswered" + FROM "FormSubmissions" FS + INNER JOIN "AvailableObservers" MO ON MO."MonitoringObserverId" = FS."MonitoringObserverId" + GROUP BY FS."PollingStationId" + UNION ALL + SELECT + PSI."PollingStationId", + 0 AS "NumberOfIncidentReports", + 0 AS "NumberOfQuickReports", + COUNT(1) AS "NumberOfFormSubmissions", + SUM("ComputeMinutesMonitoring"("ArrivalTime", "DepartureTime", "Breaks")) AS "MinutesMonitoring", + SUM(PSI."NumberOfFlaggedAnswers") AS "NumberOfFlaggedAnswers", + SUM(PSI."NumberOfQuestionsAnswered") AS "NumberOfQuestionsAnswered" + FROM "PollingStationInformation" PSI + INNER JOIN "AvailableObservers" MO ON MO."MonitoringObserverId" = PSI."MonitoringObserverId" + GROUP BY PSI."PollingStationId" + UNION ALL + SELECT + QR."PollingStationId", + 0 AS "NumberOfIncidentReports", + COUNT(1) AS "NumberOfQuickReports", + 0 AS "NumberOfFormSubmissions", + 0::float AS "MinutesMonitoring", + 0 AS "NumberOfFlaggedAnswers", + 0 AS "NumberOfQuestionsAnswered" + FROM "QuickReports" QR + INNER JOIN "AvailableObservers" MO ON MO."MonitoringObserverId" = QR."MonitoringObserverId" + WHERE QR."PollingStationId" IS NOT NULL + GROUP BY QR."PollingStationId" + UNION ALL + SELECT + IR."PollingStationId", + COUNT(1) AS "NumberOfIncidentReports", + 0 AS "NumberOfQuickReports", + 0 AS "NumberOfFormSubmissions", + 0::float AS "MinutesMonitoring", + 0 AS "NumberOfFlaggedAnswers", + 0 AS "NumberOfQuestionsAnswered" + FROM "IncidentReports" IR + INNER JOIN "AvailableObservers" MO ON MO."MonitoringObserverId" = IR."MonitoringObserverId" + WHERE IR."PollingStationId" IS NOT NULL + GROUP BY IR."PollingStationId" + ), + "PollingStationsStats" AS ( + SELECT + "PollingStationId", + SUM("NumberOfIncidentReports") AS "NumberOfIncidentReports", + SUM("NumberOfQuickReports") AS "NumberOfQuickReports", + SUM("NumberOfFormSubmissions") AS "NumberOfFormSubmissions", + SUM("MinutesMonitoring") AS "MinutesMonitoring", + SUM("NumberOfFlaggedAnswers") AS "NumberOfFlaggedAnswers", + SUM("NumberOfQuestionsAnswered") AS "NumberOfQuestionsAnswered" + FROM "PollingStationsStatsRaw" + GROUP BY "PollingStationId" + ), + "ElectionPollingStations" AS ( + SELECT + PS."Id", + NULLIF(PS."Level1", '') AS "Level1", + NULLIF(PS."Level2", '') AS "Level2", + NULLIF(PS."Level3", '') AS "Level3", + NULLIF(PS."Level4", '') AS "Level4", + NULLIF(PS."Level5", '') AS "Level5" + FROM "PollingStations" PS + WHERE PS."ElectionRoundId" = @electionRoundId + AND PS."Level1" != '' + ), + "PollingStationsPerLevel" AS ( + SELECT + CASE + WHEN GROUPING(PS."Level1") = 1 THEN 0 + WHEN GROUPING(PS."Level2") = 1 THEN 1 + WHEN GROUPING(PS."Level3") = 1 THEN 2 + WHEN GROUPING(PS."Level4") = 1 THEN 3 + WHEN GROUPING(PS."Level5") = 1 THEN 4 + ELSE 5 + END AS "Level", + CASE + WHEN GROUPING(PS."Level1") = 1 THEN '/' + ELSE CONCAT_WS(' / ', PS."Level1", PS."Level2", PS."Level3", PS."Level4", PS."Level5") + END AS "Path", + COUNT(PS."Id") AS "NumberOfPollingStations" + FROM "ElectionPollingStations" PS + GROUP BY GROUPING SETS ( + (), + (PS."Level1"), + (PS."Level1", PS."Level2"), + (PS."Level1", PS."Level2", PS."Level3"), + (PS."Level1", PS."Level2", PS."Level3", PS."Level4"), + (PS."Level1", PS."Level2", PS."Level3", PS."Level4", PS."Level5") + ) + HAVING + GROUPING(PS."Level1") = 1 + OR ( + GROUPING(PS."Level2") = 1 + AND PS."Level1" IS NOT NULL + ) + OR ( + GROUPING(PS."Level3") = 1 + AND GROUPING(PS."Level2") = 0 + AND PS."Level2" IS NOT NULL + ) + OR ( + GROUPING(PS."Level4") = 1 + AND GROUPING(PS."Level3") = 0 + AND PS."Level3" IS NOT NULL + ) + OR ( + GROUPING(PS."Level5") = 1 + AND GROUPING(PS."Level4") = 0 + AND PS."Level4" IS NOT NULL + ) + OR ( + GROUPING(PS."Level5") = 0 + AND PS."Level5" IS NOT NULL + ) + ), + "PollingStationLevelsStats" AS ( + SELECT + CASE + WHEN GROUPING(PS."Level1") = 1 THEN 0 + WHEN GROUPING(PS."Level2") = 1 THEN 1 + WHEN GROUPING(PS."Level3") = 1 THEN 2 + WHEN GROUPING(PS."Level4") = 1 THEN 3 + WHEN GROUPING(PS."Level5") = 1 THEN 4 + ELSE 5 + END AS "Level", + CASE + WHEN GROUPING(PS."Level1") = 1 THEN '/' + ELSE CONCAT_WS(' / ', PS."Level1", PS."Level2", PS."Level3", PS."Level4", PS."Level5") + END AS "Path", + COUNT(PSV."PollingStationId") AS "NumberOfVisitedPollingStations", + COALESCE(SUM(PSV."NumberOfIncidentReports"), 0) AS "NumberOfIncidentReports", + COALESCE(SUM(PSV."NumberOfQuickReports"), 0) AS "NumberOfQuickReports", + COALESCE(SUM(PSV."NumberOfFormSubmissions"), 0) AS "NumberOfFormSubmissions", + COALESCE(SUM(PSV."MinutesMonitoring"), 0) AS "MinutesMonitoring", + COALESCE(SUM(PSV."NumberOfFlaggedAnswers"), 0) AS "NumberOfFlaggedAnswers", + COALESCE(SUM(PSV."NumberOfQuestionsAnswered"), 0) AS "NumberOfQuestionsAnswered" + FROM "PollingStationsStats" PSV + INNER JOIN "ElectionPollingStations" PS ON PS."Id" = PSV."PollingStationId" + GROUP BY GROUPING SETS ( + (), + (PS."Level1"), + (PS."Level1", PS."Level2"), + (PS."Level1", PS."Level2", PS."Level3"), + (PS."Level1", PS."Level2", PS."Level3", PS."Level4"), + (PS."Level1", PS."Level2", PS."Level3", PS."Level4", PS."Level5") + ) + HAVING + GROUPING(PS."Level1") = 1 + OR ( + GROUPING(PS."Level2") = 1 + AND PS."Level1" IS NOT NULL + ) + OR ( + GROUPING(PS."Level3") = 1 + AND GROUPING(PS."Level2") = 0 + AND PS."Level2" IS NOT NULL + ) + OR ( + GROUPING(PS."Level4") = 1 + AND GROUPING(PS."Level3") = 0 + AND PS."Level3" IS NOT NULL + ) + OR ( + GROUPING(PS."Level5") = 1 + AND GROUPING(PS."Level4") = 0 + AND PS."Level4" IS NOT NULL + ) + OR ( + GROUPING(PS."Level5") = 0 + AND PS."Level5" IS NOT NULL + ) + ) + SELECT + S."Path", + S."Level", + S."NumberOfVisitedPollingStations", + PS."NumberOfPollingStations", + S."NumberOfIncidentReports", + S."NumberOfQuickReports", + S."NumberOfFormSubmissions", + S."MinutesMonitoring", + S."NumberOfFlaggedAnswers", + S."NumberOfQuestionsAnswered", + CASE WHEN S."NumberOfVisitedPollingStations" > 0 THEN 1 ELSE 0 END AS "ActiveObservers", + ( + S."NumberOfVisitedPollingStations" * 100.0 / NULLIF(PS."NumberOfPollingStations", 0) + ) AS "CoveragePercentage" + FROM "PollingStationLevelsStats" S + INNER JOIN "PollingStationsPerLevel" PS ON S."Level" = PS."Level" AND S."Path" = PS."Path"; + """; + + var queryArgs = new + { + electionRoundId = req.ElectionRoundId, + ngoId = req.NgoId, + monitoringObserverId = req.MonitoringObserverId, + dataSource = req.DataSource.ToString() + }; + + List levelStats; + using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct)) + { + levelStats = (await dbConnection.QueryAsync(sql, queryArgs)).ToList(); + } + + return new Response + { + Levels = levelStats + .Where(x => x.Level is >= 1 and <= 5) + .GroupBy(x => x.Level) + .OrderBy(g => g.Key) + .Select(g => new LevelStatistics + { + Level = g.Key, + Stats = g.OrderBy(s => s.Path).ToList() + }) + .Where(l => l.Stats.Count > 0) + .ToList() + }; + } +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Request.cs b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Request.cs new file mode 100644 index 000000000..faaf99109 --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Request.cs @@ -0,0 +1,17 @@ +using Vote.Monitor.Core.Models; +using Vote.Monitor.Core.Security; + +namespace Feature.Statistics.GetMonitoringObserverLevelStatistics; + +public class Request +{ + public Guid ElectionRoundId { get; set; } + + [FromClaim(ApplicationClaimTypes.NgoId)] + public Guid NgoId { get; set; } + + public Guid MonitoringObserverId { get; set; } + + [QueryParam] + public DataSource DataSource { get; set; } = DataSource.Ngo; +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Response.cs b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Response.cs new file mode 100644 index 000000000..72685a1d0 --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Response.cs @@ -0,0 +1,14 @@ +using Feature.Statistics.GetNgoAdminStatistics.Models; + +namespace Feature.Statistics.GetMonitoringObserverLevelStatistics; + +public class Response +{ + public List Levels { get; set; } = []; +} + +public class LevelStatistics +{ + public int Level { get; set; } + public List Stats { get; set; } = []; +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Validator.cs b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Validator.cs new file mode 100644 index 000000000..dea9f04f8 --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverLevelStatistics/Validator.cs @@ -0,0 +1,12 @@ +namespace Feature.Statistics.GetMonitoringObserverLevelStatistics; + +public class Validator : Validator +{ + public Validator() + { + RuleFor(x => x.ElectionRoundId).NotEmpty(); + RuleFor(x => x.NgoId).NotEmpty(); + RuleFor(x => x.MonitoringObserverId).NotEmpty(); + RuleFor(x => x.DataSource).NotEmpty(); + } +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverStats/Endpoint.cs b/api/src/Feature.Statistics/GetMonitoringObserverStats/Endpoint.cs new file mode 100644 index 000000000..52a9a666f --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverStats/Endpoint.cs @@ -0,0 +1,157 @@ +using Authorization.Policies; +using Authorization.Policies.Requirements; +using Dapper; +using Feature.Statistics.Options; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using Vote.Monitor.Domain.ConnectionFactory; +using ZiggyCreatures.Caching.Fusion; + +namespace Feature.Statistics.GetMonitoringObserverStats; + +public class Endpoint( + IAuthorizationService authorizationService, + INpgsqlConnectionFactory dbConnectionFactory, + IFusionCache cache, + IOptions options) : Endpoint, NotFound>> +{ + private readonly StatisticsFeatureOptions _options = options.Value; + + public override void Configure() + { + Get("/api/election-rounds/{electionRoundId}/monitoring-observers/{monitoringObserverId}:stats"); + DontAutoTag(); + Options(x => x.WithTags("statistics", "monitoring-observers")); + Summary(s => { s.Summary = "Workload statistics for a specific monitoring observer"; }); + Policies(PolicyNames.NgoAdminsOnly); + } + + public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + { + var authorizationResult = + await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); + if (!authorizationResult.Succeeded) + { + return TypedResults.NotFound(); + } + + var cacheKey = + $"monitoring-observer-stats-{req.ElectionRoundId}-{req.NgoId}-{req.MonitoringObserverId}-{req.DataSource}"; + + var response = await cache.GetOrSetAsync( + cacheKey, + async _ => await GetStatisticsAsync(req, ct), + cacheOptions => cacheOptions.SetDuration(TimeSpan.FromMinutes(_options.CacheDurationInMinutes)), + token: ct); + + return TypedResults.Ok(response); + } + + private async Task GetStatisticsAsync(Request req, CancellationToken ct) + { + const string sql = + """ + WITH "AvailableObservers" AS ( + SELECT "MonitoringObserverId" + FROM "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, @dataSource) + WHERE "MonitoringObserverId" = @monitoringObserverId + ), + "VisitedStations" AS ( + SELECT FS."PollingStationId" + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."MonitoringObserverId" = @monitoringObserverId + AND EXISTS (SELECT 1 FROM "AvailableObservers") + UNION + SELECT PSI."PollingStationId" + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."MonitoringObserverId" = @monitoringObserverId + AND EXISTS (SELECT 1 FROM "AvailableObservers") + UNION + SELECT QR."PollingStationId" + FROM "QuickReports" QR + WHERE QR."ElectionRoundId" = @electionRoundId + AND QR."MonitoringObserverId" = @monitoringObserverId + AND QR."PollingStationId" IS NOT NULL + AND EXISTS (SELECT 1 FROM "AvailableObservers") + UNION + SELECT IR."PollingStationId" + FROM "IncidentReports" IR + WHERE IR."ElectionRoundId" = @electionRoundId + AND IR."MonitoringObserverId" = @monitoringObserverId + AND IR."PollingStationId" IS NOT NULL + AND EXISTS (SELECT 1 FROM "AvailableObservers") + ) + SELECT + (SELECT COUNT(*) FROM "VisitedStations") AS "VisitedPollingStations", + ( + SELECT COUNT(*) + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."MonitoringObserverId" = @monitoringObserverId + AND EXISTS (SELECT 1 FROM "AvailableObservers") + ) AS "FormSubmissions", + ( + SELECT COALESCE(( + SELECT SUM(FS."NumberOfFlaggedAnswers") + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."MonitoringObserverId" = @monitoringObserverId + ), 0) + + COALESCE(( + SELECT SUM(PSI."NumberOfFlaggedAnswers") + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."MonitoringObserverId" = @monitoringObserverId + ), 0) + ) AS "FlaggedAnswers", + ( + SELECT COALESCE(( + SELECT SUM(FS."NumberOfQuestionsAnswered") + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."MonitoringObserverId" = @monitoringObserverId + ), 0) + + COALESCE(( + SELECT SUM(PSI."NumberOfQuestionsAnswered") + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."MonitoringObserverId" = @monitoringObserverId + ), 0) + ) AS "TotalAnswers", + ( + SELECT COALESCE(SUM("ComputeMinutesMonitoring"("ArrivalTime", "DepartureTime", "Breaks")), 0) + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."MonitoringObserverId" = @monitoringObserverId + AND EXISTS (SELECT 1 FROM "AvailableObservers") + ) AS "MinutesMonitoring", + ( + SELECT COUNT(*) + FROM "QuickReports" QR + WHERE QR."ElectionRoundId" = @electionRoundId + AND QR."MonitoringObserverId" = @monitoringObserverId + AND EXISTS (SELECT 1 FROM "AvailableObservers") + ) AS "QuickReports", + ( + SELECT COUNT(*) + FROM "IncidentReports" IR + WHERE IR."ElectionRoundId" = @electionRoundId + AND IR."MonitoringObserverId" = @monitoringObserverId + AND EXISTS (SELECT 1 FROM "AvailableObservers") + ) AS "IncidentReports"; + """; + + var queryArgs = new + { + electionRoundId = req.ElectionRoundId, + ngoId = req.NgoId, + monitoringObserverId = req.MonitoringObserverId, + dataSource = req.DataSource.ToString() + }; + + using var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct); + return await dbConnection.QueryFirstAsync(sql, queryArgs); + } +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverStats/Request.cs b/api/src/Feature.Statistics/GetMonitoringObserverStats/Request.cs new file mode 100644 index 000000000..74d1d2a69 --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverStats/Request.cs @@ -0,0 +1,17 @@ +using Vote.Monitor.Core.Models; +using Vote.Monitor.Core.Security; + +namespace Feature.Statistics.GetMonitoringObserverStats; + +public class Request +{ + public Guid ElectionRoundId { get; set; } + + [FromClaim(ApplicationClaimTypes.NgoId)] + public Guid NgoId { get; set; } + + public Guid MonitoringObserverId { get; set; } + + [QueryParam] + public DataSource DataSource { get; set; } = DataSource.Ngo; +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverStats/Response.cs b/api/src/Feature.Statistics/GetMonitoringObserverStats/Response.cs new file mode 100644 index 000000000..86163ca30 --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverStats/Response.cs @@ -0,0 +1,12 @@ +namespace Feature.Statistics.GetMonitoringObserverStats; + +public class Response +{ + public int VisitedPollingStations { get; set; } + public int FormSubmissions { get; set; } + public int FlaggedAnswers { get; set; } + public int TotalAnswers { get; set; } + public double MinutesMonitoring { get; set; } + public int QuickReports { get; set; } + public int IncidentReports { get; set; } +} diff --git a/api/src/Feature.Statistics/GetMonitoringObserverStats/Validator.cs b/api/src/Feature.Statistics/GetMonitoringObserverStats/Validator.cs new file mode 100644 index 000000000..70b53ab89 --- /dev/null +++ b/api/src/Feature.Statistics/GetMonitoringObserverStats/Validator.cs @@ -0,0 +1,12 @@ +namespace Feature.Statistics.GetMonitoringObserverStats; + +public class Validator : Validator +{ + public Validator() + { + RuleFor(x => x.ElectionRoundId).NotEmpty(); + RuleFor(x => x.NgoId).NotEmpty(); + RuleFor(x => x.MonitoringObserverId).NotEmpty(); + RuleFor(x => x.DataSource).NotEmpty(); + } +} diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs b/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs new file mode 100644 index 000000000..db2acdfca --- /dev/null +++ b/api/src/Feature.Statistics/GetPollingStationStats/Endpoint.cs @@ -0,0 +1,160 @@ +using Authorization.Policies; +using Authorization.Policies.Requirements; +using Dapper; +using Feature.Statistics.Options; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using Vote.Monitor.Domain.ConnectionFactory; +using ZiggyCreatures.Caching.Fusion; + +namespace Feature.Statistics.GetPollingStationStats; + +public class Endpoint( + IAuthorizationService authorizationService, + INpgsqlConnectionFactory dbConnectionFactory, + IFusionCache cache, + IOptions options) : Endpoint, NotFound>> +{ + private readonly StatisticsFeatureOptions _options = options.Value; + + public override void Configure() + { + Get("/api/election-rounds/{electionRoundId}/polling-stations/{pollingStationId}:stats"); + DontAutoTag(); + Options(x => x.WithTags("statistics", "polling-stations")); + Summary(s => { s.Summary = "Overview statistics for a specific polling station"; }); + Policies(PolicyNames.NgoAdminsOnly); + } + + public override async Task, NotFound>> ExecuteAsync(Request req, CancellationToken ct) + { + var authorizationResult = + await authorizationService.AuthorizeAsync(User, new MonitoringNgoAdminRequirement(req.ElectionRoundId)); + if (!authorizationResult.Succeeded) + { + return TypedResults.NotFound(); + } + + var cacheKey = + $"polling-station-stats-{req.ElectionRoundId}-{req.NgoId}-{req.PollingStationId}-{req.DataSource}"; + + var response = await cache.GetOrSetAsync( + cacheKey, + async _ => await GetStatisticsAsync(req, ct), + cacheOptions => cacheOptions.SetDuration(TimeSpan.FromMinutes(_options.CacheDurationInMinutes)), + token: ct); + + return TypedResults.Ok(response); + } + + private async Task GetStatisticsAsync(Request req, CancellationToken ct) + { + const string sql = + """ + WITH "AvailableObservers" AS ( + SELECT "MonitoringObserverId" + FROM "GetAvailableMonitoringObservers"(@electionRoundId, @ngoId, @dataSource) + ) + SELECT + ( + SELECT COALESCE(( + SELECT SUM(FS."NumberOfQuestionsAnswered") + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."PollingStationId" = @pollingStationId + AND FS."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ), 0) + + COALESCE(( + SELECT SUM(PSI."NumberOfQuestionsAnswered") + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."PollingStationId" = @pollingStationId + AND PSI."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ), 0) + ) AS "TotalAnswers", + ( + SELECT COALESCE(( + SELECT SUM(FS."NumberOfFlaggedAnswers") + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."PollingStationId" = @pollingStationId + AND FS."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ), 0) + + COALESCE(( + SELECT SUM(PSI."NumberOfFlaggedAnswers") + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."PollingStationId" = @pollingStationId + AND PSI."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ), 0) + ) AS "FlaggedAnswers", + ( + SELECT COUNT(DISTINCT "MonitoringObserverId") + FROM ( + SELECT FS."MonitoringObserverId" + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."PollingStationId" = @pollingStationId + AND FS."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + UNION + SELECT PSI."MonitoringObserverId" + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."PollingStationId" = @pollingStationId + AND PSI."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + UNION + SELECT QR."MonitoringObserverId" + FROM "QuickReports" QR + WHERE QR."ElectionRoundId" = @electionRoundId + AND QR."PollingStationId" = @pollingStationId + AND QR."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + UNION + SELECT IR."MonitoringObserverId" + FROM "IncidentReports" IR + WHERE IR."ElectionRoundId" = @electionRoundId + AND IR."PollingStationId" = @pollingStationId + AND IR."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ) T + ) AS "ObserversVisited", + ( + SELECT COALESCE(SUM("ComputeMinutesMonitoring"("ArrivalTime", "DepartureTime", "Breaks")), 0) + FROM "PollingStationInformation" PSI + WHERE PSI."ElectionRoundId" = @electionRoundId + AND PSI."PollingStationId" = @pollingStationId + AND PSI."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ) AS "MinutesMonitoring", + ( + SELECT COUNT(*) + FROM "FormSubmissions" FS + WHERE FS."ElectionRoundId" = @electionRoundId + AND FS."PollingStationId" = @pollingStationId + AND FS."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ) AS "FormSubmissions", + ( + SELECT COUNT(*) + FROM "QuickReports" QR + WHERE QR."ElectionRoundId" = @electionRoundId + AND QR."PollingStationId" = @pollingStationId + AND QR."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ) AS "QuickReports", + ( + SELECT COUNT(*) + FROM "IncidentReports" IR + WHERE IR."ElectionRoundId" = @electionRoundId + AND IR."PollingStationId" = @pollingStationId + AND IR."MonitoringObserverId" IN (SELECT "MonitoringObserverId" FROM "AvailableObservers") + ) AS "IncidentReports"; + """; + + var queryArgs = new + { + electionRoundId = req.ElectionRoundId, + ngoId = req.NgoId, + pollingStationId = req.PollingStationId, + dataSource = req.DataSource.ToString() + }; + + using var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct); + return await dbConnection.QueryFirstAsync(sql, queryArgs); + } +} diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Request.cs b/api/src/Feature.Statistics/GetPollingStationStats/Request.cs new file mode 100644 index 000000000..a5f5e23be --- /dev/null +++ b/api/src/Feature.Statistics/GetPollingStationStats/Request.cs @@ -0,0 +1,17 @@ +using Vote.Monitor.Core.Models; +using Vote.Monitor.Core.Security; + +namespace Feature.Statistics.GetPollingStationStats; + +public class Request +{ + public Guid ElectionRoundId { get; set; } + + [FromClaim(ApplicationClaimTypes.NgoId)] + public Guid NgoId { get; set; } + + public Guid PollingStationId { get; set; } + + [QueryParam] + public DataSource DataSource { get; set; } = DataSource.Ngo; +} diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Response.cs b/api/src/Feature.Statistics/GetPollingStationStats/Response.cs new file mode 100644 index 000000000..5f754a33f --- /dev/null +++ b/api/src/Feature.Statistics/GetPollingStationStats/Response.cs @@ -0,0 +1,12 @@ +namespace Feature.Statistics.GetPollingStationStats; + +public class Response +{ + public int TotalAnswers { get; set; } + public int FlaggedAnswers { get; set; } + public int ObserversVisited { get; set; } + public double MinutesMonitoring { get; set; } + public int FormSubmissions { get; set; } + public int QuickReports { get; set; } + public int IncidentReports { get; set; } +} diff --git a/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs b/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs new file mode 100644 index 000000000..4f565fa66 --- /dev/null +++ b/api/src/Feature.Statistics/GetPollingStationStats/Validator.cs @@ -0,0 +1,12 @@ +namespace Feature.Statistics.GetPollingStationStats; + +public class Validator : Validator +{ + public Validator() + { + RuleFor(x => x.ElectionRoundId).NotEmpty(); + RuleFor(x => x.NgoId).NotEmpty(); + RuleFor(x => x.PollingStationId).NotEmpty(); + RuleFor(x => x.DataSource).NotEmpty(); + } +} diff --git a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportCitizenReportsFilers.cs b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportCitizenReportsFilers.cs index 0b20d63eb..464d1d61d 100644 --- a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportCitizenReportsFilers.cs +++ b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportCitizenReportsFilers.cs @@ -12,6 +12,7 @@ public record ExportCitizenReportsFilers public string? Level3Filter { get; init; } public string? Level4Filter { get; init; } public string? Level5Filter { get; init; } + public Guid? LocationId { get; init; } public bool? HasFlaggedAnswers { get; init; } public CitizenReportFollowUpStatus? FollowUpStatus { get; init; } 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 10cd90209..cc89c6c8b 100644 --- a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportFormSubmissionsFilters.cs +++ b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportFormSubmissionsFilters.cs @@ -17,6 +17,7 @@ public record ExportFormSubmissionsFilters public string? Level4Filter { get; init; } public string? Level5Filter { get; init; } public string? PollingStationNumberFilter { get; init; } + public Guid? PollingStationId { get; init; } public bool? HasFlaggedAnswers { get; init; } public SubmissionFollowUpStatus? FollowUpStatus { get; init; } 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 d6205d3fd..2868e37f5 100644 --- a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportIncidentReportsFilters.cs +++ b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportIncidentReportsFilters.cs @@ -14,6 +14,7 @@ public record ExportIncidentReportsFilters public string? Level4Filter { get; init; } public string? Level5Filter { get; init; } public string? PollingStationNumberFilter { get; init; } + public Guid? PollingStationId { get; init; } public bool? HasFlaggedAnswers { get; init; } public Guid? MonitoringObserverId { get; init; } public string[]? TagsFilter { get; init; } = []; diff --git a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportQuickReportsFilters.cs b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportQuickReportsFilters.cs index 67f806add..babb431f2 100644 --- a/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportQuickReportsFilters.cs +++ b/api/src/Vote.Monitor.Domain/Entities/ExportedDataAggregate/Filters/ExportQuickReportsFilters.cs @@ -24,5 +24,6 @@ public record ExportQuickReportsFilters public DateTime? ToDateFilter { get; init; } public bool? HasAttachments { get; init; } public Guid? MonitoringObserverId { get; init; } + public Guid? PollingStationId { get; init; } public string[]? TagsFilter { get; init; } = []; } diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/CitizenReports/ExportCitizenReportsJob.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/CitizenReports/ExportCitizenReportsJob.cs index a352045e7..f91d3e3e4 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/CitizenReports/ExportCitizenReportsJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/CitizenReports/ExportCitizenReportsJob.cs @@ -184,6 +184,7 @@ @followUpStatus IS NULL AND (@level3 IS NULL OR L."Level3" = @level3) AND (@level4 IS NULL OR L."Level4" = @level4) AND (@level5 IS NULL OR L."Level5" = @level5) + AND (@locationId IS NULL OR CR."LocationId" = @locationId) AND (@hasFlaggedAnswers is NULL OR @hasFlaggedAnswers = false OR 1 = 2) AND (@formId IS NULL OR CR."FormId" = @formId) AND (@questionsAnswered IS NULL @@ -212,6 +213,7 @@ ORDER BY level3 = filters?.Level3Filter, level4 = filters?.Level4Filter, level5 = filters?.Level5Filter, + locationId = filters?.LocationId, formId = filters?.FormId, hasAttachments = filters?.HasAttachments, hasNotes = filters?.HasNotes, 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 8ba387128..9c5ed7b7b 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsJob.cs @@ -367,6 +367,10 @@ @LEVEL4 IS NULL @LEVEL5 IS NULL OR PS."Level5" = @LEVEL5 ) + AND ( + @POLLINGSTATIONID IS NULL + OR PS."Id" = @POLLINGSTATIONID + ) AND ( @POLLINGSTATIONNUMBER IS NULL OR PS."Number" = @POLLINGSTATIONNUMBER @@ -429,6 +433,7 @@ @HASATTACHMENTS IS NULL level4 = filters.Level4Filter, level5 = filters.Level5Filter, pollingStationNumber = filters.PollingStationNumberFilter, + pollingStationId = filters.PollingStationId, hasFlaggedAnswers = filters.HasFlaggedAnswers, followUpStatus = filters.FollowUpStatus?.ToString(), tagsFilter = filters.TagsFilter ?? [], diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsSimplifiedJob.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsSimplifiedJob.cs index c4813fa8e..ce09471d1 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsSimplifiedJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/FormSubmissions/ExportFormSubmissionsSimplifiedJob.cs @@ -367,6 +367,10 @@ @LEVEL4 IS NULL @LEVEL5 IS NULL OR PS."Level5" = @LEVEL5 ) + AND ( + @POLLINGSTATIONID IS NULL + OR PS."Id" = @POLLINGSTATIONID + ) AND ( @POLLINGSTATIONNUMBER IS NULL OR PS."Number" = @POLLINGSTATIONNUMBER @@ -429,6 +433,7 @@ @HASATTACHMENTS IS NULL level4 = filters.Level4Filter, level5 = filters.Level5Filter, pollingStationNumber = filters.PollingStationNumberFilter, + pollingStationId = filters.PollingStationId, hasFlaggedAnswers = filters.HasFlaggedAnswers, followUpStatus = filters.FollowUpStatus?.ToString(), tagsFilter = filters.TagsFilter ?? [], 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 e1e49d202..2ee0fb1b9 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ExportIncidentReportsJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/IncidentReports/ExportIncidentReportsJob.cs @@ -233,6 +233,7 @@ OR IR."PhoneNumber" ILIKE @searchText 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 (@pollingStationId IS NULL OR IR."PollingStationId" = @pollingStationId) AND (@hasFlaggedAnswers IS NULL OR (IR."NumberOfFlaggedAnswers" = 0 AND @hasFlaggedAnswers = FALSE) OR (IR."NumberOfFlaggedAnswers" > 0 AND @hasFlaggedAnswers = TRUE) @@ -264,6 +265,7 @@ OR IR."PhoneNumber" ILIKE @searchText level4 = filters.Level4Filter, level5 = filters.Level5Filter, pollingStationNumber = filters.PollingStationNumberFilter, + pollingStationId = filters.PollingStationId, hasFlaggedAnswers = filters.HasFlaggedAnswers, followUpStatus = filters.FollowUpStatus?.ToString(), tagsFilter = filters.TagsFilter ?? [], diff --git a/api/src/Vote.Monitor.Hangfire/Jobs/Export/QuickReports/ExportQuickReportsJob.cs b/api/src/Vote.Monitor.Hangfire/Jobs/Export/QuickReports/ExportQuickReportsJob.cs index a5f0059cc..c4ee7db64 100644 --- a/api/src/Vote.Monitor.Hangfire/Jobs/Export/QuickReports/ExportQuickReportsJob.cs +++ b/api/src/Vote.Monitor.Hangfire/Jobs/Export/QuickReports/ExportQuickReportsJob.cs @@ -184,6 +184,10 @@ @LEVEL4 IS NULL @LEVEL5 IS NULL OR PS."Level5" = @LEVEL5 ) + AND ( + @POLLINGSTATIONID IS NULL + OR QR."PollingStationId" = @POLLINGSTATIONID + ) AND ( @FROMDATE IS NULL OR QR."LastUpdatedAt" >= @FROMDATE::TIMESTAMP @@ -246,6 +250,7 @@ ORDER BY toDate = filters.ToDateFilter?.ToString("O"), hasAttachments = filters.HasAttachments, monitoringObserverId = filters.MonitoringObserverId, + pollingStationId = filters.PollingStationId, tagsFilter = filters.TagsFilter ?? [], }; diff --git a/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx b/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx index 824447dfc..015ac9b14 100644 --- a/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx +++ b/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx @@ -25,6 +25,7 @@ export function CitizenReportsByEntryTable(props: CitizenReportsByEntryTableProp const queryParams = useMemo(() => { const params = [ ['followUpStatus', debouncedSearch.citizenReportFollowUpStatus], + ['locationId', debouncedSearch.locationId], ].filter(([_, value]) => value); return Object.fromEntries(params) as FormSubmissionsSearchParams; diff --git a/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx b/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx index 64fb97734..e68f9152c 100644 --- a/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx +++ b/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx @@ -49,6 +49,7 @@ export function CitizenReportsTab(): FunctionComponent { const params = [ ['followUpStatus', search.citizenReportFollowUpStatus], ['hasFlaggedAnswers', search.hasFlaggedAnswers], + ['locationId', search.locationId], ].filter(([_, value]) => value); return Object.fromEntries(params); diff --git a/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx b/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx index b1300ee30..268cfe3a1 100644 --- a/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx @@ -43,6 +43,7 @@ export function FormSubmissionsAggregatedByFormTable({ level4Filter: search.level4Filter, level5Filter: search.level5Filter, pollingStationNumberFilter: search.pollingStationNumberFilter, + pollingStationId: search.pollingStationId, followUpStatus: search.followUpStatus, questionsAnswered: search.questionsAnswered, hasNotes: search.hasNotes, @@ -69,6 +70,7 @@ export function FormSubmissionsAggregatedByFormTable({ level4Filter: search.level4Filter, level5Filter: search.level5Filter, pollingStationNumberFilter: search.pollingStationNumberFilter, + pollingStationId: search.pollingStationId, followUpStatus: search.followUpStatus, questionsAnswered: search.questionsAnswered, hasNotes: toBoolean(search.hasNotes), diff --git a/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx b/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx index 894609bbe..789435239 100644 --- a/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx @@ -37,6 +37,7 @@ export interface FormSubmissionsSearchRequest{ toDateFilter: string | undefined; coalitionMemberId: string | undefined; monitoringObserverId: string | undefined; + pollingStationId: string | undefined; } export function FormSubmissionsByEntryTable({ searchText }: FormSubmissionsByEntryTableProps): FunctionComponent { @@ -71,6 +72,7 @@ export function FormSubmissionsByEntryTable({ searchText }: FormSubmissionsByEnt toDateFilter: debouncedSearch.submissionsToDate?.toISOString(), coalitionMemberId: debouncedSearch.coalitionMemberId, monitoringObserverId: debouncedSearch.monitoringObserverId || undefined, + pollingStationId: debouncedSearch.pollingStationId || undefined, }; return params; diff --git a/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx b/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx index 9bcb1e531..df95e662a 100644 --- a/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx +++ b/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx @@ -80,6 +80,7 @@ export default function FormSubmissionsTab(): FunctionComponent { toDateFilter: search.submissionsToDate?.toISOString(), coalitionMemberId: search.coalitionMemberId, monitoringObserverId: search.monitoringObserverId || undefined, + pollingStationId: search.pollingStationId || undefined, }; return params; diff --git a/web/src/features/responses/components/IncidentReportsByEntryTable/IncidentReportsByEntryTable.tsx b/web/src/features/responses/components/IncidentReportsByEntryTable/IncidentReportsByEntryTable.tsx index 19cb2ff6e..b39c5ecef 100644 --- a/web/src/features/responses/components/IncidentReportsByEntryTable/IncidentReportsByEntryTable.tsx +++ b/web/src/features/responses/components/IncidentReportsByEntryTable/IncidentReportsByEntryTable.tsx @@ -39,6 +39,7 @@ export function IncidentReportsByEntryTable({ searchText }: FormsTableByEntryPro ['followUpStatus', debouncedSearch.incidentReportFollowUpStatus], ['locationType', debouncedSearch.incidentReportLocationType], ['monitoringObserverId', debouncedSearch.monitoringObserverId], + ['pollingStationId', debouncedSearch.pollingStationId], ].filter(([_, value]) => value); return Object.fromEntries(params) as IncidentReportsSearchParams; diff --git a/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx b/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx index 235742d42..443715d3c 100644 --- a/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx +++ b/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx @@ -66,6 +66,7 @@ export default function IncidentReportsTab(): FunctionComponent { ['followUpStatus', search.incidentReportFollowUpStatus], ['locationType', search.incidentReportLocationType], ['monitoringObserverId', search.monitoringObserverId], + ['pollingStationId', search.pollingStationId], ].filter(([_, value]) => value); return Object.fromEntries(params); diff --git a/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx b/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx index 7f68e4b08..9f1259e1a 100644 --- a/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx +++ b/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx @@ -51,6 +51,7 @@ export interface QuickReportFilterRequest { incidentCategory: IncidentCategory | undefined; coalitionMemberId: string | undefined; monitoringObserverId: string | undefined; + pollingStationId: string | undefined; hasAttachments: boolean | undefined; tagsFilter: string[] | undefined; } @@ -89,6 +90,7 @@ export function QuickReportsTab(): FunctionComponent { incidentCategory: debouncedSearch.incidentCategory, coalitionMemberId: search.coalitionMemberId, monitoringObserverId: debouncedSearch.monitoringObserverId || undefined, + pollingStationId: debouncedSearch.pollingStationId || undefined, searchText: searchText, hasAttachments: toBoolean(debouncedSearch.hasAttachments), tagsFilter: debouncedSearch.tagsFilter, diff --git a/web/src/features/responses/models/search-params.ts b/web/src/features/responses/models/search-params.ts index af8c87c27..fa07d5a7c 100644 --- a/web/src/features/responses/models/search-params.ts +++ b/web/src/features/responses/models/search-params.ts @@ -29,6 +29,7 @@ export const FormSubmissionsSearchParamsSchema = ResponsesPageSearchParamsSchema level4Filter: z.string().catch('').optional(), level5Filter: z.string().catch('').optional(), pollingStationNumberFilter: z.string().catch('').optional(), + pollingStationId: z.string().catch('').optional(), hasFlaggedAnswers: z.string().catch('').optional(), monitoringObserverId: z.string().catch('').optional(), tagsFilter: z.array(z.string()).optional().catch([]).optional(), @@ -45,6 +46,7 @@ export const FormSubmissionsSearchParamsSchema = ResponsesPageSearchParamsSchema hasNotes: z.string().catch('').optional(), hasAttachments: z.string().catch('').optional(), formId: z.string().optional(), + locationId: z.string().catch('').optional(), submissionsFromDate: z.coerce.date().optional(), submissionsToDate: z.coerce.date().optional(), @@ -67,6 +69,7 @@ export const QuickReportsSearchParamsSchema = ZDataSourceSearchSchema.merge( incidentCategory: z.nativeEnum(IncidentCategory).optional(), hasAttachments: z.string().catch('').optional(), monitoringObserverId: z.string().catch('').optional(), + pollingStationId: z.string().catch('').optional(), tagsFilter: z.array(z.string()).optional().catch([]).optional(), }) ); @@ -76,6 +79,7 @@ export type QuickReportsSearchParams = z.infer