diff --git a/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx b/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx new file mode 100644 index 000000000..160a62fc0 --- /dev/null +++ b/web/src/features/election-rounds/components/MonitoringNgosDashboard/AddMonitoringNgoDialog.tsx @@ -0,0 +1,110 @@ +import { authApi } from '@/common/auth-api'; +import { Button } from '@/components/ui/button'; +import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; +import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { toast } from '@/components/ui/use-toast'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { ColumnDef } from '@tanstack/react-table'; +import { useDebounce } from '@uidotdev/usehooks'; +import { Plus } from 'lucide-react'; +import { ChangeEvent, useMemo, useState } from 'react'; +import { MonitoringNgoModel } from '../../models/types'; +import { monitoringNgoKeys, useAvailableMonitoringNgos } from './queries'; + +export interface AddMonitoringNgoDialogProps { + electionRoundId: string; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const useMonitoringNgoSearch = () => { + const [searchText, setSearchText] = useState(''); + const debouncedSearchText = useDebounce(searchText, 300); + + const handleSearchInput = (ev: ChangeEvent): void => { + setSearchText(ev.currentTarget.value); + }; + + const queryParams = useMemo(() => { + const params = [['searchText', debouncedSearchText]].filter(([_, value]) => value); + + return Object.fromEntries(params); + }, [debouncedSearchText]); + + return { searchText, queryParams, handleSearchInput }; +}; + +function AddMonitoringNgoDialog({ open, onOpenChange, electionRoundId }: AddMonitoringNgoDialogProps) { + const { searchText, handleSearchInput, queryParams } = useMonitoringNgoSearch(); + const queryClient = useQueryClient(); + const addMonitoringNgoMutation = useMutation({ + mutationFn: async (ngoId: string) => { + return await authApi.post(`election-rounds/${electionRoundId}/monitoring-ngos`, { ngoId }); + }, + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: monitoringNgoKeys.all(electionRoundId) }); + onOpenChange(false); + toast({ + title: 'Success', + description: 'Added monitoring NGO', + }); + }, + //TODO Add error handling + }); + + const monitoringNgosColDefs: ColumnDef[] = [ + { + accessorKey: 'name', + enableSorting: true, + header: ({ column }) => , + }, + + { + id: 'actions', + cell: ({ row }) => { + const ngoId = row.original.id; + + return ( + + ); + }, + }, + ]; + + return ( + + { + e.preventDefault(); + }} + onEscapeKeyDown={(e) => { + e.preventDefault(); + }}> + + Add monitoring NGO + + +
+ useAvailableMonitoringNgos(electionRoundId, params)} + queryParams={queryParams} + /> +
+
+
+ ); +} + +export default AddMonitoringNgoDialog; diff --git a/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx b/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx index 4d8edce80..65f5b2990 100644 --- a/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx +++ b/web/src/features/election-rounds/components/MonitoringNgosDashboard/MonitoringNgosDashboard.tsx @@ -1,25 +1,194 @@ +import { authApi } from '@/common/auth-api'; +import type { FunctionComponent } from '@/common/types'; +import { useConfirm } from '@/components/ui/alert-dialog-provider'; +import { Button, buttonVariants } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { Separator } from '@/components/ui/separator'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { useDialog } from '@/components/ui/use-dialog'; +import { toast } from '@/components/ui/use-toast'; +import { NgoStatusBadge } from '@/features/ngos/components/NgoStatusBadges'; +import { EllipsisVerticalIcon } from '@heroicons/react/24/solid'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { ColumnDef } from '@tanstack/react-table'; +import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; +import { Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import { MonitoringNgoModel } from '../../models/types'; +import AddMonitoringNgoDialog from './AddMonitoringNgoDialog'; +import { monitoringNgoKeys, useMonitoringNgos } from './queries'; + +type MonitoringNgosTableProps = { + columns: ColumnDef[]; + data: MonitoringNgoModel[]; +}; + +function MonitoringNgosTable({ columns, data }: MonitoringNgosTableProps): FunctionComponent { + const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + const rows = table.getRowModel().rows; + + return ( + <> + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + + {rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ + ); +} export interface MonitoringNgosDashboardProps { electionRoundId: string; } function MonitoringNgosDashboard({ electionRoundId }: MonitoringNgosDashboardProps) { const { t } = useTranslation(); + const addMonitoringNgoDialog = useDialog(); + const { data } = useMonitoringNgos(electionRoundId); + const queryClient = useQueryClient(); + const confirm = useConfirm(); + const deleteMonitoringNgoMutation = useMutation({ + mutationFn: async (ngoId: string) => { + return await authApi.delete(`election-rounds/${electionRoundId}/monitoring-ngos/${ngoId}`); + }, + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: monitoringNgoKeys.all(electionRoundId) }); + toast({ + title: 'Success', + description: 'Removed monitoring NGO', + }); + }, + //TODO Add error handling + }); + + const monitoringNgosColDefs: ColumnDef[] = [ + { + accessorKey: 'name', + enableSorting: true, + header: ({ column }) => , + }, + + { + accessorKey: 'ngoStatus', + enableSorting: false, + header: ({ column }) => , + cell: ({ + row: { + original: { ngoStatus }, + }, + }) => { + return ; + }, + }, + { + id: 'actions', + cell: ({ row }) => { + const ngoId = row.original.ngoId; + + return ( +
+ + + + + + { + e.stopPropagation(); + + if ( + await confirm({ + title: `Delete ${row.original.name}?`, + body: 'This action is permanent and cannot be undone. Once deleted, this NGO admin cannot be retrieved.', + actionButton: 'Delete', + actionButtonClass: buttonVariants({ variant: 'destructive' }), + cancelButton: 'Cancel', + }) + ) { + deleteMonitoringNgoMutation.mutate(ngoId); + } + }}> + Delete + + + +
+ ); + }, + }, + ]; return ( - - -
- - {t('electionEvent.monitoringNgos.cardTitle')} - -
- -
- {electionRoundId} -
+ <> + + +
+ + {t('electionEvent.monitoringNgos.cardTitle')} + +
+ +
+
+ +
+ + + +
+ {addMonitoringNgoDialog.dialogProps.open && ( + + )} + ); } diff --git a/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts b/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts new file mode 100644 index 000000000..e6088020f --- /dev/null +++ b/web/src/features/election-rounds/components/MonitoringNgosDashboard/queries.ts @@ -0,0 +1,68 @@ +import { authApi } from '@/common/auth-api'; +import { DataTableParameters, ElectionRoundStatus, PageResponse } from '@/common/types'; +import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { MonitoringNgoModel } from '../../models/types'; +const STALE_TIME = 1000 * 60 * 15; // fifteen minutes + +export interface ElectionsRoundsQueryParams { + searchText: string | undefined; + countryId: string | undefined; + electionRoundStatus: ElectionRoundStatus | undefined; +} + +export const monitoringNgoKeys = { + all: (electionRoundId: string) => ['monitoringNgos', electionRoundId] as const, + availableForMonitoring: (electionRoundId: string, params: DataTableParameters) => + [...monitoringNgoKeys.all(electionRoundId), 'available', { ...params }] as const, +}; + +type MonitoringNgosPageResponse = { + monitoringNgos: MonitoringNgoModel[]; +}; + +export function useMonitoringNgos(electionRoundId: string): UseQueryResult { + return useQuery({ + queryKey: monitoringNgoKeys.all(electionRoundId), + placeholderData: { monitoringNgos: [] }, + queryFn: async () => { + const response = await authApi.get( + `election-rounds/${electionRoundId}/monitoring-ngos` + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch monitoring NGOs for election round'); + } + + return response.data; + }, + enabled: !!electionRoundId, + + staleTime: STALE_TIME, + }); +} + +export function useAvailableMonitoringNgos( + electionRoundId: string, + p: DataTableParameters +): UseQueryResult, Error> { + return useQuery({ + queryKey: monitoringNgoKeys.availableForMonitoring(electionRoundId, p), + queryFn: async () => { + const response = await authApi.get>( + `election-rounds/${electionRoundId}/monitoring-ngos:available`, + { + params: { + ...p.otherParams, + }, + } + ); + + if (response.status !== 200) { + throw new Error('Failed to fetch ngo admins'); + } + + return response.data; + }, + staleTime: STALE_TIME, + }); +} diff --git a/web/src/features/election-rounds/models/types.tsx b/web/src/features/election-rounds/models/types.tsx index 9bfe11a07..2851c6c03 100644 --- a/web/src/features/election-rounds/models/types.tsx +++ b/web/src/features/election-rounds/models/types.tsx @@ -4,7 +4,8 @@ import { NGOStatus } from '@/features/ngos/models/NGO'; export interface MonitoringNgoModel { id: string; name: string; - status: NGOStatus; + ngoId: string; + ngoStatus: NGOStatus; } export interface ElectionRoundModel {