Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
0fe3fbe
fix: make dropdown menus scrollable
imdeaconu Sep 6, 2024
4ca1a44
fix: truncate overflowing table columns
imdeaconu Sep 6, 2024
921aa13
Merge branch 'commitglobal:main' into main
imdeaconu Sep 6, 2024
e5a2869
Merge branch 'commitglobal:main' into main
imdeaconu Sep 6, 2024
7866a67
Merge branch 'commitglobal:main' into main
imdeaconu Sep 9, 2024
9ea6c42
Merge branch 'commitglobal:main' into main
imdeaconu Sep 10, 2024
1bd449d
Merge branch 'commitglobal:main' into main
imdeaconu Sep 11, 2024
c9874d1
Squashed commit of the following:
imdeaconu Sep 11, 2024
b7715f3
Merge branch 'commitglobal:main' into main
imdeaconu Sep 12, 2024
0facf65
Squashed commit of the following:
imdeaconu Sep 13, 2024
67f681d
chore: remove unused import
imdeaconu Sep 13, 2024
8d73252
chore: delete duplicated / unused classes
imdeaconu Sep 16, 2024
63b21b9
Merge branch 'commitglobal:main' into main
imdeaconu Sep 17, 2024
1892e6e
Merge branch 'commitglobal:main' into main
imdeaconu Sep 18, 2024
abb7c01
feature: add searching to MonitoringObserversTagFilter
imdeaconu Sep 19, 2024
9d0b8ae
Merge branch 'commitglobal:main' into main
imdeaconu Sep 20, 2024
c9fcd3e
chore: update config files
imdeaconu Sep 20, 2024
333ba49
Revert "[NGO Admin] Rewrite the tag selector component (#675)"
imdeaconu Sep 23, 2024
580b68e
Merge branch 'main' of https://github.com/commitglobal/votemonitor
imdeaconu Sep 23, 2024
ba2dad9
Merge branch 'commitglobal:main' into main
imdeaconu Sep 25, 2024
eea4faa
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Sep 26, 2024
29b8163
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Sep 26, 2024
68a44ee
Merge branch 'commitglobal-main'
imdeaconu Sep 26, 2024
7cf3244
Merge branch 'main' of https://github.com/commitglobal/votemonitor in…
imdeaconu Oct 1, 2024
b6abee7
Merge branch 'commitglobal-main-s1'
imdeaconu Oct 1, 2024
cc71856
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
e45ea22
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
50d15b6
Merge branch 'commitglobal:main' into main
imdeaconu Oct 2, 2024
1ed8e99
Merge branch 'commitglobal:main' into main
imdeaconu Oct 3, 2024
c2f1395
Merge branch 'commitglobal:main' into main
imdeaconu Oct 7, 2024
2c6d5f0
Merge branch 'commitglobal:main' into main
imdeaconu Oct 9, 2024
8c8e18f
Merge branch 'commitglobal:main' into main
imdeaconu Oct 9, 2024
db46a6d
Merge branch 'commitglobal:main' into main
imdeaconu Oct 10, 2024
d4b0263
Merge branch 'commitglobal:main' into main
imdeaconu Oct 12, 2024
79864cd
Merge branch 'commitglobal:main' into main
imdeaconu Oct 14, 2024
5ae7edf
Merge branch 'commitglobal:main' into main
imdeaconu Oct 15, 2024
c0fe98a
Merge branch 'commitglobal:main' into main
imdeaconu Oct 17, 2024
b7b3c5c
Merge branch 'commitglobal:main' into main
imdeaconu Oct 18, 2024
a892349
Merge branch 'commitglobal:main' into main
imdeaconu Oct 19, 2024
2d98793
Merge branch 'commitglobal:main' into main
imdeaconu Oct 20, 2024
66ba8d0
Merge branch 'commitglobal:main' into main
imdeaconu Oct 21, 2024
a86608d
Merge branch 'commitglobal:main' into main
imdeaconu Oct 22, 2024
aa1745c
Merge branch 'commitglobal:main' into main
imdeaconu Oct 23, 2024
9942029
Merge branch 'commitglobal:main' into main
imdeaconu Oct 23, 2024
d855c24
Merge branch 'commitglobal:main' into main
imdeaconu Oct 25, 2024
5a2b99d
Merge branch 'commitglobal:main' into main
imdeaconu Oct 28, 2024
e9ea9a3
Merge branch 'commitglobal:main' into main
imdeaconu Oct 29, 2024
fdaba4b
Merge branch 'commitglobal:main' into main
imdeaconu Oct 29, 2024
777ab43
Merge branch 'commitglobal:main' into main
imdeaconu Nov 4, 2024
15101d6
Merge branch 'commitglobal:main' into main
imdeaconu Nov 6, 2024
9289b11
Merge branch 'commitglobal:main' into main
imdeaconu Nov 28, 2024
a82ea16
Merge branch 'commitglobal:main' into main
imdeaconu Nov 30, 2024
914fc58
Merge branch 'commitglobal:main' into main
imdeaconu Dec 10, 2024
d9640c9
Merge branch 'commitglobal:main' into main
imdeaconu Jan 27, 2025
3e82dc6
Merge branch 'commitglobal:main' into main
imdeaconu Jan 27, 2025
315dfe9
Merge branch 'commitglobal:main' into main
imdeaconu Jan 29, 2025
6e96bf5
Merge branch 'commitglobal:main' into main
imdeaconu Feb 5, 2025
740f04b
WIP: rename prop for alternative filter key in Observer Tags and add …
imdeaconu Feb 14, 2025
fff3a05
WIP: fix push messages receipients query not invalidating after edits
imdeaconu Feb 14, 2025
a3771c5
invalidate targeted observers query after a morning observer is added
imdeaconu Feb 14, 2025
bd2e6bd
Merge remote-tracking branch 'upstream/main'
imdeaconu Feb 18, 2025
3592375
Merge branch 'commitglobal:main' into main
imdeaconu Feb 28, 2025
c580139
Merge branch 'commitglobal:main' into main
imdeaconu Mar 1, 2025
920bf92
Merge branch 'commitglobal:main' into main
imdeaconu Mar 6, 2025
2bf0ae4
Merge branch 'commitglobal:main' into main
imdeaconu Mar 10, 2025
d40e86b
Merge branch 'commitglobal:main' into main
imdeaconu Mar 10, 2025
f3f0c86
Merge branch 'commitglobal:main' into main
imdeaconu Mar 12, 2025
2836a0c
Merge branch 'commitglobal:main' into main
imdeaconu Mar 14, 2025
8e8fed4
Merge branch 'commitglobal:main' into main
imdeaconu Mar 18, 2025
63fd505
Merge branch 'commitglobal:main' into main
imdeaconu Mar 18, 2025
51cdf1f
Merge branch 'commitglobal:main' into main
imdeaconu Mar 19, 2025
296c5b9
Merge branch 'commitglobal:main' into main
imdeaconu Mar 20, 2025
a65447c
WIPȘ add monitoring ngos table
imdeaconu Mar 20, 2025
c2ef6b5
WIP: add modal for assigning a new monitoring NGO
imdeaconu Mar 20, 2025
016265e
Merge branch 'commitglobal:main' into feature/monitoring-ngos-screen
imdeaconu Mar 20, 2025
e7c8fb1
Merge branch 'commitglobal:main' into feature/monitoring-ngos-screen
imdeaconu Mar 21, 2025
ab90ad2
WIP: fix bugs
imdeaconu Mar 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>): 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<MonitoringNgoModel>[] = [
{
accessorKey: 'name',
enableSorting: true,
header: ({ column }) => <DataTableColumnHeader title='Name' column={column} />,
},

{
id: 'actions',
cell: ({ row }) => {
const ngoId = row.original.id;

return (
<Button
title='Add'
onClick={() => {
addMonitoringNgoMutation.mutate(ngoId);
}}>
<Plus className='mr-2' width={18} height={18} />
Add
</Button>
);
},
},
];

return (
<Dialog open={open} onOpenChange={onOpenChange} modal={true}>
<DialogContent
className='min-w-[450px] max-h-[950px]'
onInteractOutside={(e) => {
e.preventDefault();
}}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}>
<DialogHeader>
<DialogTitle className='mb-3.5'>Add monitoring NGO</DialogTitle>
<Input value={searchText} onChange={handleSearchInput} className='max-w-md' placeholder='Search' />
</DialogHeader>
<div className='flex flex-col gap-3 overflow-auto h-[650px]'>
<QueryParamsDataTable
columns={monitoringNgosColDefs}
useQuery={(params) => useAvailableMonitoringNgos(electionRoundId, params)}
queryParams={queryParams}
/>
</div>
</DialogContent>
</Dialog>
);
}

export default AddMonitoringNgoDialog;
Original file line number Diff line number Diff line change
@@ -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<MonitoringNgoModel>[];
data: MonitoringNgoModel[];
};

function MonitoringNgosTable({ columns, data }: MonitoringNgosTableProps): FunctionComponent {
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});

const rows = table.getRowModel().rows;

return (
<>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} style={{ width: header.getSize() }}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>

<TableBody>
{rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell className='h-24 text-center' colSpan={columns.length}>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</>
);
}

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<MonitoringNgoModel>[] = [
{
accessorKey: 'name',
enableSorting: true,
header: ({ column }) => <DataTableColumnHeader title='Name' column={column} />,
},

{
accessorKey: 'ngoStatus',
enableSorting: false,
header: ({ column }) => <DataTableColumnHeader title='Status' column={column} />,
cell: ({
row: {
original: { ngoStatus },
},
}) => {
return <NgoStatusBadge status={ngoStatus} />;
},
},
{
id: 'actions',
cell: ({ row }) => {
const ngoId = row.original.ngoId;

return (
<div className='text-right'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost-primary' size='icon'>
<span className='sr-only'>Actions</span>
<EllipsisVerticalIcon className='w-6 h-6' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem
className='text-red-600'
onClick={async (e) => {
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
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
];

return (
<Card>
<CardHeader className='flex gap-2 flex-column'>
<div className='flex flex-row items-center justify-between'>
<CardTitle className='text-2xl font-semibold leading-none tracking-tight'>
{t('electionEvent.monitoringNgos.cardTitle')}
</CardTitle>
</div>
<Separator />
</CardHeader>
<CardContent className='flex flex-col items-baseline gap-6'>{electionRoundId}</CardContent>
</Card>
<>
<Card>
<CardHeader className='flex gap-2 flex-column'>
<div className='flex flex-row items-center justify-between'>
<CardTitle className='text-2xl font-semibold leading-none tracking-tight'>
{t('electionEvent.monitoringNgos.cardTitle')}
</CardTitle>
<div className='flex md:flex-row-reverse gap-4 table-actions'>
<Button title='Add admin' onClick={() => addMonitoringNgoDialog.trigger()}>
<Plus className='mr-2' width={18} height={18} />
Add monitoring NGO
</Button>
</div>
</div>
<Separator />
</CardHeader>
<CardContent className='flex flex-col items-baseline gap-6'>
<MonitoringNgosTable columns={monitoringNgosColDefs} data={data?.monitoringNgos ?? []} />
</CardContent>
</Card>
{addMonitoringNgoDialog.dialogProps.open && (
<AddMonitoringNgoDialog electionRoundId={electionRoundId} {...addMonitoringNgoDialog.dialogProps} />
)}
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<MonitoringNgosPageResponse, Error> {
return useQuery({
queryKey: monitoringNgoKeys.all(electionRoundId),
placeholderData: { monitoringNgos: [] },
queryFn: async () => {
const response = await authApi.get<MonitoringNgosPageResponse>(
`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<PageResponse<MonitoringNgoModel>, Error> {
return useQuery({
queryKey: monitoringNgoKeys.availableForMonitoring(electionRoundId, p),
queryFn: async () => {
const response = await authApi.get<PageResponse<MonitoringNgoModel>>(
`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,
});
}
3 changes: 2 additions & 1 deletion web/src/features/election-rounds/models/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down