Skip to content

Commit

Permalink
Merge branch 'develop' into feature/1127-margin-between-submission-an…
Browse files Browse the repository at this point in the history
…d-approval-panel
  • Loading branch information
ifirmawan authored Feb 6, 2024
2 parents 6d98ccf + 880c2db commit e16d16e
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 138 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RTMIS

[![Build Status](https://akvo.semaphoreci.com/badges/rtmis/branches/main.svg?style=shields)](https://akvo.semaphoreci.com/projects/rtmis) [![Repo Size](https://img.shields.io/github/repo-size/akvo/rtmis)](https://img.shields.io/github/repo-size/akvo/rtmis) [![Languages](https://img.shields.io/github/languages/count/akvo/rtmis)](https://img.shields.io/github/languages/count/akvo/rtmis) [![Issues](https://img.shields.io/github/issues/akvo/rtmis)](https://img.shields.io/github/issues/akvo/rtmis) [![Last Commit](https://img.shields.io/github/last-commit/akvo/rtmis/main)](https://img.shields.io/github/last-commit/akvo/rtmis/main) [![Coverage Status](https://coveralls.io/repos/github/akvo/rtmis/badge.svg)](https://coveralls.io/github/akvo/rtmis) [![Coverage Status](https://img.shields.io/readthedocs/rtmis?label=read%20the%20docs)](https://rtmis.readthedocs.io/en/latest)
[![Build Status](https://github.com/akvo/rtmis/actions/workflows/main.yml/badge.svg)](https://github.com/akvo/rtmis/actions/workflows/main.yml?query=branch%3Amain) [![Build Status](https://github.com/akvo/rtmis/actions/workflows/apk-release.yml/badge.svg)](https://github.com/akvo/rtmis/actions/workflows/apk-release.yml?query=branch%3Amain) [![Repo Size](https://img.shields.io/github/repo-size/akvo/rtmis)](https://img.shields.io/github/repo-size/akvo/rtmis) [![Languages](https://img.shields.io/github/languages/count/akvo/rtmis)](https://img.shields.io/github/languages/count/akvo/rtmis) [![Issues](https://img.shields.io/github/issues/akvo/rtmis)](https://img.shields.io/github/issues/akvo/rtmis) [![Last Commit](https://img.shields.io/github/last-commit/akvo/rtmis/main)](https://img.shields.io/github/last-commit/akvo/rtmis/main) [![Coverage Status](https://coveralls.io/repos/github/akvo/rtmis/badge.svg)](https://coveralls.io/github/akvo/rtmis) [![Coverage Status](https://img.shields.io/readthedocs/rtmis?label=read%20the%20docs)](https://rtmis.readthedocs.io/en/latest)

Real Time Monitoring Information Systems

Expand Down
50 changes: 43 additions & 7 deletions app/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import backgroundTask, {
} from './src/lib/background-task';
import crudJobs, { jobStatus, MAX_ATTEMPT } from './src/database/crud/crud-jobs';
import { ToastAndroid } from 'react-native';
import * as Location from 'expo-location';

export const setNotificationHandler = () =>
Notifications.setNotificationHandler({
Expand Down Expand Up @@ -85,6 +86,10 @@ TaskManager.defineTask(SYNC_FORM_SUBMISSION_TASK_NAME, async () => {
const App = () => {
const serverURLState = BuildParamsState.useState((s) => s.serverURL);
const syncValue = BuildParamsState.useState((s) => s.dataSyncInterval);
const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold);
const gpsAccuracyLevel = BuildParamsState.useState((s) => s.gpsAccuracyLevel);
const geoLocationTimeout = BuildParamsState.useState((s) => s.geoLocationTimeout);
const locationIsGranted = UserState.useState((s) => s.locationIsGranted);

const handleCheckSession = () => {
// check users exist
Expand Down Expand Up @@ -118,26 +123,41 @@ const App = () => {
const serverURL = configExist?.serverURL || serverURLState;
const syncInterval = configExist?.syncInterval || syncValue;
if (!configExist) {
await crudConfig.addConfig({ serverURL });
}
if (syncInterval) {
BuildParamsState.update((s) => {
s.dataSyncInterval = syncInterval;
await crudConfig.addConfig({
serverURL,
syncInterval,
gpsThreshold,
gpsAccuracyLevel,
geoLocationTimeout,
});
}
if (serverURL) {
BuildParamsState.update((s) => {
s.serverURL = serverURL;
});
api.setServerURL(serverURL);
await crudConfig.updateConfig({ serverURL });
}
if (configExist) {
/**
* Update settings values from database
*/
BuildParamsState.update((s) => {
s.dataSyncInterval = configExist.syncInterval;
s.gpsThreshold = configExist.gpsThreshold;
s.gpsAccuracyLevel = configExist.gpsAccuracyLevel;
s.geoLocationTimeout = configExist.geoLocationTimeout;
});
}
console.info('[CONFIG] Server URL', serverURL);
};

const handleInitDB = useCallback(async () => {
/**
* Exclude the reset in the try-catch block
* to prevent other queries from being skipped after this process.
*/
await conn.reset();
try {
await conn.reset();
const db = conn.init;
const queries = tables.map((t) => {
const queryString = query.initialQuery(t.name, t.fields);
Expand Down Expand Up @@ -187,6 +207,22 @@ const App = () => {
handleOnRegisterTask();
}, [handleOnRegisterTask]);

const requestAccessLocation = useCallback(async () => {
if (locationIsGranted) {
return;
}
const { status } = await Location.requestForegroundPermissionsAsync();
if (status === 'granted') {
UserState.update((s) => {
s.locationIsGranted = true;
});
}
}, [locationIsGranted]);

useEffect(() => {
requestAccessLocation();
}, [requestAccessLocation]);

return (
<SafeAreaProvider>
<Navigation testID="navigation-element" />
Expand Down
2 changes: 1 addition & 1 deletion app/src/database/crud/crud-jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const jobsQuery = () => {
const insertQuery = query.insert(tableName, {
...data,
createdAt,
uuid: Crypto.randomUUID(),
uuid: Crypto.randomUUID(), // TODO: Remove if not needed
});
return await conn.tx(db, insertQuery, []);
} catch (error) {
Expand Down
9 changes: 6 additions & 3 deletions app/src/database/tables.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const tables = [
password: 'TEXT',
active: 'TINYINT',
token: 'TEXT',
administrationList: 'TEXT',
administrationList: 'TEXT', // TODO: Remove
},
},
{
Expand All @@ -20,6 +20,9 @@ export const tables = [
syncInterval: 'REAL',
syncWifiOnly: 'TINYINT',
lang: 'VARCHAR(255) DEFAULT "en" NOT NULL',
gpsThreshold: 'INTEGER NULL',
gpsAccuracyLevel: 'INTEGER NULL',
geoLocationTimeout: 'INTEGER NULL',
},
},
{
Expand Down Expand Up @@ -59,7 +62,7 @@ export const tables = [
formId: 'INTEGER NOT NULL',
uuid: 'TEXT type UNIQUE',
name: 'VARCHAR(255)',
administration: 'VARCHAR(255)',
administration: 'VARCHAR(255)', // TODO: Remove
syncedAt: 'DATETIME',
json: 'TEXT',
},
Expand All @@ -76,7 +79,7 @@ export const tables = [
name: 'jobs',
fields: {
id: 'INTEGER PRIMARY KEY NOT NULL',
uuid: 'TEXT type UNIQUE',
uuid: 'TEXT type UNIQUE', // TODO: Remove if not used
user: 'INTEGER NOT NULL',
type: 'VARCHAR(191)',
status: 'INTEGER NOT NULL',
Expand Down
12 changes: 11 additions & 1 deletion app/src/form/components/Question.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,17 @@ const Question = memo(({ group, activeQuestions = [], index }) => {
});
FormState.update((s) => {
const preValues = preFilled?.fill?.reduce((prev, current) => {
return { [current['id']]: current['answer'], ...prev };
/**
* Make sure the answer criteria are not replaced by previous values
* eg:
* Previous value = "Update"
* Answer criteria = "New"
*/
const answer =
id === current['id']
? current['answer']
: values?.[current['id']] || current['answer'];
return { [current['id']]: answer, ...prev };
}, {});
s.prefilled = preValues;
});
Expand Down
48 changes: 31 additions & 17 deletions app/src/form/fields/TypeGeo.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import React, { useState } from 'react';
import { View } from 'react-native';
import { Text, Button } from '@rneui/themed';

import { UIState, FormState, BuildParamsState } from '../../store';
import { FormState, BuildParamsState, UserState } from '../../store';
import { FieldLabel } from '../support';
import { styles } from '../styles';
import { loc, i18n } from '../../lib';

const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) => {
const [errorMsg, setErrorMsg] = useState(null);
const [gpsAccuracy, setGpsAccuracy] = useState(null);
const [currLocation, setCurrLocation] = useState({ lat: null, lng: null });
const [loading, setLoading] = useState(false);
const [latitude, longitude] = value || [];

const gpsAccuracyLevel = BuildParamsState.useState((s) => s.gpsAccuracyLevel);
const geoLocationTimeout = BuildParamsState.useState((s) => s.geoLocationTimeout);
const gpsThreshold = BuildParamsState.useState((s) => s.gpsThreshold);
const isOnline = UIState.useState((s) => s.online);
const activeLang = FormState.useState((s) => s.lang);
const savedLocation = UserState.useState((s) => s.currentLocation);

const trans = i18n.text(activeLang);

const requiredValue = required ? requiredSign : null;

const handleGetCurrLocation = useCallback(async () => {
setLoading(true);
const getCurrentLocation = async () => {
await loc.getCurrentLocation(
({ coords }) => {
const { latitude: lat, longitude: lng, accuracy } = coords;
Expand All @@ -32,10 +32,6 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign })
*/
setGpsAccuracy(Math.floor(accuracy));
// console.info('GPS accuracy:', accuracy, 'GPS Threshold:', gpsThreshold);
setCurrLocation({
lat,
lng,
});
FormState.update((s) => {
s.currentValues = { ...s.currentValues, [id]: [lat, lng] };
});
Expand All @@ -45,13 +41,35 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign })
setLoading(false);
setErrorMsg(message);
setGpsAccuracy(-1);
setCurrLocation({ lat: -1.3855559, lng: 37.9938594 });

FormState.update((s) => {
s.currentValues = { ...s.currentValues, [id]: [-1.3855559, 37.9938594] };
});
},
gpsAccuracyLevel,
);
}, [gpsThreshold, id]);
};

const handleGetCurrLocation = async () => {
const geoTimeout = geoLocationTimeout * 1000;
setLoading(true);
setTimeout(() => {
if (!value?.length && savedLocation?.coords) {
/**
* Insert a saved location when a GEO question has no answer after timeout
*/
const { latitude: lat, longitude: lng, accuracy } = savedLocation.coords;
setGpsAccuracy(Math.floor(accuracy));
FormState.update((s) => {
s.currentValues = { ...s.currentValues, [id]: [lat, lng] };
});
}
if (loading) {
setLoading(false);
}
}, geoTimeout);
await getCurrentLocation();
};

return (
<View>
Expand Down Expand Up @@ -85,11 +103,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign })
</Text>
)}
<View style={styles.geoButtonGroup}>
<Button
onPress={() => handleGetCurrLocation()}
testID="button-curr-location"
disabled={loading}
>
<Button onPress={() => handleGetCurrLocation()} testID="button-curr-location">
{loading
? trans.fetchingLocation
: gpsAccuracy !== null
Expand Down
29 changes: 27 additions & 2 deletions app/src/lib/loc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as Location from 'expo-location';

const getCurrentLocation = async (success, error) => {
const getCurrentLocation = async (success, error, level = null) => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status === 'granted') {
const findLevel = accuracyLevels.find((l) => l.value === level);
const accuracy = findLevel?.value || Location.Accuracy.Highest;
const result = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Highest,
accuracy,
});
success(result);
} else {
Expand All @@ -19,3 +21,26 @@ const loc = {
};

export default loc;

export const accuracyLevels = [
{
label: 'Lowest',
value: 1,
},
{
label: 'Low',
value: 2,
},
{
label: 'Balanced',
value: 3,
},
{
label: 'High',
value: 4,
},
{
label: 'Highest',
value: 5,
},
];
43 changes: 42 additions & 1 deletion app/src/pages/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { Button } from '@rneui/themed';
import Icon from 'react-native-vector-icons/Ionicons';
import { Platform, ToastAndroid } from 'react-native';
import { BaseLayout } from '../components';
import { FormState, UserState, UIState } from '../store';
import { FormState, UserState, UIState, BuildParamsState } from '../store';
import { crudForms } from '../database/crud';
import { i18n } from '../lib';
import * as Notifications from 'expo-notifications';
import * as Location from 'expo-location';

const Home = ({ navigation, route }) => {
const params = route?.params || null;
Expand All @@ -15,6 +16,9 @@ const Home = ({ navigation, route }) => {
const [appLang, setAppLang] = useState('en');
const [loading, setloading] = useState(true);

const locationIsGranted = UserState.useState((s) => s.locationIsGranted);
const gpsAccuracyLevel = BuildParamsState.useState((s) => s.gpsAccuracyLevel);
const gpsInterval = BuildParamsState.useState((s) => s.gpsInterval);
const isManualSynced = UIState.useState((s) => s.isManualSynced);
const activeLang = UIState.useState((s) => s.lang);
const trans = i18n.text(activeLang);
Expand Down Expand Up @@ -101,6 +105,43 @@ const Home = ({ navigation, route }) => {
return () => subscription.remove();
}, []);

const watchCurrentPosition = useCallback(
async (unsubscribe = false) => {
if (!locationIsGranted) {
return;
}
const timeInterval = gpsInterval * 1000; // miliseconds
/**
* Subscribe to the user's current location
* @tutorial https://docs.expo.dev/versions/latest/sdk/location/#locationwatchpositionasyncoptions-callback
*/
const watch = await Location.watchPositionAsync(
{
accuracy: gpsAccuracyLevel,
timeInterval,
},
(res) => {
console.info('[CURRENT LOC]', res?.coords);
UserState.update((s) => {
s.currentLocation = res;
});
},
);

if (unsubscribe) {
watch.remove();
}
},
[gpsAccuracyLevel, gpsInterval, locationIsGranted],
);

useEffect(() => {
watchCurrentPosition();
return () => {
watchCurrentPosition(true);
};
}, [watchCurrentPosition]);

return (
<BaseLayout
title={trans.homePageTitle}
Expand Down
Loading

0 comments on commit e16d16e

Please sign in to comment.