Skip to content

Commit

Permalink
Merge pull request #97 from akvo/feature/90-implement-survey-timer
Browse files Browse the repository at this point in the history
Feature/90 implement survey timer
  • Loading branch information
dedenbangkit committed Jul 31, 2023
2 parents 0b31c31 + 742c12d commit 5a6c8aa
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 29 deletions.
1 change: 1 addition & 0 deletions app/src/components/LogoutButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const LogoutButton = () => {
s.currentValues = {}; // answers
s.questionGroupListCurrentValues = {}; // answers for question group list component
s.dataPointName = [];
s.surveyDuration = 0;
});

/**
Expand Down
5 changes: 3 additions & 2 deletions app/src/database/crud/crud-datapoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ const dataPointsQuery = () => {
},
saveDataPoint: async ({ form, user, name, geo, submitted, duration, json }) => {
const submittedAt = submitted ? { submittedAt: new Date().toISOString() } : {};
const geoVal = geo ? { geo } : {};
const insertQuery = query.insert('datapoints', {
form,
user,
name,
geo,
...geoVal,
submitted,
duration,
createdAt: new Date().toISOString(),
Expand All @@ -71,7 +72,7 @@ const dataPointsQuery = () => {
geo,
submitted,
duration,
submittedAt,
submittedAt: submitted && !submittedAt ? new Date().toISOString() : submittedAt,
syncedAt,
json: json ? JSON.stringify(json).replace(/'/g, "''") : null,
},
Expand Down
1 change: 1 addition & 0 deletions app/src/form/FormContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const FormContainer = ({ forms, initialValues = {}, onSubmit, onSave }) => {
s.currentValues = {};
s.questionGroupListCurrentValues = {};
s.dataPointName = [];
s.surveyDuration = 0;
});
formRef.current?.resetForm();
};
Expand Down
1 change: 1 addition & 0 deletions app/src/navigation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const Navigation = (props) => {
s.currentValues = {};
s.questionGroupListCurrentValues = {};
s.dataPointName = [];
s.surveyDuration = 0;
});
}
UIState.update((s) => {
Expand Down
28 changes: 25 additions & 3 deletions app/src/pages/FormData.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { BaseLayout } from '../components';
import { Button } from '@rneui/themed';
import { UserState } from '../store';
import { crudDataPoints } from '../database/crud';
import Icon from 'react-native-vector-icons/Ionicons';

const convertMinutesToHHMM = (minutes) => {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;

const formattedHours = String(hours).padStart(2, '0');
const formattedMinutes = String(remainingMinutes).padStart(2, '0');

return `${formattedHours}h ${formattedMinutes}m`;
};

const FormData = ({ navigation, route }) => {
const formId = route?.params?.id;
const showSubmitted = route?.params?.showSubmitted || false;
const activeUserId = UserState.useState((s) => s.id);
const [search, setSearch] = useState(null);

const [data, setData] = useState([]);

Expand All @@ -26,7 +37,10 @@ const FormData = ({ navigation, route }) => {
results = results.map((res) => {
const createdAt = new Date(res.createdAt).toLocaleDateString('en-GB');
const syncedAt = res.syncedAt ? new Date(res.syncedAt).toLocaleDateString('en-GB') : '-';
let subtitlesTemp = [`Created: ${createdAt}`, `Survey Duration: ${res.duration}`];
let subtitlesTemp = [
`Created: ${createdAt}`,
`Survey Duration: ${convertMinutesToHHMM(res.duration)}`,
];
if (showSubmitted) {
subtitlesTemp = [...subtitlesTemp, `Sync: ${syncedAt}`];
}
Expand All @@ -42,6 +56,12 @@ const FormData = ({ navigation, route }) => {
fetchData();
}, []);

const filteredData = useMemo(() => {
return data.filter(
(d) => (search && d?.name?.toLowerCase().includes(search.toLowerCase())) || !search,
);
}, [data, search]);

const handleFormDataListAction = (id) => {
if (showSubmitted) {
return null;
Expand All @@ -59,14 +79,16 @@ const FormData = ({ navigation, route }) => {
search={{
show: true,
placeholder: 'Search datapoint',
value: search,
action: setSearch,
}}
leftComponent={
<Button type="clear" onPress={goBack} testID="arrow-back-button">
<Icon name="arrow-back" size={18} />
</Button>
}
>
<BaseLayout.Content data={data} action={handleFormDataListAction} />
<BaseLayout.Content data={filteredData} action={handleFormDataListAction} />
</BaseLayout>
);
};
Expand Down
42 changes: 35 additions & 7 deletions app/src/pages/FormPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ import { FormContainer } from '../form';
import { SaveDialogMenu, SaveDropdownMenu } from '../form/support';
import { BaseLayout } from '../components';
import { FormState } from '../store';
import { crudDataPoints, crudForms } from '../database/crud';
import { crudDataPoints } from '../database/crud';
import { UserState } from '../store';
import { generateDataPointName } from '../form/lib';

const convertDurationToMinutes = (currentDataPoint, newDataPoint) => {
const totalDuration = (currentDataPoint?.duration || 0) + newDataPoint.duration;
if (!totalDuration) {
return 0;
}
return totalDuration / 60;
};

const FormPage = ({ navigation, route }) => {
const { form: selectedForm, dataPointName } = FormState.useState((s) => s);
const { form: selectedForm, dataPointName, surveyDuration } = FormState.useState((s) => s);
const userId = UserState.useState((s) => s.id);
const [onSaveFormParams, setOnSaveFormParams] = React.useState({});
const [showDialogMenu, setShowDialogMenu] = React.useState(false);
Expand All @@ -33,6 +41,19 @@ const FormPage = ({ navigation, route }) => {
const [currentDataPoint, setCurrentDataPoint] = React.useState({});
const [loading, setLoading] = React.useState(false);

React.useEffect(() => {
let counter = 0;
const timerInterval = setInterval(() => {
counter++;
FormState.update((s) => {
s.surveyDuration = counter;
});
}, 1000);
return () => {
clearInterval(timerInterval);
};
}, []);

React.useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
const values = onSaveFormParams?.values;
Expand Down Expand Up @@ -99,13 +120,17 @@ const FormPage = ({ navigation, route }) => {
name: values?.name || 'Untitled',
geo: values?.geo || null,
submitted: 0,
duration: 0, // TODO:: set duration
duration: surveyDuration,
json: values?.answers || {},
};
const dbCall = isNewSubmission
? crudDataPoints.saveDataPoint
: crudDataPoints.updateDataPoint;
await dbCall({ ...currentDataPoint, ...saveData });
await dbCall({
...currentDataPoint,
...saveData,
duration: convertDurationToMinutes(currentDataPoint, saveData),
});
if (Platform.OS === 'android') {
ToastAndroid.show(`Data point ${values?.name} saved`, ToastAndroid.LONG);
}
Expand Down Expand Up @@ -160,14 +185,17 @@ const FormPage = ({ navigation, route }) => {
name: values.name,
geo: values.geo,
submitted: 1,
duration: 0, // TODO:: set duration
duration: surveyDuration,
json: answers,
};

const dbCall = isNewSubmission
? crudDataPoints.saveDataPoint
: crudDataPoints.updateDataPoint;
await dbCall({ ...currentDataPoint, ...submitData });
await dbCall({
...currentDataPoint,
...submitData,
duration: convertDurationToMinutes(currentDataPoint, submitData),
});
if (Platform.OS === 'android') {
ToastAndroid.show(`Data point ${values.name} submitted`, ToastAndroid.LONG);
}
Expand Down
67 changes: 55 additions & 12 deletions app/src/pages/__tests__/FormData.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ describe('FormDataPage', () => {
const mockData = [
{
id: 1,
name: 'Datapoint 1',
createdAt: '2023-07-18T12:34:56.789Z',
duration: '30 minutes',
duration: 145,
syncedAt: '2023-07-18T13:00:00.000Z',
},
];

crudDataPoints.selectDataPointsByFormAndSubmitted.mockResolvedValue(mockData);
const tree = render(<FormDataPage />);
await waitFor(() => expect(tree.toJSON()).toMatchSnapshot());
Expand All @@ -38,8 +40,9 @@ describe('FormDataPage', () => {
const mockData = [
{
id: 1,
name: 'Datapoint 1',
createdAt: '2023-07-18T12:34:56.789Z',
duration: '30 minutes',
duration: 145,
syncedAt: '2023-07-18T13:00:00.000Z',
submitted: 1,
},
Expand All @@ -51,9 +54,11 @@ describe('FormDataPage', () => {

await waitFor(() => {
expect(wrapper.getByText('Form Name')).toBeTruthy();
expect(wrapper.getByText('Created: 18/07/2023')).toBeTruthy();
expect(wrapper.getByText('Survey Duration: 30 minutes')).toBeTruthy();
expect(wrapper.getByText('Sync: 18/07/2023')).toBeTruthy();
const list0 = wrapper.getByTestId('card-touchable-0');
expect(list0.props.children[0].props.title).toEqual('Datapoint 1');
expect(list0.props.children[0].props.subTitles[0]).toEqual('Created: 18/07/2023');
expect(list0.props.children[0].props.subTitles[1]).toEqual('Survey Duration: 02h 25m');
expect(list0.props.children[0].props.subTitles[2]).toEqual('Sync: 18/07/2023');
});
});

Expand All @@ -69,8 +74,9 @@ describe('FormDataPage', () => {
const mockData = [
{
id: 1,
name: 'Datapoint 1',
createdAt: '2023-07-18T12:34:56.789Z',
duration: '30 minutes',
duration: 145,
syncedAt: null,
submitted: 0,
},
Expand All @@ -82,18 +88,21 @@ describe('FormDataPage', () => {

await waitFor(() => {
expect(wrapper.getByText('Form Name')).toBeTruthy();
expect(wrapper.getByText('Created: 18/07/2023')).toBeTruthy();
expect(wrapper.getByText('Survey Duration: 30 minutes')).toBeTruthy();
expect(wrapper.queryByText('Sync: -')).toBeFalsy();
const list0 = wrapper.getByTestId('card-touchable-0');
expect(list0.props.children[0].props.title).toEqual('Datapoint 1');
expect(list0.props.children[0].props.subTitles[0]).toEqual('Created: 18/07/2023');
expect(list0.props.children[0].props.subTitles[1]).toEqual('Survey Duration: 02h 25m');
expect(list0.props.children[0].props.subTitles[2]).toEqual(undefined);
});
});

it('should have search input field', () => {
const mockData = [
{
id: 1,
name: 'Datapoint 1',
createdAt: '2023-07-18T12:34:56.789Z',
duration: '30 minutes',
duration: 145,
syncedAt: '2023-07-18T13:00:00.000Z',
},
];
Expand All @@ -102,7 +111,40 @@ describe('FormDataPage', () => {
expect(wrapper.queryByTestId('search-bar')).toBeTruthy();
});

it.todo('should filter list of datapoint by search value');
it('should filter list of datapoint by search value', async () => {
const mockData = [
{
id: 1,
name: 'Datapoint 1',
createdAt: '2023-07-18T12:34:56.789Z',
duration: 145,
syncedAt: '2023-07-18T13:00:00.000Z',
},
{
id: 2,
name: 'Datapoint 2',
createdAt: '2023-07-18T12:34:56.789Z',
duration: 145,
syncedAt: '2023-07-18T13:00:00.000Z',
},
];

crudDataPoints.selectDataPointsByFormAndSubmitted.mockResolvedValue(mockData);

const wrapper = render(<FormDataPage />);

const searchField = wrapper.getByTestId('search-bar');
expect(searchField).toBeDefined();
fireEvent.changeText(searchField, 'Datapoint 1');

await waitFor(() => {
const list0 = wrapper.queryByTestId('card-touchable-0');
expect(list0).toBeTruthy();

const list1 = wrapper.queryByTestId('card-touchable-1');
expect(list1).toBeFalsy();
});
});

it('should navigate to FormPage with correct route params when datapoint list pressed', async () => {
const mockNavigation = useNavigation();
Expand All @@ -117,8 +159,9 @@ describe('FormDataPage', () => {
const mockData = [
{
id: 1,
name: 'Datapoint 1',
createdAt: '2023-07-18T12:34:56.789Z',
duration: '30 minutes',
duration: 145,
syncedAt: null,
submitted: 0,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { render, fireEvent, waitFor, act } from '@testing-library/react-native';
jest.useFakeTimers();
import FormPage from '../FormPage';
import crudDataPoints from '../../database/crud/crud-datapoints';
import { UserState } from '../../store';
import { UserState, FormState } from '../../store';

const mockFormContainer = jest.fn();
const mockRoute = {
Expand Down Expand Up @@ -297,6 +297,9 @@ describe('FormPage continue saved submision then submit', () => {
UserState.update((s) => {
s.id = 1;
});
FormState.update((s) => {
s.surveyDuration = 9;
});
});

await waitFor(() => {
Expand All @@ -312,6 +315,7 @@ describe('FormPage continue saved submision then submit', () => {
name: mockValues.name,
submitted: 1,
json: mockValues.answers,
duration: 0.15, // in minutes
});
expect(ToastAndroid.show).toHaveBeenCalledTimes(1);
// call refreshForm
Expand Down
Loading

0 comments on commit 5a6c8aa

Please sign in to comment.