Skip to content

Commit 6238959

Browse files
authored
[PlatformAdmin] Add feature to import individual polling station (#881)
* fix: make dropdown menus scrollable * fix: truncate overflowing table columns * Squashed commit of the following: commit 742f250 Author: imdeaconu <imdeaconu@gmail.com> Date: Wed Sep 11 19:54:55 2024 +0300 add read notification checkmark commit ea11fa0 Author: imdeaconu <imdeaconu@gmail.com> Date: Wed Sep 11 19:54:30 2024 +0300 add read notification column * Squashed commit of the following: commit d8833dc Author: imdeaconu <imdeaconu@gmail.com> Date: Fri Sep 13 13:29:31 2024 +0300 WIP: add selector functionality commit 3608c0e Author: imdeaconu <imdeaconu@gmail.com> Date: Fri Sep 13 10:00:05 2024 +0300 WIP: create new tags input * chore: remove unused import * chore: delete duplicated / unused classes * feature: add searching to MonitoringObserversTagFilter * chore: update config files * Revert "[NGO Admin] Rewrite the tag selector component (#675)" This reverts commit 2ad0e90. * Merge branch 'main' of https://github.com/commitglobal/votemonitor into commitglobal-main * WIP: rename prop for alternative filter key in Observer Tags and add it to the Push Message form * WIP: fix push messages receipients query not invalidating after edits * invalidate targeted observers query after a morning observer is added * add a modal for creating an individual polling station * remove console log * delete unused variables
1 parent 728f68b commit 6238959

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { authApi } from '@/common/auth-api';
2+
import { importPollingStationSchema } from '@/common/types';
3+
import { Button } from '@/components/ui/button';
4+
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog';
5+
import { Form, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
6+
import { Input } from '@/components/ui/input';
7+
import { toast } from '@/components/ui/use-toast';
8+
import { useCurrentElectionRoundStore } from '@/context/election-round.store';
9+
import { ImportPollingStationRow } from '@/features/polling-stations/PollingStationsImport/PollingStationsImport';
10+
import { pollingStationsKeys } from '@/hooks/polling-stations-levels';
11+
import { zodResolver } from '@hookform/resolvers/zod';
12+
import { useMutation, useQueryClient } from '@tanstack/react-query';
13+
import { useForm } from 'react-hook-form';
14+
import { useTranslation } from 'react-i18next';
15+
16+
export interface CreatePollingStationDialogProps {
17+
open: boolean;
18+
onOpenChange: (open: any) => void;
19+
}
20+
21+
function CreatePollingStationDialog({ open, onOpenChange }: CreatePollingStationDialogProps) {
22+
const { t } = useTranslation('translation', { keyPrefix: 'electionEvent.pollingStations' });
23+
const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId);
24+
const queryClient = useQueryClient();
25+
26+
const form = useForm<ImportPollingStationRow>({
27+
mode: 'all',
28+
resolver: zodResolver(importPollingStationSchema),
29+
});
30+
31+
const newPollingStationMutation = useMutation({
32+
mutationFn: ({ electionRoundId, values }: { electionRoundId: string; values: ImportPollingStationRow }) => {
33+
console.log(values);
34+
return authApi.post(`/election-rounds/${electionRoundId}/polling-stations`, values);
35+
},
36+
37+
onSuccess: (_, { electionRoundId }) => {
38+
toast({
39+
title: 'Success',
40+
description: t('addPollingStation.onSuccess'),
41+
});
42+
43+
queryClient.invalidateQueries({ queryKey: pollingStationsKeys.all(electionRoundId) });
44+
45+
form.reset({});
46+
onOpenChange(false);
47+
},
48+
onError: (err) => {
49+
toast({
50+
title: t('addPollingStation.onError'),
51+
description: 'Please contact tech support',
52+
variant: 'destructive',
53+
});
54+
},
55+
});
56+
57+
function onSubmit(values: ImportPollingStationRow) {
58+
newPollingStationMutation.mutate({ electionRoundId: currentElectionRoundId, values });
59+
}
60+
61+
return (
62+
<Dialog open={open} onOpenChange={onOpenChange} modal={true}>
63+
<DialogContent
64+
className='min-w-[650px] min-h-[350px]'
65+
onInteractOutside={(e) => {
66+
e.preventDefault();
67+
}}
68+
onEscapeKeyDown={(e) => {
69+
e.preventDefault();
70+
}}>
71+
<DialogTitle className='mb-3.5'>{t('addPollingStation.title')}</DialogTitle>
72+
<div className='flex flex-col gap-3'>
73+
<Form {...form}>
74+
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
75+
<FormField
76+
control={form.control}
77+
name='level1'
78+
render={({ field, fieldState }) => (
79+
<FormItem>
80+
<FormLabel>{t('headers.level1')}</FormLabel>
81+
<Input placeholder={t('headers.level1')} {...field} {...fieldState} />
82+
<FormMessage />
83+
</FormItem>
84+
)}
85+
/>
86+
87+
<FormField
88+
control={form.control}
89+
name='level2'
90+
render={({ field, fieldState }) => (
91+
<FormItem>
92+
<FormLabel>{t('headers.level2')}</FormLabel>
93+
<Input placeholder={t('headers.level2')} {...field} {...fieldState} />
94+
<FormMessage />
95+
</FormItem>
96+
)}
97+
/>
98+
99+
<FormField
100+
control={form.control}
101+
name='level3'
102+
render={({ field, fieldState }) => (
103+
<FormItem>
104+
<FormLabel>{t('headers.level3')}</FormLabel>
105+
<Input placeholder={t('headers.level3')} {...field} {...fieldState} />
106+
<FormMessage />
107+
</FormItem>
108+
)}
109+
/>
110+
<FormField
111+
control={form.control}
112+
name='level4'
113+
render={({ field, fieldState }) => (
114+
<FormItem>
115+
<FormLabel>{t('headers.level4')}</FormLabel>
116+
<Input placeholder={t('headers.level4')} {...field} {...fieldState} />
117+
<FormMessage />
118+
</FormItem>
119+
)}
120+
/>
121+
<FormField
122+
control={form.control}
123+
name='level5'
124+
render={({ field, fieldState }) => (
125+
<FormItem>
126+
<FormLabel>{t('headers.level5')}</FormLabel>
127+
<Input placeholder={t('headers.level5')} {...field} {...fieldState} />
128+
<FormMessage />
129+
</FormItem>
130+
)}
131+
/>
132+
133+
<FormField
134+
control={form.control}
135+
name='number'
136+
render={({ field, fieldState }) => (
137+
<FormItem>
138+
<FormLabel>{t('headers.number')}</FormLabel>
139+
<Input placeholder={t('headers.number')} {...field} {...fieldState} />
140+
<FormMessage />
141+
</FormItem>
142+
)}
143+
/>
144+
<FormField
145+
control={form.control}
146+
name='address'
147+
render={({ field, fieldState }) => (
148+
<FormItem>
149+
<FormLabel>{t('headers.address')}</FormLabel>
150+
<Input placeholder={t('headers.address')} {...field} {...fieldState} />
151+
<FormMessage />
152+
</FormItem>
153+
)}
154+
/>
155+
<FormField
156+
control={form.control}
157+
name='displayOrder'
158+
render={({ field, fieldState }) => (
159+
<FormItem>
160+
<FormLabel>{t('headers.displayOrder')}</FormLabel>
161+
<Input placeholder={t('headers.displayOrder')} {...field} {...fieldState} />
162+
<FormMessage />
163+
</FormItem>
164+
)}
165+
/>
166+
167+
<DialogFooter>
168+
<DialogClose asChild>
169+
<Button className='text-purple-900 border border-purple-900 border-input bg-background hover:bg-purple-50 hover:text-purple-600'>
170+
Cancel
171+
</Button>
172+
</DialogClose>
173+
<Button title={t('addPollingStation.addBtnText')} type='submit' className='px-6'>
174+
{t('addPollingStation.addBtnText')}
175+
</Button>
176+
</DialogFooter>
177+
</form>
178+
</Form>
179+
</div>
180+
</DialogContent>
181+
</Dialog>
182+
);
183+
}
184+
185+
export default CreatePollingStationDialog;

web/src/components/PollingStationsDashboard/PollingStationsDashboard.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ import { queryClient } from '@/main';
1818
import { ArrowUpTrayIcon, FunnelIcon } from '@heroicons/react/24/outline';
1919
import { Link, useNavigate, useRouter, useSearch } from '@tanstack/react-router';
2020
import { useDebounce } from '@uidotdev/usehooks';
21+
import { Plus } from 'lucide-react';
2122
import { useCallback, useContext, useMemo, useState, type ReactElement } from 'react';
2223
import { PollingStationDataTableRowActions } from '../PollingStationDataTableRowActions/PollingStationDataTableRowActions';
24+
import { useDialog } from '../ui/use-dialog';
2325
import { useToast } from '../ui/use-toast';
2426
import { pollingStationColDefs } from './column-defs';
27+
import CreatePollingStationDialog from './CreatePollingStationDialog';
2528
import { useDeletePollingStationMutation, usePollingStations, useUpdatePollingStationMutation } from './hooks';
2629

2730
export default function PollingStationsDashboard(): ReactElement {
@@ -33,6 +36,7 @@ export default function PollingStationsDashboard(): ReactElement {
3336
const router = useRouter();
3437
const { mutate: deletePollingStationMutation } = useDeletePollingStationMutation();
3538
const { mutate: updatePollingStationMutation } = useUpdatePollingStationMutation();
39+
const createPollingStationDialog = useDialog();
3640

3741
const deletePollingStationCallback = useCallback(
3842
(pollingStation: PollingStation) =>
@@ -162,6 +166,19 @@ export default function PollingStationsDashboard(): ReactElement {
162166

163167
<div className='flex items-center gap-4'>
164168
<ExportDataButton exportedDataType={ExportedDataType.PollingStations} />
169+
{userRole === 'PlatformAdmin' && (
170+
<>
171+
<Button
172+
variant='secondary'
173+
disabled={electionRound?.status === ElectionRoundStatus.Archived}
174+
onClick={() => createPollingStationDialog.trigger()}>
175+
<Plus className='mr-2' width={18} height={18} />
176+
{i18n.t('electionEvent.pollingStations.addPollingStation.addBtnText')}
177+
</Button>
178+
<CreatePollingStationDialog {...createPollingStationDialog.dialogProps} />
179+
</>
180+
)}
181+
165182
{userRole === 'PlatformAdmin' && (
166183
<Link
167184
to={'/election-rounds/$electionRoundId/polling-stations/import'}

0 commit comments

Comments
 (0)