diff --git a/web/src/features/filtering/components/ActiveFilters.tsx b/web/src/features/filtering/components/ActiveFilters.tsx index 13fe5ffb1..9342c369c 100644 --- a/web/src/features/filtering/components/ActiveFilters.tsx +++ b/web/src/features/filtering/components/ActiveFilters.tsx @@ -2,18 +2,18 @@ import { DateTimeFormat } from '@/common/formats'; import { FilterBadge } from '@/components/ui/badge'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useFormSubmissionsFilters } from '@/features/responses/hooks/form-submissions-queries'; -import { useNavigate } from '@tanstack/react-router'; -import { format } from 'date-fns/format'; -import { FC, useCallback } from 'react'; -import { FILTER_KEY, FILTER_LABEL } from '../filtering-enums'; -import { isNotNilOrWhitespace, toBoolean } from '@/lib/utils'; import { mapFormSubmissionFollowUpStatus, mapIncidentCategory, + mapIncidentReportLocationType, mapQuickReportFollowUpStatus, mapQuickReportLocationType, } from '@/features/responses/utils/helpers'; -import { QuickReportFollowUpStatus } from '@/common/types'; +import { isNotNilOrWhitespace, toBoolean } from '@/lib/utils'; +import { useNavigate } from '@tanstack/react-router'; +import { format } from 'date-fns/format'; +import { FC, useCallback } from 'react'; +import { FILTER_KEY, FILTER_LABEL } from '../filtering-enums'; interface ActiveFilterProps { filterId: string; @@ -48,7 +48,6 @@ const FILTER_LABELS = new Map([ [FILTER_KEY.LocationL3, FILTER_LABEL.LocationL3], [FILTER_KEY.LocationL4, FILTER_LABEL.LocationL4], [FILTER_KEY.LocationL5, FILTER_LABEL.LocationL5], - [FILTER_KEY.FormSubmissionsMonitoringObserverTags, FILTER_LABEL.FormSubmissionsMonitoringObserverTags], [FILTER_KEY.PollingStationNumber, FILTER_LABEL.PollingStationNumber], [FILTER_KEY.FormId, FILTER_LABEL.FormId], [FILTER_KEY.FormStatusFilter, FILTER_LABEL.FormStatus], @@ -59,12 +58,16 @@ const FILTER_LABELS = new Map([ [FILTER_KEY.QuickReportIncidentCategory, FILTER_LABEL.QuickReportIncidentCategory], [FILTER_KEY.QuickReportFollowUpStatus, FILTER_LABEL.QuickReportFollowUpStatus], [FILTER_KEY.HasQuickReports, FILTER_LABEL.HasQuickReports], + [FILTER_KEY.IncidentReportLocationType, FILTER_LABEL.IncidentReportLocationType], + [FILTER_KEY.QuickReportLocationType, FILTER_LABEL.IncidentReportLocationType], ]); const FILTER_VALUE_LOCALIZATORS = new Map string>([ [FILTER_KEY.QuickReportFollowUpStatus, mapQuickReportFollowUpStatus], [FILTER_KEY.FormSubmissionFollowUpStatus, mapFormSubmissionFollowUpStatus], [FILTER_KEY.QuickReportIncidentCategory, mapIncidentCategory], + [FILTER_KEY.IncidentReportLocationType, mapIncidentReportLocationType], + [FILTER_KEY.QuickReportLocationType, mapQuickReportLocationType], ]); const ActiveFilter: FC = ({ filterId, value, isArray }) => { diff --git a/web/src/features/filtering/components/FilteringIcon.tsx b/web/src/features/filtering/components/FilteringIcon.tsx new file mode 100644 index 000000000..6de702ca1 --- /dev/null +++ b/web/src/features/filtering/components/FilteringIcon.tsx @@ -0,0 +1,20 @@ +import { FunnelIcon } from '@heroicons/react/24/outline'; +import { FC, SetStateAction } from 'react'; + +interface FilteringIconProps { + filteringIsExpanded: boolean; + setFilteringIsExpanded: (value: SetStateAction) => void; +} + +export const FilteringIcon: FC = ({ filteringIsExpanded, setFilteringIsExpanded }) => { + return ( + { + setFilteringIsExpanded((prev) => !prev); + }} + /> + ); +}; diff --git a/web/src/features/filtering/components/LocationTypeFilters.tsx b/web/src/features/filtering/components/LocationTypeFilters.tsx new file mode 100644 index 000000000..c19fe3f14 --- /dev/null +++ b/web/src/features/filtering/components/LocationTypeFilters.tsx @@ -0,0 +1,68 @@ +import { SelectFilter, SelectFilterOption } from '@/features/filtering/components/SelectFilter'; +import { FILTER_KEY } from '@/features/filtering/filtering-enums'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { IncidentReportLocationType } from '@/features/responses/models/incident-report'; +import { QuickReportLocationType } from '@/features/responses/models/quick-report'; +import { mapIncidentReportLocationType, mapQuickReportLocationType } from '@/features/responses/utils/helpers'; +import { FC } from 'react'; + +export const IncidentReportsLocationTypeFilter: FC = () => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onChange = (value: string) => { + navigateHandler({ [FILTER_KEY.IncidentReportLocationType]: value }); + }; + const options: SelectFilterOption[] = [ + { + value: IncidentReportLocationType.PollingStation, + label: mapIncidentReportLocationType(IncidentReportLocationType.PollingStation), + }, + + { + value: IncidentReportLocationType.OtherLocation, + label: mapIncidentReportLocationType(IncidentReportLocationType.OtherLocation), + }, + ]; + + return ( + + ); +}; + +export const QuickReportsLocationTypeFilter: FC = () => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onChange = (value: string) => { + navigateHandler({ [FILTER_KEY.QuickReportLocationType]: value }); + }; + const options: SelectFilterOption[] = [ + { + value: QuickReportLocationType.NotRelatedToAPollingStation, + label: mapQuickReportLocationType(QuickReportLocationType.NotRelatedToAPollingStation), + }, + + { + value: QuickReportLocationType.OtherPollingStation, + label: mapQuickReportLocationType(QuickReportLocationType.OtherPollingStation), + }, + + { + value: QuickReportLocationType.VisitedPollingStation, + label: mapQuickReportLocationType(QuickReportLocationType.VisitedPollingStation), + }, + ]; + + return ( + + ); +}; diff --git a/web/src/features/filtering/filtering-enums.ts b/web/src/features/filtering/filtering-enums.ts index 13703a5ad..b7acbb8ad 100644 --- a/web/src/features/filtering/filtering-enums.ts +++ b/web/src/features/filtering/filtering-enums.ts @@ -16,7 +16,6 @@ export const enum FILTER_KEY { LocationL4 = 'level4Filter', LocationL5 = 'level5Filter', PollingStationNumber = 'pollingStationNumberFilter', - FormSubmissionsMonitoringObserverTags = 'tagsFilter', ViewBy = 'viewBy', Tab = 'tab', FormId = 'formId', @@ -26,14 +25,16 @@ export const enum FILTER_KEY { ToDate = 'submissionsToDate', SearchText = 'searchText', FormIsCompleted = 'formIsCompleted', - QuickReportIncidentCategory ='incidentCategory', - QuickReportFollowUpStatus ='quickReportFollowUpStatus', - HasQuickReports ='hasQuickReports', + QuickReportIncidentCategory = 'incidentCategory', + QuickReportFollowUpStatus = 'quickReportFollowUpStatus', + HasQuickReports = 'hasQuickReports', + IncidentReportLocationType = 'incidentReportLocationType', + QuickReportLocationType = 'quickReportLocationType', } export const enum FILTER_LABEL { MonitoringObserverStatus = 'Observer status', - MonitoringObserverTags = 'Tags', + MonitoringObserverTags = 'Observer tags', FormTypeFilter = 'Form type', HasFlaggedAnswers = 'Flagged answers', FollowUpStatus = 'Follow-up status', @@ -45,7 +46,6 @@ export const enum FILTER_LABEL { LocationL4 = 'Location - L4', LocationL5 = 'Location - L5', PollingStationNumber = 'Polling station number', - FormSubmissionsMonitoringObserverTags = 'Observer tags', MediaFiles = 'Has attachments', FormId = 'Form', FormStatus = 'Form status', @@ -53,7 +53,8 @@ export const enum FILTER_LABEL { ToDate = 'To Date', SearchText = 'Search text', FormCompleted = 'Form completed', - QuickReportIncidentCategory ='Incident category', - QuickReportFollowUpStatus ='Quick report follow up status', - HasQuickReports ='Has quick reports', + QuickReportIncidentCategory = 'Incident category', + QuickReportFollowUpStatus = 'Quick report follow up status', + HasQuickReports = 'Has quick reports', + IncidentReportLocationType = 'Location type', } diff --git a/web/src/features/filtering/hooks/useFilteringContainer.ts b/web/src/features/filtering/hooks/useFilteringContainer.ts index 85d05f2cf..b4ed6ed7c 100644 --- a/web/src/features/filtering/hooks/useFilteringContainer.ts +++ b/web/src/features/filtering/hooks/useFilteringContainer.ts @@ -1,6 +1,6 @@ import { useSetPrevSearch } from '@/common/prev-search-store'; import { useNavigate, useSearch } from '@tanstack/react-router'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { HIDDEN_FILTERS } from '../components/ActiveFilters'; import { FILTER_KEY } from '../filtering-enums'; @@ -14,12 +14,13 @@ export function useFilteringContainer() { const setPrevSearch = useSetPrevSearch(); const filteringIsActive = useMemo(() => { - return Object.entries(queryParams) .filter(([key, _]) => !HIDDEN_FILTERS.includes(key)) .some(([_, value]) => !!value); }, [queryParams]); + const [filteringIsExpanded, setFilteringIsExpanded] = useState(filteringIsActive ?? false); + const navigateHandler = useCallback( (search: Record) => { void navigate({ @@ -44,5 +45,13 @@ export function useFilteringContainer() { setPrevSearch(filterObject(queryParams, HIDDEN_FILTERS)); }; - return { queryParams, filteringIsActive, navigate, navigateHandler, resetFilters }; + return { + queryParams, + filteringIsActive, + filteringIsExpanded, + setFilteringIsExpanded, + navigate, + navigateHandler, + resetFilters, + }; } diff --git a/web/src/features/forms/components/Dashboard/Dashboard.tsx b/web/src/features/forms/components/Dashboard/Dashboard.tsx index 9a0386a67..54b80b27d 100644 --- a/web/src/features/forms/components/Dashboard/Dashboard.tsx +++ b/web/src/features/forms/components/Dashboard/Dashboard.tsx @@ -44,13 +44,14 @@ import { formsKeys, useForms } from '../../queries'; import AddTranslationsDialog, { useAddTranslationsDialog } from './AddTranslationsDialog'; import CreateForm from './CreateForm'; import { FormFilters } from './FormFilters/FormFilters'; +import { FilteringIcon } from '@/features/filtering/components/FilteringIcon'; export default function FormsDashboard(): ReactElement { const navigate = useNavigate(); const search = Route.useSearch(); const debouncedSearch = useDebounce(search, 300); const [searchText, setSearchText] = useState(''); - const { filteringIsActive } = useFilteringContainer(); + const { filteringIsExpanded, setFilteringIsExpanded } = useFilteringContainer(); const queryParams = useMemo(() => { const params = [ @@ -347,7 +348,6 @@ export default function FormsDashboard(): ReactElement { return defaultColumns; }, [currentElectionRoundId, isMonitoringNgoForCitizenReporting]); - const [isFiltering, setIsFiltering] = useState(filteringIsActive); const handleSearchInput = (ev: React.FormEvent) => { setSearchText(ev.currentTarget.value); @@ -526,18 +526,12 @@ export default function FormsDashboard(): ReactElement {
<> - { - setIsFiltering((prev) => !prev); - }} - /> +
- {isFiltering && } + {filteringIsExpanded && } ) => { setSearchText(ev.currentTarget.value); @@ -208,11 +208,6 @@ function MonitoringObserversList() { }, }); - const changeIsFiltering = () => { - setFiltersExpanded((prev) => { - return !prev; - }); - }; function handleResendInviteToObserver(id?: string): void { setMonitoringObserverId(id); @@ -339,16 +334,12 @@ function MonitoringObserversList() {
<> - +
- {filtersExpanded && } + {filteringIsExpanded && } = ({ isFilteringFormSubmissions }) => { - const COMPONENT_FILTER_KEY = isFilteringFormSubmissions - ? FILTER_KEY.FormSubmissionsMonitoringObserverTags - : FILTER_KEY.MonitoringObserverTags; +export const MonitoringObserverTagsSelect: FC = () => { + const COMPONENT_FILTER_KEY = FILTER_KEY.MonitoringObserverTags; const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: tags } = useMonitoringObserversTags(currentElectionRoundId); diff --git a/web/src/features/polling-stations/components/Dashboard/Dashboard.tsx b/web/src/features/polling-stations/components/Dashboard/Dashboard.tsx index 7ca1daaf5..810dc5f48 100644 --- a/web/src/features/polling-stations/components/Dashboard/Dashboard.tsx +++ b/web/src/features/polling-stations/components/Dashboard/Dashboard.tsx @@ -15,15 +15,19 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { FilteringIcon } from '@/features/filtering/components/FilteringIcon'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { ExportDataButton } from '@/features/responses/components/ExportDataButton/ExportDataButton'; import { ExportedDataType } from '@/features/responses/models/data-export'; -import { FunnelIcon } from '@heroicons/react/24/outline'; +import i18n from '@/i18n'; import { useNavigate, useSearch } from '@tanstack/react-router'; import { useDebounce } from '@uidotdev/usehooks'; -import { useCallback, useMemo, useState, type ReactElement } from 'react'; -import i18n from '@/i18n'; +import { useCallback, useMemo, type ReactElement } from 'react'; -function usePollingStations(electionRoundId: string, queryParams: DataTableParameters): UseQueryResult, Error> { +function usePollingStations( + electionRoundId: string, + queryParams: DataTableParameters +): UseQueryResult, Error> { return useQuery({ queryKey: ['pollingStations', electionRoundId, queryParams], queryFn: async () => { @@ -36,7 +40,8 @@ function usePollingStations(electionRoundId: string, queryParams: DataTableParam }; const searchParams = buildURLSearchParams(params); - const response = await authApi.get>(`/election-rounds/${electionRoundId}/polling-stations:list`, + const response = await authApi.get>( + `/election-rounds/${electionRoundId}/polling-stations:list`, { params: searchParams, } @@ -48,13 +53,15 @@ function usePollingStations(electionRoundId: string, queryParams: DataTableParam return response.data; }, - enabled: !!electionRoundId + enabled: !!electionRoundId, }); } export const pollingStationColDefs: ColumnDef[] = [ { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'level1', enableSorting: true, enableGlobalFilter: true, @@ -62,14 +69,12 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { level1 }, }, - }) => ( -

- {level1} -

- ), + }) =>

{level1}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'level2', enableSorting: true, enableGlobalFilter: true, @@ -77,14 +82,12 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { level2 }, }, - }) => ( -

- {level2} -

- ), + }) =>

{level2}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'level3', enableSorting: true, enableGlobalFilter: true, @@ -92,14 +95,12 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { level3 }, }, - }) => ( -

- {level3} -

- ), + }) =>

{level3}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'level4', enableSorting: true, enableGlobalFilter: true, @@ -107,14 +108,12 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { level4 }, }, - }) => ( -

- {level4} -

- ), + }) =>

{level4}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'level5', enableSorting: true, enableGlobalFilter: true, @@ -122,14 +121,12 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { level5 }, }, - }) => ( -

- {level5} -

- ), + }) =>

{level5}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'number', enableSorting: true, enableGlobalFilter: true, @@ -137,14 +134,15 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { number }, }, - }) => ( -

- {number} -

- ), + }) =>

{number}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'address', enableSorting: true, enableGlobalFilter: true, @@ -152,14 +150,12 @@ export const pollingStationColDefs: ColumnDef[] = [ row: { original: { address }, }, - }) => ( -

- {address} -

- ), + }) =>

{address}

, }, { - header: ({ column }) => , + header: ({ column }) => ( + + ), accessorKey: 'tags', enableSorting: false, enableGlobalFilter: true, @@ -171,7 +167,6 @@ export const pollingStationColDefs: ColumnDef[] = [ }, ]; - export default function PollingStationsDashboard(): ReactElement { const navigate = useNavigate(); @@ -184,14 +179,6 @@ export default function PollingStationsDashboard(): ReactElement { pollingStationNumberFilter?: string; }; - const [isFiltering, setFiltering] = useState(Object.keys(search).some(k => k === 'level1Filter' || k === 'level2Filter' || k === 'level3Filter' || k === 'level4Filter' || k === 'level5Filter')); - - const changeIsFiltering = () => { - setFiltering((prev) => { - return !prev; - }); - }; - const onClearFilter = useCallback( (filter: string | string[]) => () => { const filters = Array.isArray(filter) @@ -202,9 +189,10 @@ export default function PollingStationsDashboard(): ReactElement { [navigate] ); - const debouncedSearch = useDebounce(search, 300); - const currentElectionRoundId = useCurrentElectionRoundStore(s => s.currentElectionRoundId); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + + const { filteringIsExpanded, setFilteringIsExpanded } = useFilteringContainer(); const queryParams = useMemo(() => { const params = [ @@ -228,49 +216,59 @@ export default function PollingStationsDashboard(): ReactElement {
-
- <> - - +
- {isFiltering && (
+ {filteringIsExpanded && ( +
+ - - - -
)} + +
+ )} {Object.entries(search).length > 0 && (
- - {search.level1Filter && ( - + )} {search.level2Filter && ( - + )} {search.level3Filter && ( - + )} {search.level4Filter && ( - + )} {search.level5Filter && ( @@ -278,11 +276,14 @@ export default function PollingStationsDashboard(): ReactElement { )}
)} - - usePollingStations(currentElectionRoundId, params)} queryParams={queryParams} /> + usePollingStations(currentElectionRoundId, params)} + queryParams={queryParams} + /> ); -} \ No newline at end of file +} diff --git a/web/src/features/responses/components/CitizenReportsAggregatedByFormTable/CitizenReportsAggregatedByFormTable.tsx b/web/src/features/responses/components/CitizenReportsAggregatedByFormTable/CitizenReportsAggregatedByFormTable.tsx index fee9c48ab..b1b564bd1 100644 --- a/web/src/features/responses/components/CitizenReportsAggregatedByFormTable/CitizenReportsAggregatedByFormTable.tsx +++ b/web/src/features/responses/components/CitizenReportsAggregatedByFormTable/CitizenReportsAggregatedByFormTable.tsx @@ -3,15 +3,40 @@ import { CardContent } from '@/components/ui/card'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { getRouteApi } from '@tanstack/react-router'; -import { useCallback } from 'react'; +import { useDebounce } from '@uidotdev/usehooks'; +import { useCallback, useMemo } from 'react'; import { useCitizenReportsAggregatedByForm } from '../../hooks/citizen-reports'; +import { FormSubmissionsSearchParams } from '../../models/search-params'; import { citizenReportsAggregatedByFormColumnDefs } from '../../utils/column-defs'; +import { useCitizenReportsColumnsVisibility } from '../../store/column-visibility'; const routeApi = getRouteApi('/responses/'); export function CitizenReportsAggregatedByFormTable(): FunctionComponent { const navigate = routeApi.useNavigate(); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + const search = routeApi.useSearch(); + const debouncedSearch = useDebounce(search, 300); + const columnsVisibility = useCitizenReportsColumnsVisibility(); + + const queryParams = useMemo(() => { + const params = [ + ['hasFlaggedAnswers', debouncedSearch.hasFlaggedAnswers], + ['level1Filter', debouncedSearch.level1Filter], + ['level2Filter', debouncedSearch.level2Filter], + ['level3Filter', debouncedSearch.level3Filter], + ['level4Filter', debouncedSearch.level4Filter], + ['level5Filter', debouncedSearch.level5Filter], + ['pollingStationNumberFilter', debouncedSearch.pollingStationNumberFilter], + ['followUpStatus', debouncedSearch.followUpStatus], + ['questionsAnswered', debouncedSearch.questionsAnswered], + ['hasNotes', debouncedSearch.hasNotes], + ['hasAttachments', debouncedSearch.hasAttachments], + ['formId', debouncedSearch.formId], + ].filter(([_, value]) => value); + + return Object.fromEntries(params) as FormSubmissionsSearchParams; + }, [debouncedSearch]); const navigateToAggregatedForm = useCallback( (formId: string) => { @@ -26,6 +51,8 @@ export function CitizenReportsAggregatedByFormTable(): FunctionComponent { columns={citizenReportsAggregatedByFormColumnDefs} useQuery={(params) => useCitizenReportsAggregatedByForm(currentElectionRoundId, params)} onRowClick={navigateToAggregatedForm} + queryParams={queryParams} + columnVisibility={columnsVisibility} />
); diff --git a/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx b/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx index 8aa4dbcf5..77f1a172f 100644 --- a/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx +++ b/web/src/features/responses/components/CitizenReportsByEntryTable/CitizenReportsByEntryTable.tsx @@ -8,21 +8,20 @@ import { useDebounce } from '@uidotdev/usehooks'; import { useCallback, useMemo } from 'react'; import { useCitizenReports } from '../../hooks/citizen-reports'; import type { FormSubmissionsSearchParams } from '../../models/search-params'; +import { useCitizenReportsColumnsVisibility } from '../../store/column-visibility'; import { citizenReportsByEntryColumnDefs } from '../../utils/column-defs'; -type CitizenReportsByEntryTableProps = { -}; +type CitizenReportsByEntryTableProps = {}; export function CitizenReportsByEntryTable(props: CitizenReportsByEntryTableProps): FunctionComponent { const navigate = useNavigate(); const search = Route.useSearch(); const debouncedSearch = useDebounce(search, 300); - const currentElectionRoundId = useCurrentElectionRoundStore(s => s.currentElectionRoundId); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + const columnsVisibility = useCitizenReportsColumnsVisibility(); const queryParams = useMemo(() => { - const params = [ - ['followUpStatus', debouncedSearch.citizenReportFollowUpStatus], - ].filter(([_, value]) => value); + const params = [['followUpStatus', debouncedSearch.citizenReportFollowUpStatus]].filter(([_, value]) => value); return Object.fromEntries(params) as FormSubmissionsSearchParams; }, [debouncedSearch]); @@ -41,6 +40,7 @@ export function CitizenReportsByEntryTable(props: CitizenReportsByEntryTableProp useQuery={(params) => useCitizenReports(currentElectionRoundId, params)} queryParams={queryParams} onRowClick={navigateToCitizenReport} + columnVisibility={columnsVisibility} />
); diff --git a/web/src/features/responses/components/CitizenReportsColumnVisibilitySelector/CitizenReportsColumnVisibilitySelector.tsx b/web/src/features/responses/components/CitizenReportsColumnVisibilitySelector/CitizenReportsColumnVisibilitySelector.tsx new file mode 100644 index 000000000..3548b05f9 --- /dev/null +++ b/web/src/features/responses/components/CitizenReportsColumnVisibilitySelector/CitizenReportsColumnVisibilitySelector.tsx @@ -0,0 +1,40 @@ +import { FunctionComponent } from '@/common/types'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Cog8ToothIcon } from '@heroicons/react/24/outline'; +import { useCitizenReportsColumnsVisibility, useCitizenReportsToggleColumn } from '../../store/column-visibility'; +import { citizenReportsColumnVisibilityOptions } from '../../utils/column-visibility-options'; + +export function CitizenReportsColumnVisibilitySelector(): FunctionComponent { + const columnsVisibility = useCitizenReportsColumnsVisibility(); + const toggleColumn = useCitizenReportsToggleColumn(); + + return ( + + + + + + Table columns + + {citizenReportsColumnVisibilityOptions.map((option) => ( + { + toggleColumn(option.id, checked); + }}> + {option.label} + + ))} + + + ); +} diff --git a/web/src/features/responses/components/CitizenReportsFiltersByEntry/CitizenReportsFiltersByEntry.tsx b/web/src/features/responses/components/CitizenReportsFiltersByEntry/CitizenReportsFiltersByEntry.tsx index a050baf38..52965699b 100644 --- a/web/src/features/responses/components/CitizenReportsFiltersByEntry/CitizenReportsFiltersByEntry.tsx +++ b/web/src/features/responses/components/CitizenReportsFiltersByEntry/CitizenReportsFiltersByEntry.tsx @@ -1,121 +1,31 @@ -import { useSetPrevSearch } from '@/common/prev-search-store'; -import { CitizenReportFollowUpStatus, FunctionComponent } from '@/common/types'; -import { FilterBadge } from '@/components/ui/badge'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { FILTER_KEY } from '@/features/filtering/filtering-enums'; -import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; -import { Route } from '@/routes/responses'; -import { useNavigate } from '@tanstack/react-router'; -import { useCallback, useState } from 'react'; -import type { FormSubmissionsSearchParams } from '../../models/search-params'; -import { mapCitizenReportFollowUpStatus } from '../../utils/helpers'; -import { ResetFiltersButton } from '../ResetFiltersButton/ResetFiltersButton'; - -export function CitizenReportsFiltersByEntry(): FunctionComponent { - const navigate = useNavigate({ from: '/responses' }); - const search = Route.useSearch(); - const setPrevSearch = useSetPrevSearch(); - const { filteringIsActive } = useFilteringContainer(); - const [isFiltering, setIsFiltering] = useState(filteringIsActive); - - const navigateHandler = useCallback( - (search: Record) => { - void navigate({ - // @ts-ignore - search: (prev) => { - const newSearch: Record = { - ...prev, - ...search, - }; - setPrevSearch(newSearch); - return newSearch; - }, - }); - }, - [navigate, setPrevSearch] - ); - - const onClearFilter = useCallback( - (filter: keyof FormSubmissionsSearchParams | (keyof FormSubmissionsSearchParams)[]) => () => { - const filters = Array.isArray(filter) - ? Object.fromEntries(filter.map((key) => [key, undefined])) - : { [filter]: undefined }; - navigateHandler(filters); - }, - [navigateHandler] - ); - +import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { FormSubmissionsCompletionFilter } from '@/features/filtering/components/FormSubmissionsCompletionFilter'; +import { FormTypeFilter } from '@/features/filtering/components/FormTypeFilter'; +import { MonitoringObserverTagsSelect } from '@/features/monitoring-observers/filtering/MonitoringObserverTagsSelect'; +import { FC } from 'react'; +import { FormSubmissionsFlaggedAnswersFilter } from '../../../filtering/components/FormSubmissionsFlaggedAnswersFilter'; +import { FormSubmissionsFollowUpFilter } from '../../../filtering/components/FormSubmissionsFollowUpFilter'; +import { FormSubmissionsFormFilter } from '../../../filtering/components/FormSubmissionsFormFilter'; +import { FormSubmissionsFromDateFilter } from '../../../filtering/components/FormSubmissionsFromDateFilter'; +import { FormSubmissionsMediaFilesFilter } from '../../../filtering/components/FormSubmissionsMediaFilesFilter'; +import { FormSubmissionsQuestionNotesFilter } from '../../../filtering/components/FormSubmissionsQuestionNotesFilter'; +import { FormSubmissionsQuestionsAnsweredFilter } from '../../../filtering/components/FormSubmissionsQuestionsAnsweredFilter'; +import { FormSubmissionsToDateFilter } from '../../../filtering/components/FormSubmissionsToDateFilter'; + +export const CitizenReportsFiltersByEntry: FC = () => { return ( - <> - - - - - - - {isFiltering && ( -
- {search.formTypeFilter && ( - - )} - - {search.citizenReportFollowUpStatus && ( - - )} - - {search.hasFlaggedAnswers && ( - - )} -
- )} - + + + + + + + + + + + + ); -} +}; diff --git a/web/src/features/responses/components/CitizenReportsFiltersByForm/CitizenReportsFiltersByForm.tsx b/web/src/features/responses/components/CitizenReportsFiltersByForm/CitizenReportsFiltersByForm.tsx new file mode 100644 index 000000000..21ce1b5a1 --- /dev/null +++ b/web/src/features/responses/components/CitizenReportsFiltersByForm/CitizenReportsFiltersByForm.tsx @@ -0,0 +1,23 @@ +import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { FormSubmissionsFollowUpFilter } from '@/features/filtering/components/FormSubmissionsFollowUpFilter'; +import { FormSubmissionsFormFilter } from '@/features/filtering/components/FormSubmissionsFormFilter'; +import { FormSubmissionsQuestionsAnsweredFilter } from '@/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter'; +import { FC } from 'react'; +import { FormSubmissionsFlaggedAnswersFilter } from '../../../filtering/components/FormSubmissionsFlaggedAnswersFilter'; +import { FormSubmissionsMediaFilesFilter } from '../../../filtering/components/FormSubmissionsMediaFilesFilter'; +import { FormSubmissionsQuestionNotesFilter } from '../../../filtering/components/FormSubmissionsQuestionNotesFilter'; + +export const CitizenReportsFiltersByForm: FC = () => { + return ( + + + + + + + + + + ); +}; diff --git a/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx b/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx index 62a14162e..26ca36d43 100644 --- a/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx +++ b/web/src/features/responses/components/CitizenReportsTab/CitizenReportsTab.tsx @@ -11,17 +11,20 @@ import { import { Separator } from '@/components/ui/separator'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; import { useNavigate } from '@tanstack/react-router'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { ExportedDataType } from '../../models/data-export'; import { ExportDataButton } from '../ExportDataButton/ExportDataButton'; import { FunctionComponent } from '@/common/types'; +import { FilteringIcon } from '@/features/filtering/components/FilteringIcon'; import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { Route } from '@/routes/responses'; import { CitizenReportsAggregatedByFormTable } from '../CitizenReportsAggregatedByFormTable/CitizenReportsAggregatedByFormTable'; import { CitizenReportsByEntryTable } from '../CitizenReportsByEntryTable/CitizenReportsByEntryTable'; +import { CitizenReportsColumnVisibilitySelector } from '../CitizenReportsColumnVisibilitySelector/CitizenReportsColumnVisibilitySelector'; import { CitizenReportsFiltersByEntry } from '../CitizenReportsFiltersByEntry/CitizenReportsFiltersByEntry'; +import { CitizenReportsFiltersByForm } from '../CitizenReportsFiltersByForm/CitizenReportsFiltersByForm'; const viewBy: Record = { byEntry: 'View by entry', @@ -31,12 +34,10 @@ const viewBy: Record = { export function CitizenReportsTab(): FunctionComponent { const navigate = useNavigate(); const search = Route.useSearch(); - const { filteringIsActive } = useFilteringContainer(); + const { filteringIsExpanded, setFilteringIsExpanded } = useFilteringContainer(); const { viewBy: byFilter } = search; - const [isFiltering, setIsFiltering] = useState(filteringIsActive); - const setPrevSearch = useSetPrevSearch(); useEffect(() => { if (byFilter === 'byObserver') { @@ -57,11 +58,11 @@ export function CitizenReportsTab(): FunctionComponent { return ( -
+
Citizen reports
- + @@ -75,7 +76,7 @@ export function CitizenReportsTab(): FunctionComponent { onValueChange={(value) => { setPrevSearch({ [FILTER_KEY.ViewBy]: value, [FILTER_KEY.Tab]: 'citizen-reports' }); void navigate({ search: { [FILTER_KEY.ViewBy]: value, [FILTER_KEY.Tab]: 'citizen-reports' } }); - setIsFiltering(false); + setFilteringIsExpanded(false); }} value={byFilter}> {Object.entries(viewBy).map(([value, label]) => ( @@ -88,12 +89,18 @@ export function CitizenReportsTab(): FunctionComponent {
+ +
+ + +
- {isFiltering && ( -
+ {filteringIsExpanded && ( +
{byFilter === 'byEntry' && } + {byFilter === 'byForm' && }
)} diff --git a/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx b/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx index a7bfa4e17..15127df37 100644 --- a/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsAggregatedByFormTable/FormSubmissionsAggregatedByFormTable.tsx @@ -42,7 +42,7 @@ export function FormSubmissionsAggregatedByFormTable({ questionsAnswered: search.questionsAnswered, hasNotes: search.hasNotes, hasAttachments: search.hasAttachments, - tagsFilter: search.tagsFilter, + tagsFilter: search.tags, submissionsFromDate: search.submissionsFromDate, submissionsToDate: search.submissionsToDate, formIsCompleted: search.formIsCompleted, @@ -66,7 +66,7 @@ export function FormSubmissionsAggregatedByFormTable({ ['questionsAnswered', search.questionsAnswered], ['hasNotes', search.hasNotes], ['hasAttachments', search.hasAttachments], - ['tagsFilter', search.tagsFilter], + ['tagsFilter', search.tags], ['formId', search.formId], ['fromDateFilter', search.submissionsFromDate?.toISOString()], ['toDateFilter', search.submissionsToDate?.toISOString()], diff --git a/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx b/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx index 252dd7c8f..8d374bb77 100644 --- a/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsByEntryTable/FormSubmissionsByEntryTable.tsx @@ -38,7 +38,7 @@ export function FormSubmissionsByEntryTable({ searchText }: FormSubmissionsByEnt ['questionsAnswered', debouncedSearch.questionsAnswered], ['hasNotes', debouncedSearch.hasNotes], ['hasAttachments', debouncedSearch.hasAttachments], - ['tagsFilter', debouncedSearch.tagsFilter], + ['tagsFilter', debouncedSearch.tags], ['formId', debouncedSearch.formId], ['fromDateFilter', debouncedSearch.submissionsFromDate?.toISOString()], ['toDateFilter', debouncedSearch.submissionsToDate?.toISOString()], diff --git a/web/src/features/responses/components/FormSubmissionsByObserverTable/FormSubmissionsByObserverTable.tsx b/web/src/features/responses/components/FormSubmissionsByObserverTable/FormSubmissionsByObserverTable.tsx index 7de75d124..76947a6fb 100644 --- a/web/src/features/responses/components/FormSubmissionsByObserverTable/FormSubmissionsByObserverTable.tsx +++ b/web/src/features/responses/components/FormSubmissionsByObserverTable/FormSubmissionsByObserverTable.tsx @@ -26,7 +26,7 @@ export function FormSubmissionsByObserverTable({ searchText }: FormSubmissionsBy const params = [ ['followUpStatus', debouncedSearch.followUpStatus], ['searchText', searchText], - ['tagsFilter', debouncedSearch.tagsFilter], + ['tagsFilter', debouncedSearch.tags], ['hasFlaggedAnswers', debouncedSearch.hasFlaggedAnswers], ].filter(([_, value]) => value); diff --git a/web/src/features/responses/components/FormSubmissionsFiltersByEntry/FormSubmissionsFiltersByEntry.tsx b/web/src/features/responses/components/FormSubmissionsFiltersByEntry/FormSubmissionsFiltersByEntry.tsx index b0883dbed..14e1542fc 100644 --- a/web/src/features/responses/components/FormSubmissionsFiltersByEntry/FormSubmissionsFiltersByEntry.tsx +++ b/web/src/features/responses/components/FormSubmissionsFiltersByEntry/FormSubmissionsFiltersByEntry.tsx @@ -24,7 +24,7 @@ export const FormSubmissionsFiltersByEntry: FC = () => { - + diff --git a/web/src/features/responses/components/FormSubmissionsFiltersByForm/FormSubmissionsFiltersByForm.tsx b/web/src/features/responses/components/FormSubmissionsFiltersByForm/FormSubmissionsFiltersByForm.tsx index d693e2453..90a0e19bc 100644 --- a/web/src/features/responses/components/FormSubmissionsFiltersByForm/FormSubmissionsFiltersByForm.tsx +++ b/web/src/features/responses/components/FormSubmissionsFiltersByForm/FormSubmissionsFiltersByForm.tsx @@ -6,7 +6,6 @@ import { FormSubmissionsFormFilter } from '@/features/filtering/components/FormS import { FormSubmissionsFromDateFilter } from '@/features/filtering/components/FormSubmissionsFromDateFilter'; import { FormSubmissionsQuestionsAnsweredFilter } from '@/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter'; import { FormSubmissionsToDateFilter } from '@/features/filtering/components/FormSubmissionsToDateFilter'; -import { FormTypeFilter } from '@/features/filtering/components/FormTypeFilter'; import { MonitoringObserverTagsSelect } from '@/features/monitoring-observers/filtering/MonitoringObserverTagsSelect'; import { FC } from 'react'; import { FormSubmissionsFlaggedAnswersFilter } from '../../../filtering/components/FormSubmissionsFlaggedAnswersFilter'; @@ -23,7 +22,7 @@ export const FormSubmissionsFiltersByForm: FC = () => { - + diff --git a/web/src/features/responses/components/FormSubmissionsFiltersByObserver/FormSubmissionsFiltersByObserver.tsx b/web/src/features/responses/components/FormSubmissionsFiltersByObserver/FormSubmissionsFiltersByObserver.tsx index 0ce055d2c..e0d3669a9 100644 --- a/web/src/features/responses/components/FormSubmissionsFiltersByObserver/FormSubmissionsFiltersByObserver.tsx +++ b/web/src/features/responses/components/FormSubmissionsFiltersByObserver/FormSubmissionsFiltersByObserver.tsx @@ -8,7 +8,7 @@ export function FormSubmissionsFiltersByObserver(): FunctionComponent { return ( - + ); diff --git a/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx b/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx index 31bfe1c0b..db2773bba 100644 --- a/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx +++ b/web/src/features/responses/components/FormSubmissionsTab/FormSubmissionsTab.tsx @@ -10,7 +10,7 @@ import { } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; -import { ChevronDownIcon, FunnelIcon } from '@heroicons/react/24/outline'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; import { useDebounce } from '@uidotdev/usehooks'; import { useEffect, useMemo, useState, type ChangeEvent } from 'react'; import { ExportedDataType } from '../../models/data-export'; @@ -28,6 +28,7 @@ import { FormSubmissionsFiltersByEntry } from '../FormSubmissionsFiltersByEntry/ import { FormSubmissionsFiltersByForm } from '../FormSubmissionsFiltersByForm/FormSubmissionsFiltersByForm'; import { FormSubmissionsFiltersByObserver } from '../FormSubmissionsFiltersByObserver/FormSubmissionsFiltersByObserver'; +import { FilteringIcon } from '@/features/filtering/components/FilteringIcon'; import { Route } from '@/routes/responses'; import { useNavigate } from '@tanstack/react-router'; @@ -40,8 +41,7 @@ const viewBy: Record = { export default function FormSubmissionsTab(): FunctionComponent { const navigate = useNavigate(); const search = Route.useSearch(); - const { filteringIsActive, navigateHandler } = useFilteringContainer(); - const [filtersExpanded, setFiltersExpanded] = useState(false); + const { filteringIsExpanded, setFilteringIsExpanded, navigateHandler } = useFilteringContainer(); const { viewBy: byFilter } = search; @@ -70,7 +70,7 @@ export default function FormSubmissionsTab(): FunctionComponent { ['questionsAnswered', search.questionsAnswered], ['hasNotes', search.hasNotes], ['hasAttachments', search.hasAttachments], - ['tagsFilter', search.tagsFilter], + ['tagsFilter', search.tags], ['formId', search.formId], ['fromDateFilter', search.submissionsFromDate?.toISOString()], ['toDateFilter', search.submissionsToDate?.toISOString()], @@ -133,13 +133,7 @@ export default function FormSubmissionsTab(): FunctionComponent {
<> - { - setFiltersExpanded((prev) => !prev); - }} - /> + @@ -147,7 +141,7 @@ export default function FormSubmissionsTab(): FunctionComponent { - {filtersExpanded && ( + {filteringIsExpanded && ( <> {byFilter === 'byEntry' && } {byFilter === 'byObserver' && } diff --git a/web/src/features/responses/components/IncidentReportsByObserverTable/IncidentReportsByObserverTable.tsx b/web/src/features/responses/components/IncidentReportsByObserverTable/IncidentReportsByObserverTable.tsx index e0c61f73d..104a61453 100644 --- a/web/src/features/responses/components/IncidentReportsByObserverTable/IncidentReportsByObserverTable.tsx +++ b/web/src/features/responses/components/IncidentReportsByObserverTable/IncidentReportsByObserverTable.tsx @@ -20,14 +20,14 @@ export function IncidentReportsByObserverTable({ searchText }: IncidentReportsBy const navigate = routeApi.useNavigate(); const search = routeApi.useSearch(); const debouncedSearch = useDebounce(search, 300); - const currentElectionRoundId = useCurrentElectionRoundStore(s => s.currentElectionRoundId); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const columnsVisibility = useIncidentReportsByObserverColumns(); const queryParams = useMemo(() => { const params = [ ['followUpStatus', debouncedSearch.followUpStatus], ['searchText', searchText], - ['tagsFilter', debouncedSearch.tagsFilter], + ['tagsFilter', debouncedSearch.tags], ].filter(([_, value]) => value); return Object.fromEntries(params) as FormSubmissionsSearchParams; diff --git a/web/src/features/responses/components/IncidentReportsFiltersByEntry/IncidentReportsFiltersByEntry.tsx b/web/src/features/responses/components/IncidentReportsFiltersByEntry/IncidentReportsFiltersByEntry.tsx index 70e7c714c..3baa7029c 100644 --- a/web/src/features/responses/components/IncidentReportsFiltersByEntry/IncidentReportsFiltersByEntry.tsx +++ b/web/src/features/responses/components/IncidentReportsFiltersByEntry/IncidentReportsFiltersByEntry.tsx @@ -1,338 +1,25 @@ -import { useSetPrevSearch } from '@/common/prev-search-store'; -import { FunctionComponent, IncidentReportFollowUpStatus, QuestionsAnswered } from '@/common/types'; +import { FunctionComponent } from '@/common/types'; import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; -import { FilterBadge } from '@/components/ui/badge'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { FILTER_KEY } from '@/features/filtering/filtering-enums'; -import { Route } from '@/routes/responses'; -import { useNavigate } from '@tanstack/react-router'; -import { useCallback, useState } from 'react'; -import { IncidentReportLocationType } from '../../models/incident-report'; -import type { FormSubmissionsSearchParams } from '../../models/search-params'; -import { - mapIncidentReportFollowUpStatus, - mapIncidentReportLocationType, - mapQuestionsAnswered, -} from '../../utils/helpers'; -import { ResetFiltersButton } from '../ResetFiltersButton/ResetFiltersButton'; -import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { FormSubmissionsCompletionFilter } from '@/features/filtering/components/FormSubmissionsCompletionFilter'; +import { FormSubmissionsFlaggedAnswersFilter } from '@/features/filtering/components/FormSubmissionsFlaggedAnswersFilter'; +import { FormSubmissionsFollowUpFilter } from '@/features/filtering/components/FormSubmissionsFollowUpFilter'; +import { FormSubmissionsMediaFilesFilter } from '@/features/filtering/components/FormSubmissionsMediaFilesFilter'; +import { FormSubmissionsQuestionNotesFilter } from '@/features/filtering/components/FormSubmissionsQuestionNotesFilter'; +import { FormSubmissionsQuestionsAnsweredFilter } from '@/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter'; +import { IncidentReportsLocationTypeFilter } from '@/features/filtering/components/LocationTypeFilters'; export function IncidentReportsFiltersByEntry(): FunctionComponent { - const navigate = useNavigate({ from: '/responses' }); - const search = Route.useSearch(); - const setPrevSearch = useSetPrevSearch(); - const { filteringIsActive } = useFilteringContainer(); - const [isFiltering, setIsFiltering] = useState(filteringIsActive); - - const navigateHandler = useCallback( - (search: Record) => { - void navigate({ - // @ts-ignore - search: (prev) => { - const newSearch: Record = { - ...prev, - ...search, - }; - setPrevSearch(newSearch); - return newSearch; - }, - }); - }, - [navigate, setPrevSearch] - ); - - const onClearFilter = useCallback( - (filter: keyof FormSubmissionsSearchParams | (keyof FormSubmissionsSearchParams)[]) => () => { - const filters = Array.isArray(filter) - ? Object.fromEntries(filter.map((key) => [key, undefined])) - : { [filter]: undefined }; - navigateHandler(filters); - }, - [navigateHandler] - ); - return ( - <> - - - - - - - - - - - - - - - - + + + + + + + + - - - - {isFiltering && ( -
- {search.incidentReportLocationType && ( - - )} - - {search.incidentReportFollowUpStatus && ( - - )} - - {search.formIsCompleted && ( - - )} - - {search.hasFlaggedAnswers && ( - - )} - - {search.level1Filter && ( - - )} - - {search.level2Filter && ( - - )} - - {search.level3Filter && ( - - )} - - {search.level4Filter && ( - - )} - - {search.level5Filter && ( - - )} - - {search.pollingStationNumberFilter && ( - - )} - - {search.questionsAnswered && ( - - )} - - {search.hasNotes && ( - - )} - - {search.formIsCompleted && ( - - )} -
- )} - +
); } diff --git a/web/src/features/responses/components/IncidentReportsFiltersByForm/IncidentReportsFiltersByForm.tsx b/web/src/features/responses/components/IncidentReportsFiltersByForm/IncidentReportsFiltersByForm.tsx index a7cfefff8..3d437c367 100644 --- a/web/src/features/responses/components/IncidentReportsFiltersByForm/IncidentReportsFiltersByForm.tsx +++ b/web/src/features/responses/components/IncidentReportsFiltersByForm/IncidentReportsFiltersByForm.tsx @@ -1,338 +1,29 @@ -import { useSetPrevSearch } from '@/common/prev-search-store'; -import { FunctionComponent, IncidentReportFollowUpStatus, QuestionsAnswered } from '@/common/types'; +import { FunctionComponent } from '@/common/types'; import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; -import { FilterBadge } from '@/components/ui/badge'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { FILTER_KEY } from '@/features/filtering/filtering-enums'; -import { Route } from '@/routes/responses'; -import { useNavigate } from '@tanstack/react-router'; -import { useCallback, useState } from 'react'; -import { IncidentReportLocationType } from '../../models/incident-report'; -import type { FormSubmissionsSearchParams } from '../../models/search-params'; -import { - mapIncidentReportFollowUpStatus, - mapIncidentReportLocationType, - mapQuestionsAnswered, -} from '../../utils/helpers'; -import { ResetFiltersButton } from '../ResetFiltersButton/ResetFiltersButton'; -import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { FormSubmissionsCompletionFilter } from '@/features/filtering/components/FormSubmissionsCompletionFilter'; +import { FormSubmissionsFlaggedAnswersFilter } from '@/features/filtering/components/FormSubmissionsFlaggedAnswersFilter'; +import { FormSubmissionsFollowUpFilter } from '@/features/filtering/components/FormSubmissionsFollowUpFilter'; +import { FormSubmissionsFormFilter } from '@/features/filtering/components/FormSubmissionsFormFilter'; +import { FormSubmissionsMediaFilesFilter } from '@/features/filtering/components/FormSubmissionsMediaFilesFilter'; +import { FormSubmissionsQuestionNotesFilter } from '@/features/filtering/components/FormSubmissionsQuestionNotesFilter'; +import { FormSubmissionsQuestionsAnsweredFilter } from '@/features/filtering/components/FormSubmissionsQuestionsAnsweredFilter'; +import { IncidentReportsLocationTypeFilter } from '@/features/filtering/components/LocationTypeFilters'; export function IncidentReportsFiltersByForm(): FunctionComponent { - const navigate = useNavigate({ from: '/responses' }); - const search = Route.useSearch(); - const setPrevSearch = useSetPrevSearch(); - const { filteringIsActive } = useFilteringContainer(); - const [isFiltering, setIsFiltering] = useState(filteringIsActive); - - const navigateHandler = useCallback( - (search: Record) => { - void navigate({ - // @ts-ignore - search: (prev) => { - const newSearch: Record = { - ...prev, - ...search, - }; - setPrevSearch(newSearch); - return newSearch; - }, - }); - }, - [navigate, setPrevSearch] - ); - - const onClearFilter = useCallback( - (filter: keyof FormSubmissionsSearchParams | (keyof FormSubmissionsSearchParams)[]) => () => { - const filters = Array.isArray(filter) - ? Object.fromEntries(filter.map((key) => [key, undefined])) - : { [filter]: undefined }; - navigateHandler(filters); - }, - [navigateHandler] - ); - return ( <> - - - - - - - - - - - - - - - - - - - - - {isFiltering && ( -
- {search.incidentReportLocationType && ( - - )} - - {search.incidentReportFollowUpStatus && ( - - )} - - {search.formIsCompleted && ( - - )} - - {search.hasFlaggedAnswers && ( - - )} - - {search.level1Filter && ( - - )} - - {search.level2Filter && ( - - )} - - {search.level3Filter && ( - - )} - - {search.level4Filter && ( - - )} - - {search.level5Filter && ( - - )} - - {search.pollingStationNumberFilter && ( - - )} - - {search.questionsAnswered && ( - - )} - - {search.hasNotes && ( - - )} - - {search.formIsCompleted && ( - - )} -
- )} + + + + + + + + + + + ); } diff --git a/web/src/features/responses/components/IncidentReportsFiltersByObserver/IncidentReportsFiltersByObserver.tsx b/web/src/features/responses/components/IncidentReportsFiltersByObserver/IncidentReportsFiltersByObserver.tsx index b08c01545..de397f4f6 100644 --- a/web/src/features/responses/components/IncidentReportsFiltersByObserver/IncidentReportsFiltersByObserver.tsx +++ b/web/src/features/responses/components/IncidentReportsFiltersByObserver/IncidentReportsFiltersByObserver.tsx @@ -1,124 +1,15 @@ -import { IncidentReportFollowUpStatus, type FunctionComponent } from '@/common/types'; -import { FilterBadge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { FILTER_KEY } from '@/features/filtering/filtering-enums'; -import { useMonitoringObserversTags } from '@/hooks/tags-queries'; -import { Route } from '@/routes/responses'; -import { ChevronDownIcon } from '@heroicons/react/24/outline'; -import { useNavigate } from '@tanstack/react-router'; -import { useCallback, useState } from 'react'; -import type { FormSubmissionsSearchParams } from '../../models/search-params'; -import { mapIncidentReportFollowUpStatus } from '../../utils/helpers'; -import { ResetFiltersButton } from '../ResetFiltersButton/ResetFiltersButton'; -import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; +import { type FunctionComponent } from '@/common/types'; +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { FormSubmissionsFollowUpFilter } from '@/features/filtering/components/FormSubmissionsFollowUpFilter'; +import { MonitoringObserverTagsSelect } from '@/features/monitoring-observers/filtering/MonitoringObserverTagsSelect'; export function IncidentReportsFiltersByObserver(): FunctionComponent { - const navigate = useNavigate({ from: '/responses' }); - const search = Route.useSearch(); - const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); - const { filteringIsActive } = useFilteringContainer(); - const [isFiltering, setIsFiltering] = useState(filteringIsActive); - - const { data: tags } = useMonitoringObserversTags(currentElectionRoundId); - - const onTagsFilterChange = useCallback( - (tag: string) => () => { - void navigate({ - // @ts-ignore - search: (prev: FormSubmissionsSearchParams) => { - const prevTagsFilter = prev.tagsFilter ?? []; - const newTags = prevTagsFilter.includes(tag) - ? prevTagsFilter.filter((t) => t !== tag) - : [...prevTagsFilter, tag]; - - return { ...prev, tagsFilter: newTags.length > 0 ? newTags : undefined }; - }, - }); - }, - [navigate] - ); - - const onFollowUpFilterChange = useCallback( - (followUpStatus: string) => { - void navigate({ - // @ts-ignore - search: (prev: FormSubmissionsSearchParams) => { - return { ...prev, incidentReportFollowUpStatus: followUpStatus !== 'ALL' ? followUpStatus : undefined }; - }, - }); - }, - [navigate] - ); - return ( <> - - - - - - - - - {tags?.map((tag) => ( - - {tag} - - ))} - - - - - - {isFiltering && ( -
- {search.incidentReportFollowUpStatus && ( - onFollowUpFilterChange('ALL')} - /> - )} - {search.tagsFilter?.map((tag) => ( - - ))} -
- )} + + + + ); } diff --git a/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx b/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx index 5d09aee63..e94986e94 100644 --- a/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx +++ b/web/src/features/responses/components/IncidentReportsTab/IncidentReportsTab.tsx @@ -10,7 +10,7 @@ import { } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; -import { ChevronDownIcon, FunnelIcon } from '@heroicons/react/24/outline'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; import { useNavigate } from '@tanstack/react-router'; import { useDebounce } from '@uidotdev/usehooks'; import { useMemo, useState, type ChangeEvent } from 'react'; @@ -19,6 +19,7 @@ import type { IncidentReportsViewBy } from '../../utils/column-visibility-option import { ExportDataButton } from '../ExportDataButton/ExportDataButton'; import { FunctionComponent } from '@/common/types'; +import { FilteringIcon } from '@/features/filtering/components/FilteringIcon'; import { FILTER_KEY } from '@/features/filtering/filtering-enums'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { Route } from '@/routes/responses'; @@ -39,12 +40,10 @@ const viewBy: Record = { export default function IncidentReportsTab(): FunctionComponent { const navigate = useNavigate(); const search = Route.useSearch(); - const { filteringIsActive } = useFilteringContainer(); + const { filteringIsExpanded, setFilteringIsExpanded } = useFilteringContainer(); const { viewBy: byFilter } = search; - const [isFiltering, setIsFiltering] = useState(filteringIsActive); - const [searchText, setSearchText] = useState(''); const debouncedSearchText = useDebounce(searchText, 300); const setPrevSearch = useSetPrevSearch(); @@ -56,6 +55,7 @@ export default function IncidentReportsTab(): FunctionComponent { const queryParams = useMemo(() => { const params = [ ['searchText', searchText], + ['formId', search.formId], ['hasFlaggedAnswers', search.hasFlaggedAnswers], ['level1Filter', search.level1Filter], ['level2Filter', search.level2Filter], @@ -63,7 +63,7 @@ export default function IncidentReportsTab(): FunctionComponent { ['level4Filter', search.level4Filter], ['level5Filter', search.level5Filter], ['pollingStationNumberFilter', search.pollingStationNumberFilter], - ['followUpStatus', search.incidentReportFollowUpStatus], + ['followUpStatus', search.followUpStatus], ['locationType', search.incidentReportLocationType], ].filter(([_, value]) => value); @@ -94,7 +94,7 @@ export default function IncidentReportsTab(): FunctionComponent { onValueChange={(value) => { setPrevSearch({ [FILTER_KEY.ViewBy]: value, [FILTER_KEY.Tab]: 'incident-reports' }); void navigate({ search: { [FILTER_KEY.ViewBy]: value, [FILTER_KEY.Tab]: 'incident-reports' } }); - setIsFiltering(false); + setFilteringIsExpanded(false); }} value={byFilter}> {Object.entries(viewBy).map(([value, label]) => ( @@ -113,13 +113,7 @@ export default function IncidentReportsTab(): FunctionComponent {
<> - { - setIsFiltering((prev) => !prev); - }} - /> + @@ -127,8 +121,8 @@ export default function IncidentReportsTab(): FunctionComponent { - {isFiltering && ( -
+ {filteringIsExpanded && ( +
{byFilter === 'byEntry' && } {byFilter === 'byObserver' && } {byFilter === 'byForm' && } diff --git a/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx b/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx index 2f97305cc..df0345b84 100644 --- a/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx +++ b/web/src/features/responses/components/QuickReportsTab/QuickReportsTab.tsx @@ -1,8 +1,7 @@ import { useSetPrevSearch } from '@/common/prev-search-store'; -import { QuickReportFollowUpStatus, type FunctionComponent } from '@/common/types'; +import { type FunctionComponent } from '@/common/types'; import { PollingStationsFilters } from '@/components/PollingStationsFilters/PollingStationsFilters'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; -import { FilterBadge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DropdownMenu, @@ -12,24 +11,25 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { FilteringIcon } from '@/features/filtering/components/FilteringIcon'; +import { FormSubmissionsFollowUpFilter } from '@/features/filtering/components/FormSubmissionsFollowUpFilter'; +import { QuickReportsLocationTypeFilter } from '@/features/filtering/components/LocationTypeFilters'; +import { QuickReportsIncidentCategoryFilter } from '@/features/filtering/components/QuickReportsIncidentCategoryFilter'; import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; -import { Cog8ToothIcon, FunnelIcon } from '@heroicons/react/24/outline'; +import { Cog8ToothIcon } from '@heroicons/react/24/outline'; import { getRouteApi } from '@tanstack/react-router'; import { useDebounce } from '@uidotdev/usehooks'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { useQuickReports } from '../../hooks/quick-reports'; import { ExportedDataType } from '../../models/data-export'; -import { IncidentCategoryList, QuickReportLocationType } from '../../models/quick-report'; import type { QuickReportsSearchParams } from '../../models/search-params'; import { useQuickReportsColumnsVisibility, useQuickReportsToggleColumn } from '../../store/column-visibility'; import { quickReportsColumnDefs } from '../../utils/column-defs'; import { quickReportsColumnVisibilityOptions } from '../../utils/column-visibility-options'; -import { mapIncidentCategory, mapQuickReportFollowUpStatus, mapQuickReportLocationType } from '../../utils/helpers'; import { ExportDataButton } from '../ExportDataButton/ExportDataButton'; -import { ResetFiltersButton } from '../ResetFiltersButton/ResetFiltersButton'; const routeApi = getRouteApi('/responses/'); @@ -40,9 +40,7 @@ export function QuickReportsTab(): FunctionComponent { const columnsVisibility = useQuickReportsColumnsVisibility(); const toggleColumns = useQuickReportsToggleColumn(); - const { filteringIsActive } = useFilteringContainer(); - - const [isFiltering, setIsFiltering] = useState(filteringIsActive); + const { filteringIsExpanded, setFilteringIsExpanded } = useFilteringContainer(); const queryParams = useMemo(() => { const params = [ @@ -53,7 +51,7 @@ export function QuickReportsTab(): FunctionComponent { ['level4Filter', debouncedSearch.level4Filter], ['level5Filter', debouncedSearch.level5Filter], ['pollingStationNumberFilter', debouncedSearch.pollingStationNumberFilter], - ['quickReportFollowUpStatus', debouncedSearch.quickReportFollowUpStatus], + ['quickReportFollowUpStatus', debouncedSearch.followUpStatus], ['quickReportLocationType', debouncedSearch.quickReportLocationType], ['incidentCategory', debouncedSearch.incidentCategory], ].filter(([_, value]) => value); @@ -99,13 +97,7 @@ export function QuickReportsTab(): FunctionComponent {
- { - setIsFiltering((prev) => !prev); - }} - /> + @@ -131,158 +123,15 @@ export function QuickReportsTab(): FunctionComponent { - {isFiltering && ( -
- - - - - - - - - - {isFiltering && ( -
- {search.quickReportFollowUpStatus && ( - - )} - {search.quickReportLocationType && ( - - )} - {search.incidentCategory && ( - - )} - {search.level1Filter && ( - - )} - - {search.level2Filter && ( - - )} - - {search.level3Filter && ( - - )} - - {search.level4Filter && ( - - )} - - {search.level5Filter && ( - - )} - - {search.pollingStationNumberFilter && ( - - )} -
- )} -
+ {filteringIsExpanded && ( + <> + + + + + + + )} diff --git a/web/src/features/responses/models/search-params.ts b/web/src/features/responses/models/search-params.ts index 5e904fc80..e4af7f128 100644 --- a/web/src/features/responses/models/search-params.ts +++ b/web/src/features/responses/models/search-params.ts @@ -7,12 +7,15 @@ import { QuickReportFollowUpStatus, } from '@/common/types'; import { z } from 'zod'; -import { IncidentCategory, QuickReportLocationType } from './quick-report'; import { IncidentReportLocationType } from './incident-report'; +import { IncidentCategory, QuickReportLocationType } from './quick-report'; export const ResponsesPageSearchParamsSchema = z.object({ viewBy: z.enum(['byEntry', 'byObserver', 'byForm']).catch('byEntry').default('byEntry'), - tab: z.enum(['form-answers', 'quick-reports','citizen-reports','incident-reports']).catch('form-answers').optional(), + tab: z + .enum(['form-answers', 'quick-reports', 'citizen-reports', 'incident-reports']) + .catch('form-answers') + .optional(), }); export const FormSubmissionsSearchParamsSchema = ResponsesPageSearchParamsSchema.merge( @@ -27,7 +30,7 @@ export const FormSubmissionsSearchParamsSchema = ResponsesPageSearchParamsSchema pollingStationNumberFilter: z.string().catch('').optional(), hasFlaggedAnswers: z.string().catch('').optional(), monitoringObserverId: z.string().catch('').optional(), - tagsFilter: z.array(z.string()).optional().catch([]).optional(), + tags: z.array(z.string()).optional().catch([]).optional(), followUpStatus: z.nativeEnum(FormSubmissionFollowUpStatus).optional(), quickReportFollowUpStatus: z.nativeEnum(QuickReportFollowUpStatus).optional(), citizenReportFollowUpStatus: z.nativeEnum(CitizenReportFollowUpStatus).optional(), @@ -45,7 +48,8 @@ export const FormSubmissionsSearchParamsSchema = ResponsesPageSearchParamsSchema submissionsFromDate: z.coerce.date().optional(), submissionsToDate: z.coerce.date().optional(), - })); + }) +); export type FormSubmissionsSearchParams = z.infer; @@ -64,16 +68,17 @@ export const QuickReportsSearchParamsSchema = z.object({ export type QuickReportsSearchParams = z.infer; export const CitizenReportsSearchParamsSchema = z.object({ - citizenReportFollowUpStatus: z - .nativeEnum(CitizenReportFollowUpStatus) - .optional(), + citizenReportFollowUpStatus: z.nativeEnum(CitizenReportFollowUpStatus).optional(), }); export type CitizenReportsSearchParams = z.infer; export const IncidentReportsSearchParamsSchema = z.object({ viewBy: z.enum(['byEntry', 'byObserver', 'byForm']).catch('byEntry').default('byEntry'), - tab: z.enum(['form-answers', 'quick-reports', 'citizen-reports', 'incident-reports']).catch('form-answers').optional(), + tab: z + .enum(['form-answers', 'quick-reports', 'citizen-reports', 'incident-reports']) + .catch('form-answers') + .optional(), searchText: z.string().catch('').optional(), level1Filter: z.string().catch('').optional(), level2Filter: z.string().catch('').optional(),