Skip to content

Commit f2d638e

Browse files
committed
Addinf phone number FE field
1 parent be1e9ba commit f2d638e

File tree

8 files changed

+248
-8
lines changed

8 files changed

+248
-8
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { fireEvent } from '@testing-library/react';
3+
import { renderWithProviders } from 'src/testUtils/testUtils';
4+
import { PeopleFilter } from './PeopleFilter';
5+
6+
vi.mock('@hooks/useBaseUrl', () => ({
7+
useBaseUrl: () => ({
8+
businessArea: 'afghanistan',
9+
programId: 'test-program',
10+
baseUrl: 'afghanistan/test-program',
11+
isAllPrograms: false,
12+
}),
13+
}));
14+
15+
vi.mock('src/programContext', () => ({
16+
useProgramContext: () => ({
17+
selectedProgram: {
18+
dataCollectingType: { type: 'STANDARD' },
19+
beneficiaryGroup: {
20+
memberLabel: 'Individual',
21+
groupLabel: 'Household',
22+
groupLabelPlural: 'Households',
23+
},
24+
},
25+
}),
26+
}));
27+
28+
vi.mock('react-i18next', () => ({
29+
useTranslation: () => ({ t: (key: string) => key }),
30+
}));
31+
32+
vi.mock('react-router-dom', async () => {
33+
const actual = await vi.importActual<typeof import('react-router-dom')>(
34+
'react-router-dom',
35+
);
36+
return {
37+
...actual,
38+
useNavigate: () => vi.fn(),
39+
useLocation: () => ({
40+
pathname: '/test',
41+
search: '',
42+
hash: '',
43+
state: null,
44+
key: 'test',
45+
}),
46+
};
47+
});
48+
49+
const initialFilter = {
50+
search: '',
51+
phone: '',
52+
documentType: '',
53+
documentNumber: '',
54+
admin1: '',
55+
admin2: '',
56+
sex: '',
57+
ageMin: '',
58+
ageMax: '',
59+
flags: [],
60+
orderBy: 'unicef_id',
61+
status: '',
62+
lastRegistrationDateMin: '',
63+
lastRegistrationDateMax: '',
64+
rdiId: '',
65+
};
66+
67+
const choicesData = {
68+
documentTypeChoices: [],
69+
flagChoices: [],
70+
sexChoices: [],
71+
statusChoices: [],
72+
} as any;
73+
74+
describe('PeopleFilter — phone input apply-gate', () => {
75+
let setFilter: (filter: any) => void;
76+
let setAppliedFilter: (filter: any) => void;
77+
78+
beforeEach(() => {
79+
setFilter = vi.fn();
80+
setAppliedFilter = vi.fn();
81+
});
82+
83+
it('blocks Apply and does NOT call setAppliedFilter when phone has < 4 digits', () => {
84+
const { container } = renderWithProviders(
85+
<PeopleFilter
86+
filter={{ ...initialFilter, phone: '123' }}
87+
choicesData={choicesData}
88+
setFilter={setFilter}
89+
initialFilter={initialFilter}
90+
appliedFilter={initialFilter}
91+
setAppliedFilter={setAppliedFilter}
92+
/>,
93+
);
94+
95+
const applyButton = container.querySelector(
96+
'[data-cy="button-filters-apply"]',
97+
) as HTMLElement;
98+
expect(applyButton).toBeTruthy();
99+
fireEvent.click(applyButton);
100+
101+
expect(setAppliedFilter).not.toHaveBeenCalled();
102+
});
103+
104+
it('blocks Apply when spaced digits total < 4 after stripping', () => {
105+
const { container } = renderWithProviders(
106+
<PeopleFilter
107+
filter={{ ...initialFilter, phone: ' 1 2 3 ' }}
108+
choicesData={choicesData}
109+
setFilter={setFilter}
110+
initialFilter={initialFilter}
111+
appliedFilter={initialFilter}
112+
setAppliedFilter={setAppliedFilter}
113+
/>,
114+
);
115+
116+
const applyButton = container.querySelector(
117+
'[data-cy="button-filters-apply"]',
118+
) as HTMLElement;
119+
fireEvent.click(applyButton);
120+
121+
expect(setAppliedFilter).not.toHaveBeenCalled();
122+
});
123+
124+
it('fires Apply (setAppliedFilter called) when phone has exactly 4 digits', () => {
125+
const { container } = renderWithProviders(
126+
<PeopleFilter
127+
filter={{ ...initialFilter, phone: '1234' }}
128+
choicesData={choicesData}
129+
setFilter={setFilter}
130+
initialFilter={initialFilter}
131+
appliedFilter={initialFilter}
132+
setAppliedFilter={setAppliedFilter}
133+
/>,
134+
);
135+
136+
const applyButton = container.querySelector(
137+
'[data-cy="button-filters-apply"]',
138+
) as HTMLElement;
139+
fireEvent.click(applyButton);
140+
141+
expect(setAppliedFilter).toHaveBeenCalledTimes(1);
142+
expect(setAppliedFilter).toHaveBeenCalledWith(
143+
expect.objectContaining({ phone: '1234' }),
144+
);
145+
});
146+
147+
it('fires Apply when phone is empty (unfiltered baseline, no gate)', () => {
148+
const { container } = renderWithProviders(
149+
<PeopleFilter
150+
filter={{ ...initialFilter, phone: '' }}
151+
choicesData={choicesData}
152+
setFilter={setFilter}
153+
initialFilter={initialFilter}
154+
appliedFilter={initialFilter}
155+
setAppliedFilter={setAppliedFilter}
156+
/>,
157+
);
158+
159+
const applyButton = container.querySelector(
160+
'[data-cy="button-filters-apply"]',
161+
) as HTMLElement;
162+
fireEvent.click(applyButton);
163+
164+
expect(setAppliedFilter).toHaveBeenCalledTimes(1);
165+
});
166+
});

src/frontend/src/components/people/PeopleFilter.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Grid, MenuItem } from '@mui/material';
1010
import { AdminAreaAutocomplete } from '@shared/autocompletes/AdminAreaAutocomplete';
1111
import { generateTableOrderOptionsMember } from '@utils/constants';
1212
import { createHandleApplyFilterChange } from '@utils/utils';
13-
import { ReactElement } from 'react';
13+
import { ReactElement, useState } from 'react';
1414
import { useTranslation } from 'react-i18next';
1515
import { useLocation, useNavigate } from 'react-router-dom';
1616
import { useProgramContext } from '../../programContext';
@@ -56,11 +56,23 @@ export function PeopleFilter({
5656
appliedFilter,
5757
setAppliedFilter,
5858
);
59+
60+
const [phoneError, setPhoneError] = useState<string | null>(null);
61+
5962
const handleApplyFilter = (): void => {
63+
if (filter.phone) {
64+
const digitCount = filter.phone.replace(/\D/g, '').length;
65+
if (digitCount < 4) {
66+
setPhoneError(t('Phone search requires at least 4 digits'));
67+
return;
68+
}
69+
}
70+
setPhoneError(null);
6071
applyFilterChanges();
6172
};
6273

6374
const handleClearFilter = (): void => {
75+
setPhoneError(null);
6476
clearFilter();
6577
};
6678

@@ -86,6 +98,19 @@ export function PeopleFilter({
8698
data-cy="ind-filters-search"
8799
/>
88100
</Grid>
101+
<Grid size={{ xs: 3 }}>
102+
<SearchTextField
103+
label={t('Phone number')}
104+
value={filter.phone}
105+
onChange={(e) => {
106+
handleFilterChange('phone', e.target.value);
107+
if (phoneError) setPhoneError(null);
108+
}}
109+
data-cy="ind-filters-phone"
110+
error={!!phoneError}
111+
helperText={phoneError ?? ''}
112+
/>
113+
</Grid>
89114
<Grid container size={{ xs: 6 }} spacing={3}>
90115
<Grid size={{ xs: 6 }}>
91116
<SelectFilter

src/frontend/src/components/population/IndividualsFilter.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { SearchTextField } from '@core/SearchTextField';
1414
import { SelectFilter } from '@core/SelectFilter';
1515
import { useProgramContext } from '../../programContext';
1616
import { DocumentSearchField } from '@core/DocumentSearchField';
17-
import { ReactElement } from 'react';
17+
import { ReactElement, useState } from 'react';
1818
import { ProgramList } from '@restgenerated/models/ProgramList';
1919
import { IndividualChoices } from '@restgenerated/models/IndividualChoices';
2020
import { RdiAutocompleteRestFilter } from '@shared/autocompletes/RdiAutocompleteRestFilter';
@@ -57,11 +57,23 @@ export function IndividualsFilter({
5757
appliedFilter,
5858
setAppliedFilter,
5959
);
60+
61+
const [phoneError, setPhoneError] = useState<string | null>(null);
62+
6063
const handleApplyFilter = (): void => {
64+
if (filter.phone) {
65+
const digitCount = filter.phone.replace(/\D/g, '').length;
66+
if (digitCount < 4) {
67+
setPhoneError(t('Phone search requires at least 4 digits'));
68+
return;
69+
}
70+
}
71+
setPhoneError(null);
6172
applyFilterChanges();
6273
};
6374

6475
const handleClearFilter = (): void => {
76+
setPhoneError(null);
6577
clearFilter();
6678
};
6779

@@ -87,6 +99,19 @@ export function IndividualsFilter({
8799
data-cy="ind-filters-search"
88100
/>
89101
</Grid>
102+
<Grid size={{ xs: 3 }}>
103+
<SearchTextField
104+
label={t('Phone number')}
105+
value={filter.phone}
106+
onChange={(e) => {
107+
handleFilterChange('phone', e.target.value);
108+
if (phoneError) setPhoneError(null);
109+
}}
110+
data-cy="ind-filters-phone"
111+
error={!!phoneError}
112+
helperText={phoneError ?? ''}
113+
/>
114+
</Grid>
90115
<DocumentSearchField
91116
onChange={handleFilterChange}
92117
type={filter.documentType}

src/frontend/src/containers/pages/people/PeoplePage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const PeoplePage = (): ReactElement => {
4747

4848
const initialFilter = {
4949
search: '',
50+
phone: '',
5051
documentType: '',
5152
documentNumber: '',
5253
admin1: '',

src/frontend/src/containers/pages/population/HouseholdMembersPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const HouseholdMembersPage = (): ReactElement => {
4242

4343
const initialFilter = {
4444
search: '',
45+
phone: '',
4546
documentType: individualChoicesData?.documentTypeChoices?.[0]?.value,
4647
documentNumber: '',
4748
admin2: '',

src/frontend/src/containers/tables/people/PeopleListTable/PeopleListTable.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const PeopleListTable = ({
3737
ageMin: filter.ageMin,
3838
sex: [filter.sex],
3939
search: filter.search.trim(),
40+
phone: filter.phone?.trim() ?? '',
4041
documentType: filter.documentType,
4142
documentNumber: filter.documentNumber.trim(),
4243
admin1: [filter.admin1],
@@ -55,6 +56,7 @@ export const PeopleListTable = ({
5556
filter.ageMax,
5657
filter.sex,
5758
filter.search,
59+
filter.phone,
5860
filter.documentType,
5961
filter.documentNumber,
6062
filter.admin1,

src/frontend/src/containers/tables/population/IndividualsListTable/IndividualsListTable.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function IndividualsListTable({
4242
ageMin: filter.ageMin,
4343
sex: [filter.sex],
4444
search: filter.search.trim(),
45+
phone: filter.phone?.trim() ?? '',
4546
documentType: filter.documentType,
4647
documentNumber: filter.documentNumber.trim(),
4748
admin2: filter.admin2,
@@ -59,6 +60,7 @@ export function IndividualsListTable({
5960
filter.ageMax,
6061
filter.sex,
6162
filter.search,
63+
filter.phone,
6264
filter.documentType,
6365
filter.documentNumber,
6466
filter.admin2,
@@ -81,6 +83,7 @@ export function IndividualsListTable({
8183
filter.ageMax,
8284
filter.sex,
8385
filter.search,
86+
filter.phone,
8487
filter.documentType,
8588
filter.documentNumber,
8689
filter.admin2,
@@ -137,6 +140,7 @@ export function IndividualsListTable({
137140
businessArea,
138141
programId,
139142
filter.search,
143+
filter.phone,
140144
filter.sex,
141145
filter.documentType,
142146
filter.documentNumber,
@@ -158,6 +162,7 @@ export function IndividualsListTable({
158162
ageMin: filter.ageMin,
159163
sex: [filter.sex],
160164
search: filter.search?.trim(),
165+
phone: filter.phone?.trim() ?? '',
161166
documentType: filter.documentType,
162167
documentNumber: filter.documentNumber?.trim(),
163168
admin2: filter.admin2,

0 commit comments

Comments
 (0)