Skip to content

Commit af75e7c

Browse files
authored
Improvements on platform admin web + api (#909)
* Improvements on platform admin web + api * Update column-visibility-options.tsx
1 parent e5f926b commit af75e7c

File tree

20 files changed

+279
-54
lines changed

20 files changed

+279
-54
lines changed

api/src/Vote.Monitor.Api.Feature.Ngo/Create/Endpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public override async Task<Results<Ok<NgoModel>, ProblemDetails>> ExecuteAsync(R
3434
Id = ngo.Id,
3535
Name = ngo.Name,
3636
Status = ngo.Status,
37-
NumberOfElectionsMonitoring = 0,
37+
NumberOfMonitoredElections = 0,
3838
NumberOfNgoAdmins = 0,
3939
DateOfLastElection = null
4040
});

api/src/Vote.Monitor.Api.Feature.Ngo/Get/Endpoint.cs

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,31 @@ public override async Task<Results<Ok<NgoModel>, NotFound>> ExecuteAsync(Request
1818
CancellationToken ct)
1919
{
2020
var sql = """
21-
SELECT
22-
N."Id",
23-
N."Name",
24-
N."Status",
25-
(SELECT COUNT(1)
26-
FROM "NgoAdmins" NA
27-
WHERE NA."NgoId" = N."Id") AS "NumberOfNgoAdmins",
28-
(SELECT COUNT(1)
29-
FROM "MonitoringNgos" MN
30-
WHERE MN."NgoId" = N."Id") AS "NumberOfElectionsMonitoring",
31-
(
32-
SELECT MAX(ER."StartDate")
33-
FROM "MonitoringNgos" MN
34-
INNER JOIN "ElectionRounds" ER ON ER."Id" = MN."ElectionRoundId"
35-
WHERE MN."NgoId" = N."Id"
36-
) AS "DateOfLastElection"
37-
FROM
38-
"Ngos" N
39-
WHERE
40-
N."Id" = @ngoId
21+
SELECT N."Id",
22+
N."Name",
23+
N."Status",
24+
(SELECT COUNT(1)
25+
FROM "NgoAdmins" NA
26+
WHERE NA."NgoId" = N."Id") AS "NumberOfNgoAdmins",
27+
(SELECT COUNT(1)
28+
FROM "MonitoringNgos" MN
29+
WHERE MN."NgoId" = N."Id") AS "NumberOfMonitoredElections",
30+
31+
COALESCE((select jsonb_agg(jsonb_build_object('Id', er."Id",
32+
'Title', er."Title",
33+
'EnglishTitle', er."EnglishTitle",
34+
'StartDate', er."StartDate",
35+
'Status', er."Status"
36+
))
37+
from "MonitoringNgos" mn
38+
inner join "ElectionRounds" er on er."Id" = mn."ElectionRoundId"
39+
where mn."NgoId" = N."Id"), '[]'::JSONB) AS "MonitoredElections",
40+
(SELECT MAX(ER."StartDate")
41+
FROM "MonitoringNgos" MN
42+
INNER JOIN "ElectionRounds" ER ON ER."Id" = MN."ElectionRoundId"
43+
WHERE MN."NgoId" = N."Id") AS "DateOfLastElection"
44+
FROM "Ngos" N
45+
WHERE N."Id" = @ngoId
4146
""";
4247

4348
var queryArgs = new { ngoId = req.Id };

api/src/Vote.Monitor.Api.Feature.Ngo/List/Endpoint.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ WITH CTE_Ngos AS (
4343
WHERE NA."NgoId" = N."Id") AS "NumberOfNgoAdmins",
4444
(SELECT COUNT(1)
4545
FROM "MonitoringNgos" MN
46-
WHERE MN."NgoId" = N."Id") AS "NumberOfElectionsMonitoring",
46+
WHERE MN."NgoId" = N."Id") AS "NumberOfMonitoredElections",
4747
(
4848
SELECT MAX(ER."StartDate")
4949
FROM "MonitoringNgos" MN
@@ -81,10 +81,10 @@ ORDER BY
8181
WHEN @sortExpression = 'NumberOfNgoAdmins DESC' THEN "NumberOfNgoAdmins"
8282
END DESC,
8383
CASE
84-
WHEN @sortExpression = 'NumberOfElectionsMonitoring ASC' THEN "NumberOfElectionsMonitoring"
84+
WHEN @sortExpression = 'NumberOfMonitoredElections ASC' THEN "NumberOfMonitoredElections"
8585
END ASC,
8686
CASE
87-
WHEN @sortExpression = 'NumberOfElectionsMonitoring DESC' THEN "NumberOfElectionsMonitoring"
87+
WHEN @sortExpression = 'NumberOfMonitoredElections DESC' THEN "NumberOfMonitoredElections"
8888
END DESC,
8989
CASE
9090
WHEN @sortExpression = 'DateOfLastElection ASC' THEN "DateOfLastElection"
@@ -140,10 +140,10 @@ private static string GetSortExpression(string? sortColumnName, bool isAscending
140140
return $"{nameof(NgoModel.NumberOfNgoAdmins)} {sortOrder}";
141141
}
142142

143-
if (string.Equals(sortColumnName, nameof(NgoModel.NumberOfElectionsMonitoring),
143+
if (string.Equals(sortColumnName, nameof(NgoModel.NumberOfMonitoredElections),
144144
StringComparison.InvariantCultureIgnoreCase))
145145
{
146-
return $"{nameof(NgoModel.NumberOfElectionsMonitoring)} {sortOrder}";
146+
return $"{nameof(NgoModel.NumberOfMonitoredElections)} {sortOrder}";
147147
}
148148

149149
if (string.Equals(sortColumnName, nameof(NgoModel.DateOfLastElection),
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using Dapper;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Vote.Monitor.Core.Converters;
24

35
namespace Vote.Monitor.Api.Feature.Ngo;
46

57
public static class NgoFeatureInstaller
68
{
79
public static IServiceCollection AddNgoFeature(this IServiceCollection services)
810
{
11+
SqlMapper.AddTypeHandler(typeof(NgoModel.MonitoredElectionsModel[]),
12+
new JsonToObjectConverter<NgoModel.MonitoredElectionsModel[]>());
13+
914
return services;
1015
}
1116
}
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Vote.Monitor.Domain.Entities.NgoAggregate;
1+
using Vote.Monitor.Domain.Entities.ElectionRoundAggregate;
2+
using Vote.Monitor.Domain.Entities.NgoAggregate;
23

34
namespace Vote.Monitor.Api.Feature.Ngo;
45

@@ -8,6 +9,17 @@ public record NgoModel
89
public string Name { get; init; }
910
public NgoStatus Status { get; init; }
1011
public int NumberOfNgoAdmins { get; init; }
11-
public int NumberOfElectionsMonitoring { get; init; }
12+
public int NumberOfMonitoredElections { get; init; }
13+
14+
public MonitoredElectionsModel[] MonitoredElections { get; init; } = [];
1215
public DateOnly? DateOfLastElection { get; set; }
16+
17+
public class MonitoredElectionsModel
18+
{
19+
public Guid Id { get; set; }
20+
public string Title { get; set; }
21+
public string EnglishTitle { get; set; }
22+
public DateOnly StartDate { get; set; }
23+
public ElectionRoundStatus Status { get; set; }
24+
}
1325
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
2+
import { Separator } from '@/components/ui/separator';
3+
import { useTranslation } from 'react-i18next';
4+
5+
export interface AssignedFormTemplatesDashboardProps {
6+
electionRoundId: string;
7+
}
8+
9+
function AssignedFormTemplatesDashboard({ electionRoundId }: AssignedFormTemplatesDashboardProps) {
10+
const { t } = useTranslation();
11+
12+
return (
13+
<Card>
14+
<CardHeader className='flex gap-2 flex-column'>
15+
<div className='flex flex-row items-center justify-between'>
16+
<CardTitle className='text-2xl font-semibold leading-none tracking-tight'>
17+
{t('electionEvent.formTemplates.cardTitle')}
18+
</CardTitle>
19+
</div>
20+
<Separator />
21+
</CardHeader>
22+
<CardContent className='flex flex-col items-baseline gap-6'>{electionRoundId}</CardContent>
23+
</Card>
24+
);
25+
}
26+
27+
export default AssignedFormTemplatesDashboard;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
2+
import { Separator } from '@/components/ui/separator';
3+
import { useTranslation } from 'react-i18next';
4+
5+
export interface CitizenReportingDashboardProps {
6+
electionRoundId: string;
7+
}
8+
9+
function CitizenReportingDashboard({ electionRoundId }: CitizenReportingDashboardProps) {
10+
const { t } = useTranslation();
11+
12+
return (
13+
<Card>
14+
<CardHeader className='flex gap-2 flex-column'>
15+
<div className='flex flex-row items-center justify-between'>
16+
<CardTitle className='text-2xl font-semibold leading-none tracking-tight'>
17+
{t('electionEvent.citizenReporting.cardTitle')}
18+
</CardTitle>
19+
</div>
20+
<Separator />
21+
</CardHeader>
22+
<CardContent className='flex flex-col items-baseline gap-6'>{electionRoundId}</CardContent>
23+
</Card>
24+
);
25+
}
26+
27+
export default CitizenReportingDashboard;

web/src/features/election-rounds/components/ElectionRoundDetails/ElectionRoundDetails.tsx

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { useTranslation } from 'react-i18next';
1010
import { electionRoundDetailsQueryOptions } from '../../queries';
1111
import PollingStationsDashboard from '@/components/PollingStationsDashboard/PollingStationsDashboard';
1212
import { useNavigate } from '@tanstack/react-router';
13-
13+
import PsiFormDashboard from '../PsiFormDashboard/PsiFormDashboard';
14+
import MonitoringNgosDashboard from '../MonitoringNgosDashboard/MonitoringNgosDashboard';
15+
import CitizenReportingDashboard from '../CitizenReportingDashboard/CitizenReportingDashboard';
16+
import AssignedFormTemplatesDashboard from '../AssignedFormTemplatesDashboard/AssignedFormTemplatesDashboard';
17+
import { ElectionRoundDetailsTab } from './tabs';
1418
function ElectionRoundDetails() {
1519
const navigate = useNavigate();
1620
const { electionRoundId, tab } = Route.useParams();
@@ -26,34 +30,64 @@ function ElectionRoundDetails() {
2630
return (
2731
<Layout title={electionRound.title} subtitle={electionRound.englishTitle} enableBreadcrumbs={false}>
2832
<Tabs
29-
defaultValue={tab ?? 'event-details'}
33+
defaultValue={tab ?? ElectionRoundDetailsTab.EventDetails}
3034
onValueChange={(tab) => {
3135
navigate({
3236
to: '/election-rounds/$electionRoundId/$tab',
3337
params: { electionRoundId, tab },
34-
replace: true,
3538
});
3639
}}>
37-
<TabsList className='grid grid-cols-4 bg-gray-200 w-[800px]'>
38-
<TabsTrigger value='event-details'>{t('electionEvent.eventDetails.tabTitle')}</TabsTrigger>
39-
<TabsTrigger value='polling-stations'>{t('electionEvent.pollingStations.tabTitle')}</TabsTrigger>
40-
<TabsTrigger value='locations'>{t('electionEvent.locations.tabTitle')}</TabsTrigger>
41-
<TabsTrigger value='form-templates'>{t('electionEvent.formTemplates.tabTitle')}</TabsTrigger>
40+
<TabsList className='grid grid-cols-7 bg-gray-200'>
41+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.EventDetails}>
42+
{t('electionEvent.eventDetails.tabTitle')}
43+
</TabsTrigger>
44+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.PsiForm}>
45+
{t('electionEvent.psiForm.tabTitle')}
46+
</TabsTrigger>
47+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.PollingStations}>
48+
{t('electionEvent.pollingStations.tabTitle')}
49+
</TabsTrigger>
50+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.MonitoringNgos}>
51+
{t('electionEvent.monitoringNgos.tabTitle')}
52+
</TabsTrigger>
53+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.CitizenReporting}>
54+
{t('electionEvent.citizenReporting.tabTitle')}
55+
</TabsTrigger>
56+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.Locations}>
57+
{t('electionEvent.locations.tabTitle')}
58+
</TabsTrigger>
59+
<TabsTrigger className='whitespace-normal text-wrap' value={ElectionRoundDetailsTab.FormTemplates}>
60+
{t('electionEvent.formTemplates.tabTitle')}
61+
</TabsTrigger>
4262
</TabsList>
4363

44-
<TabsContent value='event-details'>
64+
<TabsContent value={ElectionRoundDetailsTab.EventDetails}>
4565
<ElectionEventDescription />
4666
</TabsContent>
4767

48-
<TabsContent value='polling-stations'>
68+
<TabsContent value={ElectionRoundDetailsTab.PsiForm}>
69+
<PsiFormDashboard electionRoundId={electionRoundId} />
70+
</TabsContent>
71+
72+
<TabsContent value={ElectionRoundDetailsTab.PollingStations}>
4973
<PollingStationsDashboard />
5074
</TabsContent>
5175

52-
<TabsContent value='locations'>
76+
<TabsContent value={ElectionRoundDetailsTab.MonitoringNgos}>
77+
<MonitoringNgosDashboard electionRoundId={electionRoundId} />
78+
</TabsContent>
79+
80+
<TabsContent value={ElectionRoundDetailsTab.CitizenReporting}>
81+
<CitizenReportingDashboard electionRoundId={electionRoundId} />
82+
</TabsContent>
83+
84+
<TabsContent value={ElectionRoundDetailsTab.Locations}>
5385
<LocationsDashboard />
5486
</TabsContent>
5587

56-
<TabsContent value='form-templates'>{/* <FormTemplatesAssign /> */}</TabsContent>
88+
<TabsContent value={ElectionRoundDetailsTab.FormTemplates}>
89+
<AssignedFormTemplatesDashboard electionRoundId={electionRoundId} />
90+
</TabsContent>
5791
</Tabs>
5892
</Layout>
5993
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export enum ElectionRoundDetailsTab {
2+
EventDetails = 'event-details',
3+
PsiForm = 'psi-form',
4+
PollingStations = 'polling-stations',
5+
MonitoringNgos = 'monitoring-ngos',
6+
CitizenReporting = 'citizen-reporting',
7+
Locations = 'locations',
8+
FormTemplates = 'form-templates',
9+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
2+
import { Separator } from '@/components/ui/separator';
3+
import { useTranslation } from 'react-i18next';
4+
5+
export interface MonitoringNgosDashboardProps {
6+
electionRoundId: string;
7+
}
8+
function MonitoringNgosDashboard({ electionRoundId }: MonitoringNgosDashboardProps) {
9+
const { t } = useTranslation();
10+
11+
return (
12+
<Card>
13+
<CardHeader className='flex gap-2 flex-column'>
14+
<div className='flex flex-row items-center justify-between'>
15+
<CardTitle className='text-2xl font-semibold leading-none tracking-tight'>
16+
{t('electionEvent.monitoringNgos.cardTitle')}
17+
</CardTitle>
18+
</div>
19+
<Separator />
20+
</CardHeader>
21+
<CardContent className='flex flex-col items-baseline gap-6'>{electionRoundId}</CardContent>
22+
</Card>
23+
);
24+
}
25+
26+
export default MonitoringNgosDashboard;

0 commit comments

Comments
 (0)