Skip to content

Commit e7f9c8d

Browse files
fix filtering by country
1 parent 8e3a2f1 commit e7f9c8d

File tree

8 files changed

+106
-19
lines changed

8 files changed

+106
-19
lines changed

backend/app/Http/Controllers/UserController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function index(Request $request)
3030
{
3131
// Validate the request
3232
$request->validate([
33-
'country' => 'exists:countries,id',
33+
'country' => 'nullable|string|exists:countries,name',
3434
'dateFrom' => 'integer|nullable',
3535
'dateTo' => 'integer|nullable',
3636
'page' => 'integer|min:1',

backend/app/Models/User.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ public function scopeDateRange(Builder $query, $dateFrom = null, $dateTo = null)
4747
return $query;
4848
}
4949

50-
public function scopeCountry(Builder $query, $countryId = null)
50+
public function scopeCountry(Builder $query, $countryName = null)
5151
{
52-
if ($countryId) {
53-
return $query->where('country_id', $countryId);
52+
if ($countryName) {
53+
$country = Country::where('name', $countryName)->first();
54+
return $query->where('country_id', $country->id);
5455
}
5556

5657
return $query;

backend/tests/Unit/Routes/UsersFilterTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function testFilterUsers()
3434
'date_of_birth' => '1990-01-01',
3535
]);
3636
// Get the response from the route
37-
$response = $this->get('/api/users?country=' . $countries[1]->id);
37+
$response = $this->get('/api/users?country=' . $countries[1]->name);
3838
// Check that the response is a JSON
3939
$response->assertHeader('Content-Type', 'application/json');
4040
// Check that the response is contains the correct data for the filtered user
@@ -113,7 +113,7 @@ private function _setupUsers()
113113
'date_of_birth' => '1990-01-01',
114114
]);
115115
$query = [
116-
'country' => $country->id,
116+
'country' => $country->name,
117117
'dateFrom' => date_create_from_format('Y-m-d', '1990-01-01')->getTimestamp(),
118118
'dateTo' => date_create_from_format('Y-m-d', '1990-01-01')->getTimestamp(),
119119
'page' => 1,
@@ -192,6 +192,7 @@ public function testGetUsersAllPagesNPlus1()
192192
// get all users
193193
$query['perPage'] = count($users);
194194
$query['page'] = 1;
195+
$query['country'] = $country->name;
195196
$this->get('/api/users?' . http_build_query($query));
196197

197198
// Disable the query log and get the queries

frontend/src/__tests__/components/UsersChart.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import UsersChart, { getPieChartOption } from 'components/UsersChart';
1+
import { act, fireEvent, render } from '@testing-library/react';
2+
import { UsersChart, getPieChartOption } from 'components/UsersChart';
23

34
describe('getPieChartOption', () => {
45
it('should return the correct option for the pie chart', () => {
@@ -29,4 +30,36 @@ describe('getPieChartOption', () => {
2930
expect(option.series[0].type).toEqual(expectedOption.series[0].type);
3031
expect(option.series[0].data).toEqual(expectedOption.series[0].data);
3132
});
33+
34+
it('should return the correct option for selected country', () => {
35+
const countries = [
36+
{ id: 1, name: 'United States', users_count: 10 },
37+
{ id: 2, name: 'Canada', users_count: 5 },
38+
{ id: 3, name: 'Mexico', users_count: 0 },
39+
];
40+
const { series: [{ selectedMap, selectedMode }] } = getPieChartOption(countries, 'Canada');
41+
expect(selectedMode).toEqual('single');
42+
expect(selectedMap).toEqual({ Canada: true });
43+
});
44+
45+
// disabled because it's not working at all, due to the way echarts-for-react is implemented
46+
false && it('should call setCountry when a country is clicked', async () => {
47+
const setCountry = jest.fn();
48+
const countries = {
49+
data: [{ id: 1, name: 'United States', users_count: 10 },],
50+
loaded: true,
51+
};
52+
const chart = render(
53+
<UsersChart
54+
countries={countries}
55+
countriesUpdates={() => { }}
56+
setCountry={setCountry}
57+
activeCountry={null}
58+
/>);
59+
// click at (200, 200) on the document
60+
await act(async () => {
61+
fireEvent.click(chart.container, { clientX: 200, clientY: 200 });
62+
});
63+
expect(setCountry).toHaveBeenCalledWith('United States');
64+
});
3265
});

frontend/src/__tests__/services/UsersService.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ describe('Users Service', () => {
3838
expect(response).toEqual({ users: users(dateOfBirth.JSTimestamp), totalPages });
3939
});
4040

41+
it('should send a date of birth and country filter values to the server', async () => {
42+
let request;
43+
server.use(
44+
rest.get('/api/users', async (req, res, ctx) => {
45+
request = Object.fromEntries(req.url.searchParams);
46+
return res(ctx.json({ users: [], totalPages: 0 }));
47+
})
48+
);
49+
const dateOfBirth = makeDate('01-02-2000');
50+
await UsersService.getUsers({ dateFrom: dateOfBirth.JSDate, country: 'USA', dateTo: dateOfBirth.JSDate });
51+
expect(request).toEqual({
52+
dateFrom: dateOfBirth.PHPTimestamp.toString(),
53+
dateTo: dateOfBirth.PHPTimestamp.toString(),
54+
country: 'USA',
55+
});
56+
});
57+
4158
it('should fetch a user data and convert to the correct format', async () => {
4259
const dateOfBirth = makeDate('01-02-2000');
4360
server.use(

frontend/src/__tests__/views/Home.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,13 @@ describe('Home View', () => {
9595
// Wait for the data to be fetched and displayed
9696
await waitFor(() => browser.findByText('John Doe'));
9797

98+
// dateFrom and dateTo should be converted to php timestamp format and sent as strings
99+
const dateFrom = Math.floor(UIParams.dateFrom / 1000).toString();
100+
const dateTo = Math.floor(UIParams.dateTo / 1000).toString();
98101
// Check request parameters
99102
expect(requestParams).toEqual({
100-
dateFrom: UIParams.dateFrom.toString(),
101-
dateTo: UIParams.dateTo.toString(),
103+
dateFrom: dateFrom,
104+
dateTo: dateTo,
102105
country: UIParams.country.toString(),
103106
page: UIParams.page.toString(),
104107
perPage: UIParams.perPage.toString(),

frontend/src/components/UsersChart.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React, { Component } from 'react';
22
import { connect } from 'react-redux';
33
import { countriesUpdates } from 'store/events';
4+
import { setCountry } from 'store/UISlice';
45
import ReactEcharts from 'echarts-for-react';
56

6-
function getPieChartOption(countries) {
7+
function getPieChartOption(countries, activeCountry) {
8+
const selectedMap = activeCountry ? { [activeCountry]: true } : {};
79
return {
810
title: {
911
text: 'Number of Users by Country',
@@ -26,13 +28,17 @@ function getPieChartOption(countries) {
2628
shadowOffsetX: 0,
2729
shadowColor: 'rgba(0, 0, 0, 0.5)'
2830
}
29-
}
31+
},
32+
selectedMode: 'single',
33+
selectedMap
3034
}
3135
]
3236
};
3337
}
3438

35-
class UsersChart extends Component {
39+
export class UsersChart extends Component {
40+
_echartRef = null;
41+
3642
componentDidMount() {
3743
this.props.countriesUpdates(true);
3844
}
@@ -41,8 +47,18 @@ class UsersChart extends Component {
4147
this.props.countriesUpdates(false);
4248
}
4349

50+
onEvents = {
51+
'click': (e) => {
52+
const chart = this._echartRef.getEchartsInstance();
53+
const selectedMap = chart.getModel().getComponent('series', 0).get('selectedMap');
54+
// find first key with value true or undefined
55+
const selectedCountry = Object.keys(selectedMap).find(key => selectedMap[key]);
56+
this.props.setCountry(selectedCountry);
57+
},
58+
};
59+
4460
render() {
45-
const { countries } = this.props;
61+
const { countries, activeCountry } = this.props;
4662
if (!countries.loaded) {
4763
return <h1>Loading...</h1>;
4864
}
@@ -51,17 +67,22 @@ class UsersChart extends Component {
5167
}
5268
return (
5369
<div data-testid="users-chart">
54-
<ReactEcharts option={getPieChartOption(countries.data)} />
70+
<ReactEcharts
71+
ref={(e) => { this._echartRef = e; }}
72+
option={getPieChartOption(countries.data, activeCountry)}
73+
onEvents={this.onEvents}
74+
/>
5575
</div>
5676
);
5777
}
5878
}
5979

6080
export default connect(
6181
state => ({
62-
countries: state.countries
82+
countries: state.countries,
83+
activeCountry: state.UI.country,
6384
}),
64-
{ countriesUpdates }
85+
{ countriesUpdates, setCountry }
6586
)(UsersChart);
6687

6788
// for testing

frontend/src/services/UsersService.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@
22
import axios from 'axios';
33
import { API_URL } from 'config';
44

5+
function JSTimestampToPHPTimestamp(timestamp) {
6+
return Math.floor(timestamp / 1000);
7+
}
8+
59
export async function getUsers(params) {
6-
const { data: { users, ...rest } } = await axios.get(`${API_URL}/users`, { params: params });
10+
const { dateFrom = null, dateTo = null, ...restParams } = params || {};
11+
if (dateFrom) {
12+
restParams.dateFrom = JSTimestampToPHPTimestamp(dateFrom);
13+
}
14+
if (dateTo) {
15+
restParams.dateTo = JSTimestampToPHPTimestamp(dateTo);
16+
}
17+
18+
const { data: { users, ...rest } } = await axios.get(`${API_URL}/users`, { params: restParams, });
719
return {
820
// convert PHP timestamp to JS timestamp, which is in milliseconds
921
users: users.map(user => {
@@ -38,8 +50,7 @@ function _prepareUser(user) {
3850
first_name: firstName,
3951
last_name: lastName,
4052
country_name: country,
41-
// convert date JS timestamp to PHP timestamp, which is in seconds
42-
date_of_birth: Math.floor(dateOfBirth / 1000)
53+
date_of_birth: JSTimestampToPHPTimestamp(dateOfBirth)
4354
};
4455
}
4556

0 commit comments

Comments
 (0)