Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -86,6 +86,7 @@ export function LookUpHouseholdIndividualSelectionDetail({
const initialFilterIND = {
program: isAllPrograms ? '' : programId,
search: '',
phone: '',
documentType: individualChoicesData?.documentTypeChoices?.[0]?.value,
documentNumber: '',
admin2: '',
Expand All @@ -96,6 +97,7 @@ export function LookUpHouseholdIndividualSelectionDetail({
orderBy: 'unicef_id',
status: '',
programState: 'active',
birthDate: '',
};

const [filterIND, setFilterIND] = useState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function LookUpReassignRoleModal({

const initialFilterIND = {
search: '',
phone: '',
documentType: individualChoicesData?.documentTypeChoices?.[0]?.value,
documentNumber: '',
admin2: '',
Expand All @@ -101,6 +102,7 @@ export function LookUpReassignRoleModal({
orderBy: 'unicef_id',
status: '',
household: '',
birthDate: '',
};

if (household) {
Expand Down
166 changes: 166 additions & 0 deletions src/frontend/src/components/people/PeopleFilter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent } from '@testing-library/react';
import { renderWithProviders } from 'src/testUtils/testUtils';
import { PeopleFilter } from './PeopleFilter';

vi.mock('@hooks/useBaseUrl', () => ({
useBaseUrl: () => ({
businessArea: 'afghanistan',
programId: 'test-program',
baseUrl: 'afghanistan/test-program',
isAllPrograms: false,
}),
}));

vi.mock('src/programContext', () => ({
useProgramContext: () => ({
selectedProgram: {
dataCollectingType: { type: 'STANDARD' },
beneficiaryGroup: {
memberLabel: 'Individual',
groupLabel: 'Household',
groupLabelPlural: 'Households',
},
},
}),
}));

vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));

vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>(
'react-router-dom',
);
return {
...actual,
useNavigate: () => vi.fn(),
useLocation: () => ({
pathname: '/test',
search: '',
hash: '',
state: null,
key: 'test',
}),
};
});

const initialFilter = {
search: '',
phone: '',
documentType: '',
documentNumber: '',
admin1: '',
admin2: '',
sex: '',
ageMin: '',
ageMax: '',
flags: [],
orderBy: 'unicef_id',
status: '',
lastRegistrationDateMin: '',
lastRegistrationDateMax: '',
rdiId: '',
};

const choicesData = {
documentTypeChoices: [],
flagChoices: [],
sexChoices: [],
statusChoices: [],
} as any;

describe('PeopleFilter — phone input apply-gate', () => {
let setFilter: (filter: any) => void;
let setAppliedFilter: (filter: any) => void;

beforeEach(() => {
setFilter = vi.fn();
setAppliedFilter = vi.fn();
});

it('blocks Apply and does NOT call setAppliedFilter when phone has < 4 digits', () => {
const { container } = renderWithProviders(
<PeopleFilter
filter={{ ...initialFilter, phone: '123' }}
choicesData={choicesData}
setFilter={setFilter}
initialFilter={initialFilter}
appliedFilter={initialFilter}
setAppliedFilter={setAppliedFilter}
/>,
);

const applyButton = container.querySelector(
'[data-cy="button-filters-apply"]',
) as HTMLElement;
expect(applyButton).toBeTruthy();
fireEvent.click(applyButton);

expect(setAppliedFilter).not.toHaveBeenCalled();
});

it('blocks Apply when spaced digits total < 4 after stripping', () => {
const { container } = renderWithProviders(
<PeopleFilter
filter={{ ...initialFilter, phone: ' 1 2 3 ' }}
choicesData={choicesData}
setFilter={setFilter}
initialFilter={initialFilter}
appliedFilter={initialFilter}
setAppliedFilter={setAppliedFilter}
/>,
);

const applyButton = container.querySelector(
'[data-cy="button-filters-apply"]',
) as HTMLElement;
fireEvent.click(applyButton);

expect(setAppliedFilter).not.toHaveBeenCalled();
});

it('fires Apply (setAppliedFilter called) when phone has exactly 4 digits', () => {
const { container } = renderWithProviders(
<PeopleFilter
filter={{ ...initialFilter, phone: '1234' }}
choicesData={choicesData}
setFilter={setFilter}
initialFilter={initialFilter}
appliedFilter={initialFilter}
setAppliedFilter={setAppliedFilter}
/>,
);

const applyButton = container.querySelector(
'[data-cy="button-filters-apply"]',
) as HTMLElement;
fireEvent.click(applyButton);

expect(setAppliedFilter).toHaveBeenCalledTimes(1);
expect(setAppliedFilter).toHaveBeenCalledWith(
expect.objectContaining({ phone: '1234' }),
);
});

it('fires Apply when phone is empty (unfiltered baseline, no gate)', () => {
const { container } = renderWithProviders(
<PeopleFilter
filter={{ ...initialFilter, phone: '' }}
choicesData={choicesData}
setFilter={setFilter}
initialFilter={initialFilter}
appliedFilter={initialFilter}
setAppliedFilter={setAppliedFilter}
/>,
);

const applyButton = container.querySelector(
'[data-cy="button-filters-apply"]',
) as HTMLElement;
fireEvent.click(applyButton);

expect(setAppliedFilter).toHaveBeenCalledTimes(1);
});
});
35 changes: 34 additions & 1 deletion src/frontend/src/components/people/PeopleFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Grid, MenuItem } from '@mui/material';
import { AdminAreaAutocomplete } from '@shared/autocompletes/AdminAreaAutocomplete';
import { generateTableOrderOptionsMember } from '@utils/constants';
import { createHandleApplyFilterChange } from '@utils/utils';
import { ReactElement } from 'react';
import { ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useProgramContext } from '../../programContext';
Expand Down Expand Up @@ -56,11 +56,23 @@ export function PeopleFilter({
appliedFilter,
setAppliedFilter,
);

const [phoneError, setPhoneError] = useState<string | null>(null);

const handleApplyFilter = (): void => {
if (filter.phone) {
const digitCount = filter.phone.replace(/\D/g, '').length;
if (digitCount < 4) {
setPhoneError(t('Phone search requires at least 4 digits'));
return;
}
}
setPhoneError(null);
applyFilterChanges();
};

const handleClearFilter = (): void => {
setPhoneError(null);
clearFilter();
};

Expand All @@ -86,6 +98,19 @@ export function PeopleFilter({
data-cy="ind-filters-search"
/>
</Grid>
<Grid size={{ xs: 3 }}>
<SearchTextField
label={t('Phone number')}
value={filter.phone}
onChange={(e) => {
handleFilterChange('phone', e.target.value);
if (phoneError) setPhoneError(null);
}}
data-cy="ind-filters-phone"
error={!!phoneError}
helperText={phoneError ?? ''}
/>
</Grid>
<Grid container size={{ xs: 6 }} spacing={3}>
<Grid size={{ xs: 6 }}>
<SelectFilter
Expand Down Expand Up @@ -220,6 +245,14 @@ export function PeopleFilter({
icon={<CakeIcon />}
/>
</Grid>
<Grid size={{ xs: 2 }}>
<DatePickerFilter
topLabel={t('Date of Birth')}
onChange={(date) => handleFilterChange('birthDate', date)}
value={filter.birthDate}
dataCy="ind-filters-birth-date"
/>
</Grid>
<Grid size={{ xs: 3 }}>
<SelectFilter
onChange={(e) => handleFilterChange('flags', e.target.value)}
Expand Down
35 changes: 34 additions & 1 deletion src/frontend/src/components/population/IndividualsFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { SearchTextField } from '@core/SearchTextField';
import { SelectFilter } from '@core/SelectFilter';
import { useProgramContext } from '../../programContext';
import { DocumentSearchField } from '@core/DocumentSearchField';
import { ReactElement } from 'react';
import { ReactElement, useState } from 'react';
import { ProgramList } from '@restgenerated/models/ProgramList';
import { IndividualChoices } from '@restgenerated/models/IndividualChoices';
import { RdiAutocompleteRestFilter } from '@shared/autocompletes/RdiAutocompleteRestFilter';
Expand Down Expand Up @@ -57,11 +57,23 @@ export function IndividualsFilter({
appliedFilter,
setAppliedFilter,
);

const [phoneError, setPhoneError] = useState<string | null>(null);

const handleApplyFilter = (): void => {
if (filter.phone) {
const digitCount = filter.phone.replace(/\D/g, '').length;
if (digitCount < 4) {
setPhoneError(t('Phone search requires at least 4 digits'));
return;
}
}
setPhoneError(null);
applyFilterChanges();
};

const handleClearFilter = (): void => {
setPhoneError(null);
clearFilter();
};

Expand All @@ -87,6 +99,19 @@ export function IndividualsFilter({
data-cy="ind-filters-search"
/>
</Grid>
<Grid size={{ xs: 3 }}>
<SearchTextField
label={t('Phone number')}
value={filter.phone}
onChange={(e) => {
handleFilterChange('phone', e.target.value);
if (phoneError) setPhoneError(null);
}}
data-cy="ind-filters-phone"
error={!!phoneError}
helperText={phoneError ?? ''}
/>
</Grid>
<DocumentSearchField
onChange={handleFilterChange}
type={filter.documentType}
Expand Down Expand Up @@ -181,6 +206,14 @@ export function IndividualsFilter({
icon={<CakeIcon />}
/>
</Grid>
<Grid size={{ xs: 2 }}>
<DatePickerFilter
topLabel={t('Date of Birth')}
onChange={(date) => handleFilterChange('birthDate', date)}
value={filter.birthDate}
dataCy="ind-filters-birth-date"
/>
</Grid>
<Grid size={{ xs: 3 }}>
<SelectFilter
onChange={(e) => handleFilterChange('flags', e.target.value)}
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/containers/pages/people/PeoplePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const PeoplePage = (): ReactElement => {

const initialFilter = {
search: '',
phone: '',
documentType: '',
documentNumber: '',
admin1: '',
Expand All @@ -60,6 +61,7 @@ const PeoplePage = (): ReactElement => {
lastRegistrationDateMin: '',
lastRegistrationDateMax: '',
rdiId: '',
birthDate: '',
};

const [filter, setFilter] = useState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const HouseholdMembersPage = (): ReactElement => {

const initialFilter = {
search: '',
phone: '',
documentType: individualChoicesData?.documentTypeChoices?.[0]?.value,
documentNumber: '',
admin2: '',
Expand All @@ -54,6 +55,7 @@ export const HouseholdMembersPage = (): ReactElement => {
lastRegistrationDateMin: '',
lastRegistrationDateMax: '',
rdiId: '',
birthDate: '',
};

const [filter, setFilter] = useState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const PeopleListTable = ({
ageMin: filter.ageMin,
sex: [filter.sex],
search: filter.search.trim(),
phone: filter.phone?.trim() ?? '',
documentType: filter.documentType,
documentNumber: filter.documentNumber.trim(),
admin1: [filter.admin1],
Expand All @@ -48,13 +49,15 @@ export const PeopleListTable = ({
rdiMergeStatus: 'MERGED',
orderBy: filter.orderBy,
rdiId: filter.rdiId,
birthDate: filter.birthDate,
page,
}),
[
filter.ageMin,
filter.ageMax,
filter.sex,
filter.search,
filter.phone,
filter.documentType,
filter.documentNumber,
filter.admin1,
Expand All @@ -68,6 +71,7 @@ export const PeopleListTable = ({
businessArea,
page,
filter.rdiId,
filter.birthDate,
],
);

Expand Down
Loading
Loading