diff --git a/README.md b/README.md index e027d4cb6..8d1a5ee96 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/App.js b/app/App.js index 54c88b264..194275b3b 100644 --- a/app/App.js +++ b/app/App.js @@ -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({ @@ -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 @@ -118,11 +123,12 @@ 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) { @@ -130,14 +136,28 @@ const App = () => { 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); @@ -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 ( diff --git a/app/src/database/crud/crud-jobs.js b/app/src/database/crud/crud-jobs.js index e7e781c0e..a24e18187 100644 --- a/app/src/database/crud/crud-jobs.js +++ b/app/src/database/crud/crud-jobs.js @@ -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) { diff --git a/app/src/database/tables.js b/app/src/database/tables.js index 24c116e81..b1450c91b 100644 --- a/app/src/database/tables.js +++ b/app/src/database/tables.js @@ -7,7 +7,7 @@ export const tables = [ password: 'TEXT', active: 'TINYINT', token: 'TEXT', - administrationList: 'TEXT', + administrationList: 'TEXT', // TODO: Remove }, }, { @@ -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', }, }, { @@ -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', }, @@ -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', diff --git a/app/src/form/components/Question.js b/app/src/form/components/Question.js index 961fbbaa9..7de4caadd 100644 --- a/app/src/form/components/Question.js +++ b/app/src/form/components/Question.js @@ -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; }); diff --git a/app/src/form/fields/TypeGeo.js b/app/src/form/fields/TypeGeo.js index a07a74886..93f9cb6cc 100644 --- a/app/src/form/fields/TypeGeo.js +++ b/app/src/form/fields/TypeGeo.js @@ -1,8 +1,8 @@ -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'; @@ -10,20 +10,20 @@ 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; @@ -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] }; }); @@ -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 ( @@ -85,11 +103,7 @@ const TypeGeo = ({ keyform, id, name, value, tooltip, required, requiredSign }) )} -