From 807fd68e14f1ae39d8fb976dd93a2868783c0c3a Mon Sep 17 00:00:00 2001 From: "ion.dormenco" Date: Tue, 29 Sep 2026 15:20:41 +0300 Subject: [PATCH] Add location level stats to election round and monitoring NGO stats. Co-authored-by: Cursor --- .../GetElectionRoundStatistics/Endpoint.cs | 150 +++++++++++++++++ .../GetElectionRoundStatistics/Response.cs | 8 + .../GetMonitoringNgoStatistics/Endpoint.cs | 151 ++++++++++++++++++ .../GetMonitoringNgoStatistics/Response.cs | 8 + .../Models/VisitedLocationLevelStats.cs | 13 ++ 5 files changed, 330 insertions(+) create mode 100644 api/src/Feature.Statistics/GetNgoAdminStatistics/Models/VisitedLocationLevelStats.cs diff --git a/api/src/Feature.Statistics/GetElectionRoundStatistics/Endpoint.cs b/api/src/Feature.Statistics/GetElectionRoundStatistics/Endpoint.cs index 9b9c3b660..c5d2bb31e 100644 --- a/api/src/Feature.Statistics/GetElectionRoundStatistics/Endpoint.cs +++ b/api/src/Feature.Statistics/GetElectionRoundStatistics/Endpoint.cs @@ -352,6 +352,148 @@ AND PS."Level5" IS NOT NULL INNER JOIN "PollingStationsPerLevel" PS ON S."Level" = PS."Level" AND S."Path" = PS."Path" INNER JOIN "ActiveObserversPerLevel" AOL ON S."Level" = AOL."Level" AND S."Path" = AOL."Path"; + WITH + "LocationStatsRaw" AS ( + SELECT + CR."LocationId", + COUNT(1) AS "NumberOfCitizenReports", + SUM(CR."NumberOfFlaggedAnswers") AS "NumberOfFlaggedAnswers", + SUM(CR."NumberOfQuestionsAnswered") AS "NumberOfQuestionsAnswered" + FROM "CitizenReports" CR + WHERE CR."ElectionRoundId" = @electionRoundId + AND CR."NumberOfQuestionsAnswered" > 0 + GROUP BY CR."LocationId" + ), + "ElectionLocations" AS ( + SELECT + L."Id", + NULLIF(L."Level1", '') AS "Level1", + NULLIF(L."Level2", '') AS "Level2", + NULLIF(L."Level3", '') AS "Level3", + NULLIF(L."Level4", '') AS "Level4", + NULLIF(L."Level5", '') AS "Level5" + FROM "Locations" L + WHERE L."ElectionRoundId" = @electionRoundId + AND L."Level1" != '' + ), + "LocationsPerLevel" AS ( + SELECT + CASE + WHEN GROUPING(L."Level1") = 1 THEN 0 + WHEN GROUPING(L."Level2") = 1 THEN 1 + WHEN GROUPING(L."Level3") = 1 THEN 2 + WHEN GROUPING(L."Level4") = 1 THEN 3 + WHEN GROUPING(L."Level5") = 1 THEN 4 + ELSE 5 + END AS "Level", + CASE + WHEN GROUPING(L."Level1") = 1 THEN '/' + ELSE CONCAT_WS(' / ', L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + END AS "Path", + COUNT(L."Id") AS "NumberOfLocations" + FROM "ElectionLocations" L + GROUP BY GROUPING SETS ( + (), + (L."Level1"), + (L."Level1", L."Level2"), + (L."Level1", L."Level2", L."Level3"), + (L."Level1", L."Level2", L."Level3", L."Level4"), + (L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + ) + HAVING + GROUPING(L."Level1") = 1 + OR ( + GROUPING(L."Level2") = 1 + AND L."Level1" IS NOT NULL + ) + OR ( + GROUPING(L."Level3") = 1 + AND GROUPING(L."Level2") = 0 + AND L."Level2" IS NOT NULL + ) + OR ( + GROUPING(L."Level4") = 1 + AND GROUPING(L."Level3") = 0 + AND L."Level3" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 1 + AND GROUPING(L."Level4") = 0 + AND L."Level4" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 0 + AND L."Level5" IS NOT NULL + ) + ), + "LocationLevelsStats" AS ( + SELECT + CASE + WHEN GROUPING(L."Level1") = 1 THEN 0 + WHEN GROUPING(L."Level2") = 1 THEN 1 + WHEN GROUPING(L."Level3") = 1 THEN 2 + WHEN GROUPING(L."Level4") = 1 THEN 3 + WHEN GROUPING(L."Level5") = 1 THEN 4 + ELSE 5 + END AS "Level", + CASE + WHEN GROUPING(L."Level1") = 1 THEN '/' + ELSE CONCAT_WS(' / ', L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + END AS "Path", + COUNT(LS."LocationId") AS "NumberOfVisitedLocations", + COALESCE(SUM(LS."NumberOfCitizenReports"), 0) AS "NumberOfCitizenReports", + COALESCE(SUM(LS."NumberOfFlaggedAnswers"), 0) AS "NumberOfFlaggedAnswers", + COALESCE(SUM(LS."NumberOfQuestionsAnswered"), 0) AS "NumberOfQuestionsAnswered" + FROM "LocationStatsRaw" LS + INNER JOIN "ElectionLocations" L ON L."Id" = LS."LocationId" + GROUP BY GROUPING SETS ( + (), + (L."Level1"), + (L."Level1", L."Level2"), + (L."Level1", L."Level2", L."Level3"), + (L."Level1", L."Level2", L."Level3", L."Level4"), + (L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + ) + HAVING + GROUPING(L."Level1") = 1 + OR ( + GROUPING(L."Level2") = 1 + AND L."Level1" IS NOT NULL + ) + OR ( + GROUPING(L."Level3") = 1 + AND GROUPING(L."Level2") = 0 + AND L."Level2" IS NOT NULL + ) + OR ( + GROUPING(L."Level4") = 1 + AND GROUPING(L."Level3") = 0 + AND L."Level3" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 1 + AND GROUPING(L."Level4") = 0 + AND L."Level4" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 0 + AND L."Level5" IS NOT NULL + ) + ) + SELECT + S."Path", + S."Level", + S."NumberOfVisitedLocations", + L."NumberOfLocations", + S."NumberOfCitizenReports", + S."NumberOfFlaggedAnswers", + S."NumberOfQuestionsAnswered", + ( + S."NumberOfVisitedLocations" * 100.0 / NULLIF(L."NumberOfLocations", 0) + ) AS "CoveragePercentage" + FROM "LocationLevelsStats" S + INNER JOIN "LocationsPerLevel" L ON S."Level" = L."Level" AND S."Path" = L."Path"; + WITH "AvailableObservers" AS ( SELECT MO."Id" AS "MonitoringObserverId" FROM "MonitoringObservers" MO @@ -440,6 +582,7 @@ SELECT MO."Id" AS "MonitoringObserverId" NgosStats ngosStats; ObserversStats observersStats; List visitedPollingStationsStats; + List visitedLocationStats; List formSubmissionsHistogram; List quickReportsHistogram; List citizenReportsHistogram; @@ -452,6 +595,7 @@ SELECT MO."Id" AS "MonitoringObserverId" ngosStats = multi.ReadSingle(); observersStats = multi.ReadSingle(); visitedPollingStationsStats = multi.Read().ToList(); + visitedLocationStats = multi.Read().ToList(); formSubmissionsHistogram = multi.Read().ToList(); quickReportsHistogram = multi.Read().ToList(); citizenReportsHistogram = multi.Read().ToList(); @@ -468,6 +612,12 @@ SELECT MO."Id" AS "MonitoringObserverId" Level3Stats = visitedPollingStationsStats.Where(x => x.Level == 3).ToList(), Level4Stats = visitedPollingStationsStats.Where(x => x.Level == 4).ToList(), Level5Stats = visitedPollingStationsStats.Where(x => x.Level == 5).ToList(), + TotalLocationStats = visitedLocationStats.FirstOrDefault(x => x.Level == 0), + LocationLevel1Stats = visitedLocationStats.Where(x => x.Level == 1).ToList(), + LocationLevel2Stats = visitedLocationStats.Where(x => x.Level == 2).ToList(), + LocationLevel3Stats = visitedLocationStats.Where(x => x.Level == 3).ToList(), + LocationLevel4Stats = visitedLocationStats.Where(x => x.Level == 4).ToList(), + LocationLevel5Stats = visitedLocationStats.Where(x => x.Level == 5).ToList(), FormsHistogram = formSubmissionsHistogram.Select(x => new HistogramPoint { Bucket = x.Bucket, diff --git a/api/src/Feature.Statistics/GetElectionRoundStatistics/Response.cs b/api/src/Feature.Statistics/GetElectionRoundStatistics/Response.cs index 05244daa9..0bcdd1083 100644 --- a/api/src/Feature.Statistics/GetElectionRoundStatistics/Response.cs +++ b/api/src/Feature.Statistics/GetElectionRoundStatistics/Response.cs @@ -13,6 +13,14 @@ public class Response public List Level3Stats { get; set; } = []; public List Level4Stats { get; set; } = []; public List Level5Stats { get; set; } = []; + + public VisitedLocationLevelStats? TotalLocationStats { get; set; } + public List LocationLevel1Stats { get; set; } = []; + public List LocationLevel2Stats { get; set; } = []; + public List LocationLevel3Stats { get; set; } = []; + public List LocationLevel4Stats { get; set; } = []; + public List LocationLevel5Stats { get; set; } = []; + public HistogramPoint[] FormsHistogram { get; set; } = []; public HistogramPoint[] QuestionsHistogram { get; set; } = []; public HistogramPoint[] FlaggedAnswersHistogram { get; set; } = []; diff --git a/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Endpoint.cs b/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Endpoint.cs index bef13902c..dd7a00c99 100644 --- a/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Endpoint.cs +++ b/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Endpoint.cs @@ -367,6 +367,150 @@ AND PS."Level5" IS NOT NULL INNER JOIN "PollingStationsPerLevel" PS ON S."Level" = PS."Level" AND S."Path" = PS."Path" INNER JOIN "ActiveObserversPerLevel" AOL ON S."Level" = AOL."Level" AND S."Path" = AOL."Path"; + WITH + "LocationStatsRaw" AS ( + SELECT + CR."LocationId", + COUNT(1) AS "NumberOfCitizenReports", + SUM(CR."NumberOfFlaggedAnswers") AS "NumberOfFlaggedAnswers", + SUM(CR."NumberOfQuestionsAnswered") AS "NumberOfQuestionsAnswered" + FROM "CitizenReports" CR + INNER JOIN "ElectionRounds" ER ON ER."Id" = CR."ElectionRoundId" + WHERE CR."ElectionRoundId" = @electionRoundId + AND ER."MonitoringNgoForCitizenReportingId" = @monitoringNgoId + AND CR."NumberOfQuestionsAnswered" > 0 + GROUP BY CR."LocationId" + ), + "ElectionLocations" AS ( + SELECT + L."Id", + NULLIF(L."Level1", '') AS "Level1", + NULLIF(L."Level2", '') AS "Level2", + NULLIF(L."Level3", '') AS "Level3", + NULLIF(L."Level4", '') AS "Level4", + NULLIF(L."Level5", '') AS "Level5" + FROM "Locations" L + WHERE L."ElectionRoundId" = @electionRoundId + AND L."Level1" != '' + ), + "LocationsPerLevel" AS ( + SELECT + CASE + WHEN GROUPING(L."Level1") = 1 THEN 0 + WHEN GROUPING(L."Level2") = 1 THEN 1 + WHEN GROUPING(L."Level3") = 1 THEN 2 + WHEN GROUPING(L."Level4") = 1 THEN 3 + WHEN GROUPING(L."Level5") = 1 THEN 4 + ELSE 5 + END AS "Level", + CASE + WHEN GROUPING(L."Level1") = 1 THEN '/' + ELSE CONCAT_WS(' / ', L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + END AS "Path", + COUNT(L."Id") AS "NumberOfLocations" + FROM "ElectionLocations" L + GROUP BY GROUPING SETS ( + (), + (L."Level1"), + (L."Level1", L."Level2"), + (L."Level1", L."Level2", L."Level3"), + (L."Level1", L."Level2", L."Level3", L."Level4"), + (L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + ) + HAVING + GROUPING(L."Level1") = 1 + OR ( + GROUPING(L."Level2") = 1 + AND L."Level1" IS NOT NULL + ) + OR ( + GROUPING(L."Level3") = 1 + AND GROUPING(L."Level2") = 0 + AND L."Level2" IS NOT NULL + ) + OR ( + GROUPING(L."Level4") = 1 + AND GROUPING(L."Level3") = 0 + AND L."Level3" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 1 + AND GROUPING(L."Level4") = 0 + AND L."Level4" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 0 + AND L."Level5" IS NOT NULL + ) + ), + "LocationLevelsStats" AS ( + SELECT + CASE + WHEN GROUPING(L."Level1") = 1 THEN 0 + WHEN GROUPING(L."Level2") = 1 THEN 1 + WHEN GROUPING(L."Level3") = 1 THEN 2 + WHEN GROUPING(L."Level4") = 1 THEN 3 + WHEN GROUPING(L."Level5") = 1 THEN 4 + ELSE 5 + END AS "Level", + CASE + WHEN GROUPING(L."Level1") = 1 THEN '/' + ELSE CONCAT_WS(' / ', L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + END AS "Path", + COUNT(LS."LocationId") AS "NumberOfVisitedLocations", + COALESCE(SUM(LS."NumberOfCitizenReports"), 0) AS "NumberOfCitizenReports", + COALESCE(SUM(LS."NumberOfFlaggedAnswers"), 0) AS "NumberOfFlaggedAnswers", + COALESCE(SUM(LS."NumberOfQuestionsAnswered"), 0) AS "NumberOfQuestionsAnswered" + FROM "LocationStatsRaw" LS + INNER JOIN "ElectionLocations" L ON L."Id" = LS."LocationId" + GROUP BY GROUPING SETS ( + (), + (L."Level1"), + (L."Level1", L."Level2"), + (L."Level1", L."Level2", L."Level3"), + (L."Level1", L."Level2", L."Level3", L."Level4"), + (L."Level1", L."Level2", L."Level3", L."Level4", L."Level5") + ) + HAVING + GROUPING(L."Level1") = 1 + OR ( + GROUPING(L."Level2") = 1 + AND L."Level1" IS NOT NULL + ) + OR ( + GROUPING(L."Level3") = 1 + AND GROUPING(L."Level2") = 0 + AND L."Level2" IS NOT NULL + ) + OR ( + GROUPING(L."Level4") = 1 + AND GROUPING(L."Level3") = 0 + AND L."Level3" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 1 + AND GROUPING(L."Level4") = 0 + AND L."Level4" IS NOT NULL + ) + OR ( + GROUPING(L."Level5") = 0 + AND L."Level5" IS NOT NULL + ) + ) + SELECT + S."Path", + S."Level", + S."NumberOfVisitedLocations", + L."NumberOfLocations", + S."NumberOfCitizenReports", + S."NumberOfFlaggedAnswers", + S."NumberOfQuestionsAnswered", + ( + S."NumberOfVisitedLocations" * 100.0 / NULLIF(L."NumberOfLocations", 0) + ) AS "CoveragePercentage" + FROM "LocationLevelsStats" S + INNER JOIN "LocationsPerLevel" L ON S."Level" = L."Level" AND S."Path" = L."Path"; + WITH "AvailableObservers" AS ( SELECT MO."Id" AS "MonitoringObserverId" FROM "MonitoringObservers" MO @@ -472,6 +616,7 @@ SELECT MO."Id" AS "MonitoringObserverId" var observersStats = multi.ReadSingle(); var visitedPollingStationsStats = multi.Read().ToList(); + var visitedLocationStats = multi.Read().ToList(); var formSubmissionsHistogram = multi.Read().ToList(); var quickReportsHistogram = multi.Read().ToList(); var citizenReportsHistogram = multi.Read().ToList(); @@ -487,6 +632,12 @@ SELECT MO."Id" AS "MonitoringObserverId" Level3Stats = visitedPollingStationsStats.Where(x => x.Level == 3).ToList(), Level4Stats = visitedPollingStationsStats.Where(x => x.Level == 4).ToList(), Level5Stats = visitedPollingStationsStats.Where(x => x.Level == 5).ToList(), + TotalLocationStats = visitedLocationStats.FirstOrDefault(x => x.Level == 0), + LocationLevel1Stats = visitedLocationStats.Where(x => x.Level == 1).ToList(), + LocationLevel2Stats = visitedLocationStats.Where(x => x.Level == 2).ToList(), + LocationLevel3Stats = visitedLocationStats.Where(x => x.Level == 3).ToList(), + LocationLevel4Stats = visitedLocationStats.Where(x => x.Level == 4).ToList(), + LocationLevel5Stats = visitedLocationStats.Where(x => x.Level == 5).ToList(), FormsHistogram = formSubmissionsHistogram.Select(x => new HistogramPoint { Bucket = x.Bucket, diff --git a/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Response.cs b/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Response.cs index 1498ca401..d2b9dc90b 100644 --- a/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Response.cs +++ b/api/src/Feature.Statistics/GetMonitoringNgoStatistics/Response.cs @@ -13,6 +13,14 @@ public class Response public List Level3Stats { get; set; } = []; public List Level4Stats { get; set; } = []; public List Level5Stats { get; set; } = []; + + public VisitedLocationLevelStats? TotalLocationStats { get; set; } + public List LocationLevel1Stats { get; set; } = []; + public List LocationLevel2Stats { get; set; } = []; + public List LocationLevel3Stats { get; set; } = []; + public List LocationLevel4Stats { get; set; } = []; + public List LocationLevel5Stats { get; set; } = []; + public HistogramPoint[] FormsHistogram { get; set; } = []; public HistogramPoint[] QuestionsHistogram { get; set; } = []; public HistogramPoint[] FlaggedAnswersHistogram { get; set; } = []; diff --git a/api/src/Feature.Statistics/GetNgoAdminStatistics/Models/VisitedLocationLevelStats.cs b/api/src/Feature.Statistics/GetNgoAdminStatistics/Models/VisitedLocationLevelStats.cs new file mode 100644 index 000000000..7c6b8abe7 --- /dev/null +++ b/api/src/Feature.Statistics/GetNgoAdminStatistics/Models/VisitedLocationLevelStats.cs @@ -0,0 +1,13 @@ +namespace Feature.Statistics.GetNgoAdminStatistics.Models; + +public class VisitedLocationLevelStats +{ + public string Path { get; set; } = string.Empty; + public int Level { get; set; } + public int NumberOfVisitedLocations { get; set; } + public int NumberOfLocations { get; set; } + public int NumberOfCitizenReports { get; set; } + public int NumberOfFlaggedAnswers { get; set; } + public int NumberOfQuestionsAnswered { get; set; } + public double CoveragePercentage { get; set; } +}