Skip to content

Commit e5f926b

Browse files
authored
Implement elections round management (#906)
1 parent 163c129 commit e5f926b

File tree

8 files changed

+469
-52
lines changed

8 files changed

+469
-52
lines changed

web/src/components/ElectionEventDescription/ElectionEventDescription.tsx

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,144 @@ import { Separator } from '@/components/ui/separator';
44
import { ElectionRoundStatus } from '@/common/types';
55
import { AuthContext } from '@/context/auth.context';
66
import { useCurrentElectionRoundStore } from '@/context/election-round.store';
7+
import {
8+
useArchiveElectionRound,
9+
useStartElectionRound,
10+
useUnarchiveElectionRound,
11+
useUnstartElectionRound,
12+
} from '@/features/election-rounds/hooks';
713
import { PencilIcon } from '@heroicons/react/24/outline';
8-
import { Link } from '@tanstack/react-router';
9-
import { useContext } from 'react';
14+
import { Link, useRouter } from '@tanstack/react-router';
15+
import { ArchiveIcon, FileEdit, PlayIcon } from 'lucide-react';
16+
import { useCallback, useContext } from 'react';
1017
import { useTranslation } from 'react-i18next';
1118
import { useElectionRoundDetails } from '../../features/election-event/hooks/election-event-hooks';
19+
import CoalitionDescription from '../CoalitionDescription/CoalitionDescription';
1220
import ElectionRoundStatusBadge from '../ElectionRoundStatusBadge/ElectionRoundStatusBadge';
21+
import { useConfirm } from '../ui/alert-dialog-provider';
1322
import { Button } from '../ui/button';
14-
import CoalitionDescription from '../CoalitionDescription/CoalitionDescription';
23+
import { useToast } from '../ui/use-toast';
1524

1625
export default function ElectionEventDescription() {
1726
const { t } = useTranslation();
1827
const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId);
1928
const { data: electionEvent } = useElectionRoundDetails(currentElectionRoundId);
2029
const { userRole } = useContext(AuthContext);
2130

31+
const { toast } = useToast();
32+
const confirm = useConfirm();
33+
34+
const router = useRouter();
35+
36+
const { mutate: unstartElectionRound } = useUnstartElectionRound();
37+
const { mutate: startElectionRound } = useStartElectionRound();
38+
const { mutate: archiveElectionRound } = useArchiveElectionRound();
39+
const { mutate: unarchiveElectionRound } = useUnarchiveElectionRound();
40+
41+
const handleArchiveElectionRound = useCallback(async () => {
42+
if (
43+
await confirm({
44+
title: `Archive election round: ${electionEvent!.englishTitle}?`,
45+
body: 'Are you sure you want to archive this election round?',
46+
})
47+
) {
48+
archiveElectionRound({
49+
electionRoundId: electionEvent!.id,
50+
onSuccess: () => {
51+
router.invalidate();
52+
53+
toast({
54+
title: 'Election round archived successfully',
55+
});
56+
},
57+
onError: () =>
58+
toast({
59+
title: 'Error archiving election round',
60+
description: 'Please contact tech support',
61+
variant: 'destructive',
62+
}),
63+
});
64+
}
65+
}, [electionEvent, confirm]);
66+
67+
const handleUnstartElectionRound = useCallback(async () => {
68+
if (
69+
await confirm({
70+
title: `Draft election round: ${electionEvent!.englishTitle}?`,
71+
body: 'Are you sure you want to draft this election round?',
72+
})
73+
) {
74+
unstartElectionRound({
75+
electionRoundId: electionEvent!.id,
76+
onSuccess: () => {
77+
router.invalidate();
78+
79+
toast({
80+
title: 'Election round drafted successfully',
81+
});
82+
},
83+
onError: () => {
84+
router.invalidate();
85+
toast({
86+
title: 'Error drafting election round',
87+
description: 'Please contact tech support',
88+
variant: 'destructive',
89+
});
90+
},
91+
});
92+
}
93+
}, [electionEvent, confirm]);
94+
95+
const handleUnarchiveElectionRound = useCallback(async () => {
96+
if (
97+
await confirm({
98+
title: `Unarchive election round: ${electionEvent!.englishTitle}?`,
99+
body: 'Are you sure you want to unarchive this election round?',
100+
})
101+
) {
102+
unarchiveElectionRound({
103+
electionRoundId: electionEvent!.id,
104+
onSuccess: () => {
105+
router.invalidate();
106+
toast({
107+
title: 'Election round unarchived successfully',
108+
});
109+
},
110+
onError: () =>
111+
toast({
112+
title: 'Error unarchiving election round',
113+
description: 'Please contact tech support',
114+
variant: 'destructive',
115+
}),
116+
});
117+
}
118+
}, [electionEvent, confirm]);
119+
120+
const handleStartElectionRound = useCallback(async () => {
121+
if (
122+
await confirm({
123+
title: `Start election round: ${electionEvent!.englishTitle}?`,
124+
body: 'Are you sure you want to start this election round?',
125+
})
126+
) {
127+
startElectionRound({
128+
electionRoundId: electionEvent!.id,
129+
onSuccess: () => {
130+
router.invalidate();
131+
toast({
132+
title: 'Election round started successfully',
133+
});
134+
},
135+
onError: () =>
136+
toast({
137+
title: 'Error starting election round',
138+
description: 'Please contact tech support',
139+
variant: 'destructive',
140+
}),
141+
});
142+
}
143+
}, [electionEvent, confirm]);
144+
22145
return (
23146
<div className='space-y-4'>
24147
<Card>
@@ -29,9 +152,32 @@ export default function ElectionEventDescription() {
29152
</CardTitle>
30153
{userRole === 'PlatformAdmin' && (
31154
<div className='flex justify-end gap-4 px-6'>
155+
{!(electionEvent!.status === ElectionRoundStatus.Archived) ? (
156+
<Button onClick={handleArchiveElectionRound} variant='ghost-primary' className='text-yellow-900'>
157+
<ArchiveIcon className='mr-2 h-4 w-4' />
158+
Archive
159+
</Button>
160+
) : (
161+
<Button onClick={handleUnarchiveElectionRound} variant='ghost-primary' className='text-green-900'>
162+
<ArchiveIcon className='mr-2 h-4 w-4' />
163+
Unarchive
164+
</Button>
165+
)}
166+
167+
{!(electionEvent!.status === ElectionRoundStatus.Started) ? (
168+
<Button onClick={handleStartElectionRound} variant='ghost-primary' className='text-green-900'>
169+
<PlayIcon className='mr-2 h-4 w-4' />
170+
Start
171+
</Button>
172+
) : (
173+
<Button onClick={handleUnstartElectionRound} variant='ghost-primary' className='text-slate-700'>
174+
<FileEdit className='mr-2 h-4 w-4' />
175+
Draft
176+
</Button>
177+
)}
32178
<Link to='/election-rounds/$electionRoundId/edit' params={{ electionRoundId: currentElectionRoundId }}>
33179
<Button variant='ghost-primary'>
34-
<PencilIcon className='w-[18px] mr-2 text-purple-900' />
180+
<PencilIcon className='h-4 w-4 mr-2 text-purple-900' />
35181
<span className='text-base text-purple-900'>Edit</span>
36182
</Button>
37183
</Link>

web/src/components/PollingStationsDashboard/CreatePollingStationDialog.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ function CreatePollingStationDialog({ open, onOpenChange }: CreatePollingStation
3030

3131
const newPollingStationMutation = useMutation({
3232
mutationFn: ({ electionRoundId, values }: { electionRoundId: string; values: ImportPollingStationRow }) => {
33-
console.log(values);
3433
return authApi.post(`/election-rounds/${electionRoundId}/polling-stations`, values);
3534
},
3635

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import ElectionsRoundFilter from './ElectionsRoundFilter';
1919

2020
export default function ElectionRoundsDashboard(): ReactElement {
2121
const search = Route.useSearch();
22-
const navigate = Route.useNavigate();
2322

2423
const { filteringIsActive, navigateHandler } = useFilteringContainer();
2524
const [filtersExpanded, setFiltersExpanded] = useState<boolean>(false);
@@ -30,7 +29,7 @@ export default function ElectionRoundsDashboard(): ReactElement {
3029
const params: ElectionsRoundsQueryParams = {
3130
countryId: search.countryId,
3231
electionRoundStatus: search.electionRoundStatus,
33-
searchText: search.searchText,
32+
searchText: search.searchText,
3433
};
3534

3635
return params;
@@ -77,14 +76,7 @@ export default function ElectionRoundsDashboard(): ReactElement {
7776
{filtersExpanded && <ElectionsRoundFilter />}
7877
</CardHeader>
7978
<CardContent>
80-
<QueryParamsDataTable
81-
columns={electionRoundColDefs}
82-
useQuery={useElectionRounds}
83-
queryParams={queryParams}
84-
onRowClick={(electionRoundId: string) =>
85-
navigate({ to: `/election-rounds/$electionRoundId`, params: { electionRoundId } })
86-
}
87-
/>
79+
<QueryParamsDataTable columns={electionRoundColDefs} useQuery={useElectionRounds} queryParams={queryParams} />
8880
</CardContent>
8981
</Card>
9082
{createElectionEventDialog.dialogProps.open && (

0 commit comments

Comments
 (0)