diff --git a/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs b/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs index 34bb25910..9311409a9 100644 --- a/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs +++ b/api/src/Feature.MonitoringObservers/Parser/MonitoringObserverImportModel.cs @@ -2,11 +2,11 @@ public class MonitoringObserverImportModel { - public required string Email { get; set; } - public required string FirstName { get; set; } - public required string LastName { get; set; } - public required string PhoneNumber { get; set; } - public required string[] Tags { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string PhoneNumber { get; set; } + public string[] Tags { get; set; } public override int GetHashCode() { diff --git a/web/src/features/monitoring-observers/components/CreateMonitoringObserver.tsx b/web/src/features/monitoring-observers/components/CreateMonitoringObserver.tsx new file mode 100644 index 000000000..382dee6e4 --- /dev/null +++ b/web/src/features/monitoring-observers/components/CreateMonitoringObserver.tsx @@ -0,0 +1,174 @@ +import { authApi } from '@/common/auth-api'; +import Layout from '@/components/layout/Layout'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import TagsSelectFormField from '@/components/ui/tag-selector'; +import { toast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useMonitoringObserversTags } from '@/hooks/tags-queries'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { MonitorObserverBackButton } from './MonitoringObserverBackButton'; + +export default function CreateMonitoringObserver() { + const { t } = useTranslation('translation', { keyPrefix: 'observers.addObserver' }); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + const { data: availableTags } = useMonitoringObserversTags(currentElectionRoundId); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const newObserverSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + phoneNumber: z.string(), + tags: z.any(), + }); + + type ObserverFormData = z.infer; + + const form = useForm({ + resolver: zodResolver(newObserverSchema), + }); + + function onSubmit(values: ObserverFormData) { + newObserverMutation.mutate({ electionRoundId: currentElectionRoundId, values }); + } + + const newObserverMutation = useMutation({ + mutationFn: ({ electionRoundId, values }: { electionRoundId: string; values: ObserverFormData }) => { + return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers`, { observers: [values] }); + }, + + onSuccess: () => { + toast({ + title: 'Success', + description: t('onSuccess'), + }); + + queryClient.invalidateQueries({ queryKey: ['monitoring-observers', currentElectionRoundId] }); + + navigate({ + to: '/monitoring-observers/$tab', + params: { tab: 'list' }, + }); + }, + onError: () => { + toast({ + title: t('onError'), + description: 'Please contact tech support', + variant: 'destructive', + }); + }, + }); + return ( + } enableBreadcrumbs={false}> + + +
+ {t('title')} +
+ +
+ +
+ + ( + + {t('firstName')} + + + + )} + /> + + ( + + {t('lastName')} + + + + )} + /> + + ( + + {t('email')} + + + + )} + /> + + ( + + {t('phone')} + + + + )} + /> + + ( + + Tags + + !field.value?.includes(tag)) ?? []} + defaultValue={field.value} + onValueChange={field.onChange} + placeholder='Observer tags' + /> + + + + )} + /> + +
+
+ + + +
+
+ + +
+
+
+ ); +} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index 88e31c0f9..ad482c792 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -17,7 +17,7 @@ import { Separator } from '@/components/ui/separator'; import { useDialog } from '@/components/ui/use-dialog'; import { Cog8ToothIcon, EllipsisVerticalIcon, FunnelIcon, PaperAirplaneIcon } from '@heroicons/react/24/outline'; import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; -import { useNavigate, useRouter, useSearch } from '@tanstack/react-router'; +import { Link, useNavigate, useRouter, useSearch } from '@tanstack/react-router'; import { CellContext, ColumnDef } from '@tanstack/react-table'; import { useState } from 'react'; @@ -27,9 +27,11 @@ import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import i18n from '@/i18n'; import { isQueryFiltered } from '@/lib/utils'; import { queryClient } from '@/main'; import { format } from 'date-fns'; +import { Plus } from 'lucide-react'; import { MonitoringObserversListFilters } from '../../filtering/MonitoringObserversListFilters'; import { MonitoringObserver, MonitoringObserverStatus } from '../../models/monitoring-observer'; import ImportMonitoringObserversDialog from '../MonitoringObserversList/ImportMonitoringObserversDialog'; @@ -173,9 +175,7 @@ function MonitoringObserversList() { }; const tagString = - pageParams.tags == undefined - ? '' - : pageParams.tags?.map((n: string) => `tags=${n}`).join('&'); + pageParams.tags == undefined ? '' : pageParams.tags?.map((n: string) => `tags=${n}`).join('&'); const response = await authApi.get>( `/election-rounds/${currentElectionRoundId}/monitoring-observers?${tagString ?? ''}`, @@ -276,6 +276,13 @@ function MonitoringObserversList() {
Monitoring observers list
+ + + + {!!importErrorsFileId && ( rootRoute, } as any) +const MonitoringObserversNewObserverRoute = + MonitoringObserversNewObserverImport.update({ + path: '/monitoring-observers/new-observer', + getParentRoute: () => rootRoute, + } as any) + const MonitoringObserversCreateNewMessageRoute = MonitoringObserversCreateNewMessageImport.update({ path: '/monitoring-observers/create-new-message', @@ -296,6 +303,10 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MonitoringObserversCreateNewMessageImport parentRoute: typeof rootRoute } + '/monitoring-observers/new-observer': { + preLoaderRoute: typeof MonitoringObserversNewObserverImport + parentRoute: typeof rootRoute + } '/ngos/$ngoId': { preLoaderRoute: typeof NgosNgoIdImport parentRoute: typeof rootRoute @@ -434,6 +445,7 @@ export const routeTree = rootRoute.addChildren([ FormsFormIdRoute, MonitoringObserversTabRoute, MonitoringObserversCreateNewMessageRoute, + MonitoringObserversNewObserverRoute, NgosNgoIdRoute, ObserverGuidesNewRoute, ObserversObserverIdRoute, diff --git a/web/src/routes/monitoring-observers/new-observer.tsx b/web/src/routes/monitoring-observers/new-observer.tsx new file mode 100644 index 000000000..0eb5c8d6e --- /dev/null +++ b/web/src/routes/monitoring-observers/new-observer.tsx @@ -0,0 +1,14 @@ +import CreateMonitoringObserver from '@/features/monitoring-observers/components/CreateMonitoringObserver'; +import { redirectIfNotAuth } from '@/lib/utils'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/monitoring-observers/new-observer')({ + beforeLoad: () => { + redirectIfNotAuth(); + }, + component: CreateNewObserver, +}); + +function CreateNewObserver() { + return ; +}