}
>
{D.questionnaireButton}
diff --git a/src/components/panel-body/UEpage/router.js b/src/components/panel-body/UEpage/router.js
index 0f5435d66..c971656a7 100644
--- a/src/components/panel-body/UEpage/router.js
+++ b/src/components/panel-body/UEpage/router.js
@@ -34,7 +34,7 @@ const useStyles = makeStyles(() => ({
}));
const Router = ({ match, saveUE, refresh }) => {
- const surveyUnit = useContext(SurveyUnitContext);
+ const { surveyUnit } = useContext(SurveyUnitContext);
/** refs are used for scrolling, dispatched to the clickable link and linked element */
const detailsRef = useRef('details');
diff --git a/src/components/panel-body/UEpage/stateLine.js b/src/components/panel-body/UEpage/stateLine.js
index 241281013..b6d66a99a 100644
--- a/src/components/panel-body/UEpage/stateLine.js
+++ b/src/components/panel-body/UEpage/stateLine.js
@@ -4,14 +4,14 @@ import Stepper from '@material-ui/core/Stepper';
import { makeStyles } from '@material-ui/core/styles';
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUnchecked';
-import toDoEnum from 'common-tools/enum/SUToDoEnum';
-import { convertSUStateInToDo, getLastState } from 'common-tools/functions';
+import toDoEnum from 'utils/enum/SUToDoEnum';
+import { convertSUStateInToDo, getLastState } from 'utils/functions';
import PropTypes from 'prop-types';
import React, { useContext } from 'react';
import SurveyUnitContext from './UEContext';
const StateLine = () => {
- const surveyUnit = useContext(SurveyUnitContext);
+ const { surveyUnit } = useContext(SurveyUnitContext);
const state = getLastState(surveyUnit);
const { type } = state;
diff --git a/src/components/panel-body/lateralMenu/component.js b/src/components/panel-body/lateralMenu/component.js
index 75db88957..0ba6ebf7b 100644
--- a/src/components/panel-body/lateralMenu/component.js
+++ b/src/components/panel-body/lateralMenu/component.js
@@ -1,12 +1,10 @@
import { Card, Drawer } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
-import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import D from 'i18n';
import PropTypes from 'prop-types';
-import React, { useState } from 'react';
+import React from 'react';
import { NavLink } from 'react-router-dom';
-import Notification from './notificationItem';
const useStyles = makeStyles(theme => ({
drawer: {
@@ -44,14 +42,6 @@ const useStyles = makeStyles(theme => ({
}));
const LateralMenu = ({ openDrawer, setOpenDrawer, version }) => {
const classes = useStyles();
- const [showNotifications, setShowNotifications] = useState(false);
-
- const handleClick = event => {
- if (event.target.id === 'notifications') {
- setShowNotifications(true);
- event.stopPropagation();
- }
- };
return (
<>
@@ -79,38 +69,10 @@ const LateralMenu = ({ openDrawer, setOpenDrawer, version }) => {
-
- {D.goToNotificationsPage}
-
-
{`Version : ${version}`}
-
setShowNotifications(false)}
- >
-
- {D.goToNotificationsPage}
-
-
-
-
-
>
);
};
diff --git a/src/components/panel-body/lateralMenu/notificationItem.js b/src/components/panel-body/lateralMenu/notificationItem.js
deleted file mode 100644
index b4d46151a..000000000
--- a/src/components/panel-body/lateralMenu/notificationItem.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Card from '@material-ui/core/Card';
-import { makeStyles } from '@material-ui/core/styles';
-import Typography from '@material-ui/core/Typography';
-import React from 'react';
-
-const useStyles = makeStyles(() => ({
- root: {
- padding: 8,
- height: '5em',
- margin: '0.5em',
- },
-}));
-
-const NotificationItem = () => {
- const classes = useStyles();
-
- return (
-
- Synchronisation réussie
- 09:15
-
- );
-};
-
-export default NotificationItem;
diff --git a/src/components/panel-body/queen-container/component.js b/src/components/panel-body/queen-container/component.js
index fc65ddcbf..67203a70b 100644
--- a/src/components/panel-body/queen-container/component.js
+++ b/src/components/panel-body/queen-container/component.js
@@ -1,5 +1,5 @@
import { makeStyles } from '@material-ui/core';
-import listenQueen from 'common-tools/hooks/listenQueen';
+import listenQueen from 'utils/hooks/listenQueen';
import React from 'react';
import { useHistory } from 'react-router-dom';
diff --git a/src/components/panel-body/resetData/index.js b/src/components/panel-body/resetData/index.js
new file mode 100644
index 000000000..f28702574
--- /dev/null
+++ b/src/components/panel-body/resetData/index.js
@@ -0,0 +1,141 @@
+import { Button, makeStyles, Typography } from '@material-ui/core';
+import { Delete, Warning } from '@material-ui/icons';
+import Dexie from 'dexie';
+import D from 'i18n';
+import React, { useState } from 'react';
+import { unregister } from 'serviceWorkerRegistration';
+import { ResetDialog } from './resetDialog';
+
+const useStyles = makeStyles(theme => ({
+ titleWrapper: {
+ marginTop: '2em',
+ marginBottom: '2em',
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ warningIcon: {
+ color: 'black',
+ fontSize: '11em',
+ },
+ iconWrapper: {
+ padding: '10px',
+ borderRadius: '30px',
+ backgroundColor: '#edc520',
+ },
+ title: {
+ padding: '10px 50px 10px 50px',
+ alignSelf: 'center',
+ textAlign: 'center',
+ marginLeft: '20px',
+ textTransform: 'uppercase',
+ fontWeight: 'bold',
+ borderRadius: '30px',
+ backgroundColor: '#edc520',
+ },
+ subTitle: { fontSize: '4em' },
+ deleteBtn: {
+ marginLeft: '1em',
+ backgroundColor: theme.palette.error.main,
+ },
+ goBackBtn: {
+ backgroundColor: theme.palette.success.main,
+ },
+ body: {
+ textAlign: 'center',
+ },
+ actions: {
+ marginTop: '2em',
+ },
+}));
+
+export const ResetData = () => {
+ const classes = useStyles();
+
+ const [open, setOpen] = useState(false);
+ const [deleteStatus, setDeleteStatus] = useState(null);
+
+ const handleClickOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const deleteAll = async () => {
+ setOpen(false);
+ setDeleteStatus('deleting');
+ const deleteOneTable = async tableName => {
+ await Dexie.delete(tableName);
+ };
+ const deleteAllContentOfCache = async cacheName => {
+ await caches.delete(cacheName);
+ };
+ try {
+ const databases = await Dexie.getDatabaseNames();
+ await databases.reduce(async (previousPromise, name) => {
+ await previousPromise;
+ return deleteOneTable(name);
+ }, Promise.resolve());
+ unregister();
+ const cacheNames = await caches.keys();
+ await cacheNames.reduce(async (previousPromise, cacheName) => {
+ await previousPromise;
+ return deleteAllContentOfCache(cacheName);
+ }, Promise.resolve());
+
+ window.localStorage.clear();
+ setDeleteStatus('success');
+ } catch (e) {
+ setDeleteStatus('failed');
+ }
+ };
+
+ const goBack = () => {
+ window.location = window.location.origin;
+ };
+
+ return (
+
+
+
+
+
+
+ {D.mainTitle.split(' ').map(v => (
+
+ {v}
+
+ ))}
+
+
+
+ {deleteStatus === 'deleting' &&
{D.deleting}}
+ {deleteStatus === 'success' &&
{D.deleteSuccess}}{' '}
+ {deleteStatus === 'failed' &&
{D.deleteFailed}}
+ {!(deleteStatus === 'success') &&
{D.youCanDeleteData}}
+
+ {deleteStatus !== 'deleting' && (
+
+ )}
+ {deleteStatus !== 'success' && (
+ }>
+ {D.deleteAll}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/components/panel-body/resetData/resetDialog.js b/src/components/panel-body/resetData/resetDialog.js
new file mode 100644
index 000000000..a1fe1eef0
--- /dev/null
+++ b/src/components/panel-body/resetData/resetDialog.js
@@ -0,0 +1,155 @@
+/* eslint-disable jsx-a11y/no-autofocus */
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ makeStyles,
+ TextField,
+ Typography,
+} from '@material-ui/core';
+import D from 'i18n';
+import React, { useState } from 'react';
+import { PEARL_USER_KEY } from 'utils/constants';
+
+const useStyles = makeStyles(theme => ({
+ agreeBtn: {
+ backgroundColor: theme.palette.error.main,
+ },
+ disagreeBtn: {
+ backgroundColor: theme.palette.success.main,
+ },
+ randomText: {
+ textAlign: 'center',
+ fontWeight: 'bold',
+ letterSpacing: '1.5px',
+ },
+ confirm2: { marginTop: '2em' },
+}));
+
+export const ResetDialog = ({
+ open,
+ title,
+ body,
+ agree,
+ disagree,
+ agreeFunction,
+ disagreeFunction,
+}) => {
+ const classes = useStyles();
+
+ const getRandomText = () =>
+ Math.random()
+ .toString(36)
+ .substring(2, 10)
+ .toUpperCase();
+
+ const [step, setStep] = useState(null);
+
+ const [randomText, setRandomText] = useState(() => getRandomText());
+ const [placeHolder] = useState(() => getRandomText());
+ const [values, setValues] = useState({ user: '', random: '' });
+ const [errors, setErrors] = useState({ user: false, random: false });
+
+ const clean = () => {
+ setStep(null);
+ setValues({ user: '', random: '' });
+ setErrors({ user: false, random: false });
+ setRandomText(getRandomText());
+ };
+
+ const handleChange = ({ target: { id, value } }) => {
+ setValues({ ...values, [id]: value });
+ setErrors({ ...errors, [id]: false });
+ };
+
+ const validateInput = e => {
+ e.preventDefault();
+ if (step === 'random') {
+ if (randomText === values.random) {
+ setStep('user');
+ } else setErrors({ ...errors, random: true });
+ } else if (step === 'user') {
+ const { id } = JSON.parse(window.localStorage.getItem(PEARL_USER_KEY) || '{}');
+ if ((id || '').toLowerCase() === values.user.toLowerCase()) {
+ clean();
+ agreeFunction();
+ } else setErrors({ ...errors, user: true });
+ }
+ };
+
+ const confirm = e => {
+ setStep('random');
+ };
+ const cancel = e => {
+ disagreeFunction(e);
+ clean();
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/panel-body/training/component.js b/src/components/panel-body/training/component.js
deleted file mode 100644
index 309b2aeca..000000000
--- a/src/components/panel-body/training/component.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-
-const TrainingPage = () => (
-
-
Training Page
-
-);
-
-export default TrainingPage;
diff --git a/src/components/panel-body/training/index.js b/src/components/panel-body/training/index.js
deleted file mode 100644
index b404d7fd4..000000000
--- a/src/components/panel-body/training/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './component';
diff --git a/src/components/sychronizeWrapper/index.js b/src/components/sychronizeWrapper/index.js
new file mode 100644
index 000000000..828169173
--- /dev/null
+++ b/src/components/sychronizeWrapper/index.js
@@ -0,0 +1,127 @@
+import notificationIdbService from 'indexedbb/services/notification-idb-service';
+import { synchronizePearl, useQueenSynchronisation } from 'utils/synchronize';
+import D from 'i18n';
+import React, { useContext, useEffect, useState } from 'react';
+import { analyseResult, getNotifFromResult, saveSyncPearlData } from 'utils/synchronize/check';
+import * as api from 'utils/api';
+import { AppContext } from 'Root';
+import Preloader from 'components/common/loader';
+import { SyncDialog } from './sychronizeDialog';
+
+export const SynchronizeWrapperContext = React.createContext();
+
+const SynchronizeWrapper = ({ children }) => {
+ const { online, PEARL_API_URL, PEARL_AUTHENTICATION_MODE } = useContext(AppContext);
+ const { checkQueen, synchronizeQueen, queenReady, queenError } = useQueenSynchronisation();
+
+ const [isSync, setIsSync] = useState(() => {
+ return window.localStorage.getItem('SYNCHRONIZE') === 'true';
+ });
+ const [loading, setLoading] = useState(false);
+ const [syncResult, setSyncResult] = useState(undefined);
+ const [componentReady, setComponentReady] = useState(false);
+
+ const [pearlReady, setPearlReady] = useState(null);
+ const [pearlError, setPearlError] = useState(false);
+
+ const checkPearl = async () => {
+ setPearlReady(null);
+ const { error, status } = await api.healthCheck(PEARL_API_URL, PEARL_AUTHENTICATION_MODE);
+ if (!error && status === 200) {
+ setPearlError(false);
+ setPearlReady(true);
+ } else {
+ setPearlError(true);
+ setPearlReady(true);
+ }
+ };
+
+ useEffect(() => {
+ setComponentReady(true);
+ }, [isSync]);
+
+ useEffect(() => {
+ const analyse = async () => {
+ const result = await analyseResult(PEARL_API_URL, PEARL_AUTHENTICATION_MODE);
+ window.localStorage.removeItem('SYNCHRONIZE');
+ setIsSync(false);
+ setSyncResult(result);
+ };
+
+ if (PEARL_API_URL && PEARL_AUTHENTICATION_MODE && isSync && !loading) analyse();
+ }, [PEARL_API_URL, PEARL_AUTHENTICATION_MODE, isSync, loading]);
+
+ const syncFunction = () => {
+ const launchSynchronize = async () => {
+ window.localStorage.removeItem('PEARL_SYNC_RESULT');
+ window.localStorage.removeItem('QUEEN_SYNC_RESULT');
+ window.localStorage.setItem('SYNCHRONIZE', true);
+ setLoading(true);
+ checkQueen();
+ await checkPearl();
+ };
+ if (online) launchSynchronize();
+ };
+
+ const close = async () => {
+ setSyncResult(null);
+ window.localStorage.removeItem('PEARL_SYNC_RESULT');
+ window.localStorage.removeItem('QUEEN_SYNC_RESULT');
+ };
+
+ useEffect(() => {
+ const sync = async () => {
+ setIsSync(true);
+ const result = await synchronizePearl(PEARL_API_URL, PEARL_AUTHENTICATION_MODE);
+ saveSyncPearlData(result);
+ const { error } = result;
+ if (!error) await synchronizeQueen();
+ else setLoading(false);
+ };
+
+ const failedSync = async () => {
+ const result = {
+ state: 'error',
+ messages: [D.syncNotStarted, D.syncPleaseTryAgain, D.warningOrErrorEndMessage],
+ };
+ const notif = getNotifFromResult(result);
+ await notificationIdbService.addOrUpdateNotif(notif);
+ window.localStorage.removeItem('SYNCHRONIZE');
+ setIsSync(false);
+ setSyncResult(result);
+ setLoading(false);
+ };
+ if (queenReady && pearlReady) {
+ if (!queenError && !pearlError) sync();
+ else failedSync();
+ }
+ }, [
+ queenReady,
+ queenError,
+ pearlReady,
+ pearlError,
+ synchronizeQueen,
+ PEARL_API_URL,
+ PEARL_AUTHENTICATION_MODE,
+ ]);
+
+ const context = { syncFunction, setSyncResult };
+
+ const syncMesssage = () => {
+ if (loading && isSync) return D.synchronizationInProgress;
+ if (loading) return D.synchronizationWaiting;
+ if (isSync) return D.synchronizationEnding;
+ };
+
+ return (
+
+ {componentReady && (loading || isSync) && }
+ {componentReady && !loading && !isSync && syncResult && (
+
+ )}
+ {componentReady && !loading && !isSync && children}
+
+ );
+};
+
+export default SynchronizeWrapper;
diff --git a/src/components/sychronizeWrapper/sychronizeDialog.js b/src/components/sychronizeWrapper/sychronizeDialog.js
new file mode 100644
index 000000000..14e13bde7
--- /dev/null
+++ b/src/components/sychronizeWrapper/sychronizeDialog.js
@@ -0,0 +1,146 @@
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ Divider,
+ makeStyles,
+ Typography,
+} from '@material-ui/core';
+import { ExpandMore, ThumbUpAlt } from '@material-ui/icons';
+import { IconStatus } from 'components/common/IconStatus';
+import React from 'react';
+import D from 'i18n';
+
+const useStyles = makeStyles(theme => ({
+ dialogPaper: {
+ borderRadius: '15px',
+ },
+ title: {
+ '& *': {
+ fontSize: '1.4em',
+ },
+ },
+ subTitle: {
+ '& span': {
+ fontWeight: 'bold',
+ marginLeft: '1em',
+ alignSelf: 'center',
+ },
+ display: 'flex',
+ marginBottom: '1.5em',
+ },
+ content: {
+ '& span': {
+ alignSelf: 'center',
+ },
+
+ display: 'flex',
+ },
+ noVisibleFocus: {
+ '&:focus, &:hover': {
+ backgroundColor: theme.palette.primary.main,
+ },
+ },
+ positive: { marginLeft: '0.5em', color: theme.palette.success.main },
+ details: {
+ marginTop: '2em',
+ //boxShadow: '3px 0 0.8em grey, -3px 0 0.8em grey',
+ },
+ detailsContent: { display: 'block' },
+}));
+
+export const SyncDialog = ({ close, syncResult }) => {
+ const { state, date, messages, details } = syncResult;
+
+ const { transmittedSurveyUnits, loadedSurveyUnits } = details || {};
+
+ const getdetailsMessagesByCampaign = () => {
+ const transmittedCampaigns = Object.keys(transmittedSurveyUnits || {});
+ const loadedCampaigns = Object.keys(loadedSurveyUnits || {});
+ const allCampaigns = transmittedCampaigns.reduce((_, c) => {
+ if (!loadedCampaigns.includes(c)) return [..._, c];
+ return _;
+ }, loadedCampaigns);
+ return allCampaigns.reduce((_, c) => {
+ const transNB = (transmittedSurveyUnits[c] || []).length;
+ const loadNB = (loadedSurveyUnits[c] || []).length;
+ const transmittedMessage = transNB > 0 ? D.transmittedSurveyUnits(transNB) : null;
+ const loadedMessage = loadNB > 0 ? D.loadedSurveyUnits(loadNB) : null;
+ if (transmittedMessage || loadedMessage)
+ return [..._, { campaign: c, transmittedMessage, loadedMessage }];
+ return _;
+ }, []);
+ };
+
+ const detailsMessage = getdetailsMessagesByCampaign();
+
+ const classes = useStyles();
+ return (
+
+ );
+};
diff --git a/src/i18n/buttonMessage.js b/src/i18n/buttonMessage.js
index 1bb2e4647..10ebec1fe 100644
--- a/src/i18n/buttonMessage.js
+++ b/src/i18n/buttonMessage.js
@@ -12,6 +12,13 @@ const buttonMessage = {
updateNow: { fr: 'Mettre à jour maintenant', en: 'Update now' },
yesButton: { fr: 'Oui', en: 'Yes' },
noButton: { fr: 'Non', en: 'No' },
+ iUnderstand: { fr: `J'ai compris`, en: `I understand` },
+ deleteAll: { fr: 'Tout supprimer', en: 'Delete all' },
+ markAllAsRead: { fr: 'Tout marquer comme lu', en: 'Mark all as read' },
+ goBackToHome: { fr: "Retour à l'accueil", en: 'Go back home' },
+ yesDeleteAll: { fr: 'Oui, je supprime tout', en: 'Yes, I delete all' },
+ noImNotSure: { fr: 'Non, je ne suis pas sûr(e)', en: 'No, I am not sure' },
+ confirmButton: { fr: 'Confirmer', en: 'Confirm' },
};
export default buttonMessage;
diff --git a/src/i18n/criteriaMessage.js b/src/i18n/criteriaMessage.js
index 20e430b28..fd9e703b6 100644
--- a/src/i18n/criteriaMessage.js
+++ b/src/i18n/criteriaMessage.js
@@ -1,8 +1,12 @@
const criteriaMessage = {
+ sortBy: { fr: 'Trier par', en: 'Sort by' },
remainingDays: { fr: 'Jours restants', en: 'Remaining days' },
priority: { fr: 'Priorité', en: 'Priority' },
survey: { fr: 'Enquête', en: 'Campaign' },
subSample: { fr: 'Sous-échantillon', en: 'Sub sample' },
+ sortSurvey: { fr: 'Enquêtes', en: 'Surveys' },
+ sortToDo: { fr: 'À faire', en: 'To do' },
+ sortCompleted: { fr: 'Terminées', en: 'Completed' },
};
export default criteriaMessage;
diff --git a/src/i18n/dictionary.js b/src/i18n/dictionary.js
index 2b428c42c..4a5829c9f 100644
--- a/src/i18n/dictionary.js
+++ b/src/i18n/dictionary.js
@@ -17,6 +17,10 @@ import titleMessage from './titleMessage';
import toDoMessage from './toDoMessage';
import transmissionMessage from './transmissionMessage';
import waitingMessage from './waitingMessage';
+import mailMessage from './mailMessage';
+import syncMessage from './syncMessage';
+import notificationMessage from './notificationMessage';
+import resetDataMessage from './resetDataMessage';
const dictionary = {
pageNotFound: {
@@ -41,19 +45,28 @@ const dictionary = {
connexionOK: { fr: 'Connexion OK', en: 'Connection ok' },
connexionKO: { fr: 'Pas de réseau', en: 'No network' },
interviewer: { fr: 'Enquêteur', en: 'Interviewer' },
- syncResult: { fr: 'Résultat de la synchronisation', en: 'Result of synchronization' },
- syncSuccess: { fr: 'La synchronisation a réussi.', en: 'Synchronization succeeded.' },
- syncFailure: {
- fr: 'La synchronisation a échoué, veuillez recommencer.',
- en: 'Synchronization has failed, please try again.',
+ appInstalling: {
+ fr: 'Installation, veuillez patientez...',
+ en: 'Installation, please wait...',
},
- appInstalling: { fr: 'Installation, veuillez patientez...', en: 'Installation, please wait...' },
updateAvailable: {
fr:
"Une nouvelle version de l'application est disponible et sera utilisée lorsque tous les onglets de cette page seront fermés.",
en:
'New version of the application is available and will be used when all tabs for this page are closed.',
},
+ updateInstalled: {
+ fr: "L'application a été mise à jour avec succès",
+ en: 'The application has been successfully updated',
+ },
+ updating: {
+ fr: 'Mise à jour en cours',
+ en: 'Update in progress',
+ },
+ installError: {
+ fr: "Erreur lors de l'installation de l'application",
+ en: 'Error during the installation of the application',
+ },
appReadyOffline: {
fr:
"L'application est prête à être utilisée hors ligne. (Pensez à synchroniser vos données avant)",
@@ -61,6 +74,8 @@ const dictionary = {
},
areYouSure: { fr: 'Êtes-vous sûr ?', en: 'Are you sure ?' },
delete: { fr: 'Supprimer', en: 'Delete' },
+ other: { fr: 'Autre', en: 'Other' },
+
...buttonMessage,
...navigationMessage,
...waitingMessage,
@@ -80,6 +95,10 @@ const dictionary = {
...ageGroupsMessage,
...titleMessage,
...phoneSourceMessage,
+ ...mailMessage,
+ ...syncMessage,
+ ...notificationMessage,
+ ...resetDataMessage,
};
export default dictionary;
diff --git a/src/i18n/mailMessage.js b/src/i18n/mailMessage.js
new file mode 100644
index 000000000..5b5a0283c
--- /dev/null
+++ b/src/i18n/mailMessage.js
@@ -0,0 +1,109 @@
+const commonMailMessage = {
+ autoMail: {
+ fr: `Ceci est un message envoyé automatiquement par l'application suite à une erreur lors de la synchronisation.`,
+ en: `This is a message sent automatically by the application following an error during synchronization.`,
+ },
+ subjectTitle: {
+ fr: `Problème lors de la synchronisation`,
+ en: `Problem during synchronization`,
+ },
+};
+
+const mailMessage = {
+ subjectPearlMissingUnits: {
+ fr: `${commonMailMessage.subjectTitle.fr} : Trop d'unités enquêtées`,
+ en: `${commonMailMessage.subjectTitle.en} : Too many survey-units`,
+ },
+ subjectQueenMissingUnits: {
+ fr: `${commonMailMessage.subjectTitle.fr} : Il manque des unités enquêtées`,
+ en: `${commonMailMessage.subjectTitle.en} : Survey-units are missing`,
+ },
+ subjectTempZone: {
+ fr: `${commonMailMessage.subjectTitle.fr} : La sauvegarde des certaines unités enquêtées n'a pas fonctionné correctement`,
+ en: `${commonMailMessage.subjectTitle.en} : The backup of some surveyed units did not work properly.`,
+ },
+ bodyPearlMissingUnits: {
+ fr: userId => (pearlMissing = []) => {
+ return (
+ `Bonjour Madame, Monsieur. \n\n ` +
+ `Pour information, l'utilisateur d'identifant "${userId}" a reçu lors de sa synchronisation, trop d'unités enquêtées pour la partie questionnaire.\n` +
+ `Les unités présentes "en trop" sur son poste sont : ${pearlMissing.join(', ')}.\n\n` +
+ `Merci.\n\n ${commonMailMessage.autoMail.fr}`
+ );
+ },
+ en: userId => (pearlMissing = []) => {
+ return (
+ `Hello. \n\n ` +
+ `For information, the user of identifier "${userId}" received during its synchronization, too many survey-units for the questionnaire part.\n` +
+ `The units present "in excess" on his computer are : ${pearlMissing.join(', ')}.\n\n` +
+ `Thank you.\n\n ${commonMailMessage.autoMail.en}`
+ );
+ },
+ },
+ bodyQueenMissingUnits: {
+ fr: userId => (queenMissing = []) => {
+ return (
+ `Bonjour Madame, Monsieur. \n\n ` +
+ `Pour information, l'utilisateur d'identifant "${userId}" ne peut pas accéder aux questionnaires de certaines unités enquêtées (Il n'a pas reçu toutes les unités enquêtées dont il a la charge ou il n'a pas reçu les ressources des questionnaires.)\n` +
+ `Les unités problématiques sur son poste sont : ${queenMissing.join(', ')}.\n` +
+ `Par conséquent, l'utilisateur ne peut pas collecter de réponses au questionnaire pour ces unités, l'accès au questionnaire est donc bloqué pour celles-ci.\n\n` +
+ `Merci.\n\n ${commonMailMessage.autoMail.fr}`
+ );
+ },
+ en: userId => (queenMissing = []) => {
+ return (
+ `Hello. \n\n ` +
+ `For information, the user of identifier "${userId}" cannot access the questionnaires of some survey-units (He did not receive all the survey-units for which he is responsible or he did not receive the resources for the questionnaires).\n` +
+ `The problematic survey-units on his computer are : ${queenMissing.join(', ')}.\n` +
+ `Therefore, the user cannot collect questionnaire responses for these units, so access to the questionnaire is blocked for them.\n\n` +
+ `Thank you.\n\n ${commonMailMessage.autoMail.fr}`
+ );
+ },
+ },
+ bodyTempZonePearl: {
+ fr: userId => (tempZoneUnits = []) => {
+ return (
+ `Bonjour Madame, Monsieur. \n\n` +
+ `Pour information, l'utilisateur d'identifant "${userId}" n'a pas pu sauvegardé correctement certaines unités enquêtées pour un problème de droit.\n` +
+ `Les données sont de nature organisationnelle.\n` +
+ `Les unités concernées sont : ${tempZoneUnits.join(', ')}.\n` +
+ `Ces unités ont donc été sauvegardées dans une zone tampon en attendant un éventuel traitement.\n\n` +
+ `Merci de bien en prendre notes, afin de vérifier qu'il ne s'agît pas d'une erreur.\n\n ${commonMailMessage.autoMail.fr}`
+ );
+ },
+ en: userId => (tempZoneUnits = []) => {
+ return (
+ `Hello. \n\n` +
+ `For information, the user of identifier "${userId}" was not able to correctly save some of the survey-units due to a rights issue.\n` +
+ `The data is organizational.\n` +
+ `The survey-units are : ${tempZoneUnits.join(', ')}.\n` +
+ `These units were therefore saved in a buffer zone pending possible treatment.\n\n` +
+ `Please take note of it, to make sure it is not a mistake.\n\n ${commonMailMessage.autoMail.en}`
+ );
+ },
+ },
+ bodyTempZoneQueen: {
+ fr: userId => (tempZoneUnits = []) => {
+ return (
+ `Bonjour Madame, Monsieur. \n\n` +
+ `Pour information, l'utilisateur d'identifant "${userId}" n'a pas pu sauvegardé correctement certaines unités enquêtées pour un problème de droit.\n` +
+ `Les données sont de nature questionnaire.\n` +
+ `Les unités concernées sont : ${tempZoneUnits.join(', ')}.\n` +
+ `Ces unités ont donc été sauvegardées dans une zone tampon en attendant un éventuel traitement.\n\n` +
+ `Merci de bien en prendre notes, afin de vérifier qu'il ne s'agît pas d'une erreur.\n\n ${commonMailMessage.autoMail.fr}`
+ );
+ },
+ en: userId => (tempZoneUnits = []) => {
+ return (
+ `Hello. \n\n` +
+ `For information, the user of identifier "${userId}" was not able to correctly save some of the survey-units due to a rights issue.\n` +
+ `These are questionnaire data.\n` +
+ `The survey-units are : ${tempZoneUnits.join(', ')}.\n` +
+ `These units were therefore saved in a buffer zone pending possible treatment.\n\n` +
+ `Please take note of it, to make sure it is not a mistake.\n\n ${commonMailMessage.autoMail.en}`
+ );
+ },
+ },
+};
+
+export default mailMessage;
diff --git a/src/i18n/notificationMessage.js b/src/i18n/notificationMessage.js
new file mode 100644
index 000000000..8730e5e8e
--- /dev/null
+++ b/src/i18n/notificationMessage.js
@@ -0,0 +1,9 @@
+const notificationMessage = {
+ notifications: { fr: 'Notifications', en: 'Notifications' },
+ notifManagement: { fr: 'Gestion', en: 'Management' },
+ noNotification: { fr: 'Aucune notification', en: 'No notification' },
+ notificationsType: { fr: 'Type de notifications', en: 'Type of notifications' },
+ allNotifs: { fr: 'Toutes', en: 'All' },
+};
+
+export default notificationMessage;
diff --git a/src/i18n/resetDataMessage.js b/src/i18n/resetDataMessage.js
new file mode 100644
index 000000000..fdeea8ac6
--- /dev/null
+++ b/src/i18n/resetDataMessage.js
@@ -0,0 +1,41 @@
+const resetDataMessage = {
+ deleting: { fr: 'Suppression ...', en: 'Deleting ...' },
+ deleteSuccess: {
+ fr: 'Toutes les données ont bien été supprimées.',
+ en: 'All data has been deleted.',
+ },
+ deleteFailed: {
+ fr: 'Il y a eu un problème lors de la suppression des données.',
+ en: 'There was a problem when deleting the data.',
+ },
+ youCanDeleteData: {
+ fr: 'Vous pouvez ici vider la base de données local de votre navigateur.',
+ en: 'Here you can empty the local database of your browser.',
+ },
+ firstBodyDialog: {
+ fr:
+ "Vous allez perdre l'ensemble de vos données. Assurez vous que vos données sont déjà sauvegardées (par une synchronisation ou autre). Êtes vous sûr(e) de vouloir tout supprimer ?",
+ en:
+ 'You will lose all your data. Make sure your data is already backed up (by a synchronization or otherwise). Are you sure you want to delete everything ?',
+ },
+ mainTitle: { fr: 'Zone dangereuse', en: 'Danger zone' },
+ confirmTitle: { fr: 'Confirmation', en: 'Confirmation' },
+ confirmRandom: {
+ fr: 'Veuillez entrer le texte ci dessous pour confirmer la suppression.',
+ en: 'Please enter the text below to confirm the deletion.',
+ },
+ confirmId: {
+ fr: "Plus qu'une étape, veuillez également saisir votre identifiant.",
+ en: 'More than a step, please also enter your username.',
+ },
+ confirmError: {
+ fr: 'La saisie ne correspond pas avec le texte ci-dessus.',
+ en: 'The entry does not match the text above.',
+ },
+ confirmErrorUser: {
+ fr: "Ce n'est pas votre identifiant.",
+ en: 'This is not your login.',
+ },
+};
+
+export default resetDataMessage;
diff --git a/src/i18n/surveyUnitMessage.js b/src/i18n/surveyUnitMessage.js
index b2c18de12..fa7947c52 100644
--- a/src/i18n/surveyUnitMessage.js
+++ b/src/i18n/surveyUnitMessage.js
@@ -5,5 +5,9 @@ const surveyUnitMessage = {
},
surveyUnit: { fr: 'unité enquêtée', en: 'survey unit' },
surveyUnits: { fr: 'unités enquêtées', en: 'survey units' },
+ questionnaireInaccessible: {
+ fr: 'Questionnaire inaccessible',
+ en: 'Questionnaire not available',
+ },
};
export default surveyUnitMessage;
diff --git a/src/i18n/syncMessage.js b/src/i18n/syncMessage.js
new file mode 100644
index 000000000..6b1573a95
--- /dev/null
+++ b/src/i18n/syncMessage.js
@@ -0,0 +1,86 @@
+const syncMessage = {
+ simpleSync: { fr: 'Synchronisation', en: 'Synchronization' },
+ syncResult: { fr: 'Résultat de la synchronisation', en: 'Result of synchronization' },
+ syncSuccess: { fr: 'La synchronisation a réussi.', en: 'Synchronization succeeded.' },
+ syncFailure: {
+ fr: 'La synchronisation a échoué, veuillez recommencer.',
+ en: 'Synchronization has failed, please try again.',
+ },
+ synchronizationInProgress: { fr: 'Synchronisation en cours', en: 'Synchronization in progress' },
+ synchronizationWaiting: {
+ fr: 'En attente de synchronisation',
+ en: 'Waiting for synchronization.',
+ },
+ synchronizationEnding: { fr: 'Fin de la synchronisation', en: 'End of synchronization.' },
+
+ syncNotStarted: {
+ fr: `La synchronisation n'a pas démarré, car le serveur ne répond pas.`,
+ en: `The synchronization did not start, because the server does not respond.`,
+ },
+ titleSync: {
+ fr: type => {
+ if (type === 'success') return `La synchronisation a réussi.`;
+ if (type === 'warning') return `Oups, il y a eu quelques soucis lors de la synchronisation.`;
+ if (type === 'error') return `La synchronisation a échoué.`;
+ return '';
+ },
+ en: type => {
+ if (type === 'success') return `The synchronization was successful.`;
+ if (type === 'warning') return `Oops, there were some problems during the synchronization`;
+ if (type === 'error') return `Synchronization has failed.`;
+ return '';
+ },
+ },
+ syncSuccessMessage: {
+ fr: `La synchronisation s'est bien passée, vous pouvez continuer à travailler.`,
+ en: `The synchronization went well, you can continue working.`,
+ },
+ warningOrErrorEndMessage: {
+ fr: `Nous vous rappelons qu'aucune donnée n'a été perdue. Elles sont déjà enregistrées sur le serveur ou encore sur votre poste.`,
+ en: `We remind you that no data has been lost. They are already saved on the server or on your computer.`,
+ },
+
+ syncPleaseTryAgain: {
+ fr: `Nous vous invitons à réessayer plus tard. Si ce message persiste, veuillez contacter l'assistance.`,
+ en: `Please try again later.. If this message persists, please contact support.`,
+ },
+ syncYouCanStillWork: {
+ fr: `Vous pouvez tout de même continuer à travailler.`,
+ en: `You can still continue to work.`,
+ },
+ syncQueenMissing: {
+ fr: `Certains questionnaires ne sont pas accessibles, l'administrateur de l'application a été prévenu.`,
+ en: `Some questionnaires are not accessible, the application administrator has been notified.`,
+ },
+ syncPearlMissing: {
+ fr: `Pour information, vous avez "trop" de données de niveau questionnaire si votre poste,. Cela n'est en rien bloquant. L'administrateur de l'application a été prévenu.`,
+ en: `For your information, you have "too much" questionnaire level data if your post,. This is not blocking anything. The application administrator has been notified.`,
+ },
+ syncNoPearlData: {
+ fr: `Pour information, vous n'avez récupéré aucune données.`,
+ en: `For your information, you have not retrieved any data.`,
+ },
+ syncStopOnError: {
+ fr: `La synchronisation s'est arrêtée.`,
+ en: `The synchronization has stopped.`,
+ },
+ syncTempZone: {
+ fr: `Pour information, certaines unités enquêtées n'ont pas pu être sauvegardées correctement pour un problème de droit. Ces unités ont donc été sauvegardées de manière sécurisée ailleurs, en attendant un éventuel traitement. L'administrateur de l'application a été prévenu.`,
+ en: `For your information, some of the survey-units could not be saved correctly due to legal issues. These units were therefore saved securely elsewhere, pending further processing. The application administrator has been notified.`,
+ },
+ detailsSync: { fr: 'Détails : Bilan de synchronisation', en: 'Details : Synchronization report' },
+ transmittedSurveyUnits: {
+ fr: n => (n > 1 ? `${n} unités enquêtées transmises` : `${n} unité enquêtée transmise`),
+ en: n => (n > 1 ? `${n} transmitted survey-units` : `${n} transmitted survey-unit`),
+ },
+ loadedSurveyUnits: {
+ fr: n => (n > 1 ? `${n} unités enquêtées chargées` : `${n} unité enquêtée chargée`),
+ en: n => (n > 1 ? `${n} loaded survey-units` : `${n} loaded survey-unit`),
+ },
+ nothingToDisplay: {
+ fr: 'Rien à afficher',
+ en: 'Nothing to display',
+ },
+};
+
+export default syncMessage;
diff --git a/src/i18n/waitingMessage.js b/src/i18n/waitingMessage.js
index 8ffe1eec9..49f6dcc74 100644
--- a/src/i18n/waitingMessage.js
+++ b/src/i18n/waitingMessage.js
@@ -1,7 +1,6 @@
const waitingMessage = {
pleaseWait: { fr: 'Veuillez patienter ...', en: 'Please wait ...' },
loading: { fr: 'Chargement ...', en: 'Loading ...' },
- synchronizationInProgress: { fr: 'Synchronisation en cours', en: 'Synchronization in progress' },
};
export default waitingMessage;
diff --git a/src/indexedbb/idb-config.js b/src/indexedbb/idb-config.js
index 96f448875..73a38b82d 100644
--- a/src/indexedbb/idb-config.js
+++ b/src/indexedbb/idb-config.js
@@ -1,10 +1,26 @@
import Dexie from 'dexie';
import schema from './schema.json';
+import schema2 from './schema-2.json';
export class LocalDbFactory extends Dexie {
constructor(dataBaseName) {
super(dataBaseName);
this.version(1).stores(schema);
+ // upgrade dataBase (please see https://dexie.org/docs/Tutorial/Design#database-versioning)
+ this.version(2)
+ .stores(schema2)
+ .upgrade(tx => {
+ // An upgrade function for version 2 will upgrade data based on version 1.
+ // delete unused attribute
+ return tx
+ .table('notification')
+ .toCollection()
+ .modify(notif => {
+ // Modify each friend:
+ delete notif.time;
+ delete notif.message;
+ });
+ });
}
getStore(name) {
diff --git a/src/indexedbb/schema-2.json b/src/indexedbb/schema-2.json
new file mode 100644
index 000000000..ec785ae30
--- /dev/null
+++ b/src/indexedbb/schema-2.json
@@ -0,0 +1,5 @@
+{
+ "notification": "++id,date,type,title,messages,state,read,detail,sync",
+ "surveyUnitMissing": "id",
+ "syncReport": "id,transmittedSurveyUnits,loadedSurveyUnits"
+}
diff --git a/src/indexedbb/services/notification-idb-service.js b/src/indexedbb/services/notification-idb-service.js
new file mode 100644
index 000000000..e70f20ce3
--- /dev/null
+++ b/src/indexedbb/services/notification-idb-service.js
@@ -0,0 +1,21 @@
+import AbstractIdbService from './abstract-idb-service';
+
+class NotificationIdbService extends AbstractIdbService {
+ constructor() {
+ super('notification');
+ }
+
+ async addOrUpdateNotif(item) {
+ const { id } = item;
+ /* prevent duplicated survey-unit */
+ if (id) {
+ const notification = await this.get(id);
+ if (notification) {
+ return this.update(item);
+ }
+ }
+ return this.insert(item);
+ }
+}
+
+export default new NotificationIdbService();
diff --git a/src/indexedbb/services/surveyUnitMissing-idb-service.js b/src/indexedbb/services/surveyUnitMissing-idb-service.js
new file mode 100644
index 000000000..a2f20beaf
--- /dev/null
+++ b/src/indexedbb/services/surveyUnitMissing-idb-service.js
@@ -0,0 +1,9 @@
+import AbstractIdbService from './abstract-idb-service';
+
+class SurveyUnitMissingIdbService extends AbstractIdbService {
+ constructor() {
+ super('surveyUnitMissing');
+ }
+}
+
+export default new SurveyUnitMissingIdbService();
diff --git a/src/indexedbb/services/syncReport-idb-service.js b/src/indexedbb/services/syncReport-idb-service.js
new file mode 100644
index 000000000..d4edb049a
--- /dev/null
+++ b/src/indexedbb/services/syncReport-idb-service.js
@@ -0,0 +1,18 @@
+import AbstractIdbService from './abstract-idb-service';
+
+class SyncReportIdbService extends AbstractIdbService {
+ constructor() {
+ super('syncReport');
+ }
+
+ async addOrUpdateReport(item) {
+ const { id, ...other } = item;
+ const report = await this.getById(id);
+ if (report) {
+ return this.update(item);
+ }
+ return this.insert({ id: `${id}`, ...other });
+ }
+}
+
+export default new SyncReportIdbService();
diff --git a/src/serviceWorkerRegistration.js b/src/serviceWorkerRegistration.js
index 73874bb91..2707edbf3 100644
--- a/src/serviceWorkerRegistration.js
+++ b/src/serviceWorkerRegistration.js
@@ -105,6 +105,9 @@ function registerValidSW(swUrl, config) {
};
})
.catch(error => {
+ if (config && config.onError) {
+ config.onError();
+ }
console.error('Error during service worker registration:', error);
});
}
@@ -121,6 +124,9 @@ function checkValidServiceWorker(swUrl, config) {
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
+ if (config && config.onError) {
+ config.onError();
+ }
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
@@ -137,11 +143,15 @@ function checkValidServiceWorker(swUrl, config) {
});
}
-export function unregister() {
+export function unregister(config) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
- registration.unregister();
+ registration.unregister().then(unregistered => {
+ if (config && config.onUnregister) {
+ config.onUnregister(unregistered);
+ }
+ });
})
.catch(error => {
console.error(error.message);
diff --git a/src/tests/common-tools/functions/surveyUnitFunctions.test.js b/src/tests/utils/functions/surveyUnitFunctions.test.js
similarity index 96%
rename from src/tests/common-tools/functions/surveyUnitFunctions.test.js
rename to src/tests/utils/functions/surveyUnitFunctions.test.js
index 93dadee8a..b724640fa 100644
--- a/src/tests/common-tools/functions/surveyUnitFunctions.test.js
+++ b/src/tests/utils/functions/surveyUnitFunctions.test.js
@@ -1,11 +1,11 @@
-const surveyUnitStateEnum = require('common-tools/enum/SUStateEnum');
+const surveyUnitStateEnum = require('utils/enum/SUStateEnum');
const {
getContactAttemptNumber,
getLastState,
isSelectable,
updateStateWithDates,
-} = require('common-tools/functions');
+} = require('utils/functions');
describe('getLastState', () => {
it('should return the only state', () => {
diff --git a/src/utils/api/fetcher.js b/src/utils/api/fetcher.js
new file mode 100644
index 000000000..7f801459d
--- /dev/null
+++ b/src/utils/api/fetcher.js
@@ -0,0 +1,33 @@
+// manage empty response during PUT or POST request
+const readJsonResponse = async response => {
+ try {
+ return await response.json();
+ } catch (e) {
+ return {};
+ }
+};
+
+export const fetcher = async (url, token, method, body) => {
+ const headers = { Accept: 'application/json', 'Content-Type': 'application/json' };
+ try {
+ const response = await fetch(url, {
+ headers: token ? { ...headers, Authorization: `Bearer ${token}` } : headers,
+ method,
+ body: body ? JSON.stringify(body) : null,
+ });
+ const { ok, status, statusText } = response;
+ if (ok) {
+ try {
+ const data = await readJsonResponse(response);
+ return { data, status, statusText };
+ } catch (error) {
+ return { error: true, status, statusText: error?.message };
+ }
+ } else {
+ return { error: true, status, statusText };
+ }
+ } catch (error) {
+ // network error
+ return { error: true, statusText: error.message };
+ }
+};
diff --git a/src/common-tools/api/index.js b/src/utils/api/index.js
similarity index 54%
rename from src/common-tools/api/index.js
rename to src/utils/api/index.js
index 11a3e5c38..d33126180 100644
--- a/src/common-tools/api/index.js
+++ b/src/utils/api/index.js
@@ -1 +1,2 @@
export * from './surveyUnitAPI';
+export * from './utilsAPI';
diff --git a/src/utils/api/requests.js b/src/utils/api/requests.js
new file mode 100644
index 000000000..137fb7966
--- /dev/null
+++ b/src/utils/api/requests.js
@@ -0,0 +1,33 @@
+import { fetcher } from './fetcher';
+
+const getRequest = url => token => fetcher(url, token, 'GET', null);
+const putRequest = url => token => body => fetcher(url, token, 'PUT', body);
+const postRequest = url => token => body => fetcher(url, token, 'POST', body);
+
+/* All surveyUnits */
+const getSurveyUnits = apiUrl => token => getRequest(`${apiUrl}/api/survey-units`)(token);
+
+/* SurveyUnit's data */
+const getSurveyUnitById = apiUrl => id => token =>
+ getRequest(`${apiUrl}/api/survey-unit/${id}`)(token);
+const putDataSurveyUnitById = apiUrl => id => token => body =>
+ putRequest(`${apiUrl}/api/survey-unit/${id}`)(token)(body);
+const putToTempZone = apiUrl => id => token => body =>
+ postRequest(`${apiUrl}/api/survey-unit/${id}/temp-zone`)(token)(body);
+
+const sendMail = apiUrl => subject => content => token => {
+ const mailBody = { subject, content };
+ return postRequest(`${apiUrl}/api/mail`)(token)(mailBody);
+};
+
+const healthCheck = apiUrl => token => getRequest(`${apiUrl}/api/healthcheck`)(token);
+
+export const API = {
+ getRequest,
+ getSurveyUnits,
+ getSurveyUnitById,
+ putDataSurveyUnitById,
+ putToTempZone,
+ sendMail,
+ healthCheck,
+};
diff --git a/src/utils/api/surveyUnitAPI.js b/src/utils/api/surveyUnitAPI.js
new file mode 100644
index 000000000..85bd19ce2
--- /dev/null
+++ b/src/utils/api/surveyUnitAPI.js
@@ -0,0 +1,42 @@
+import { authentication, getToken } from './utils';
+import { API } from './requests';
+
+export const getSurveyUnits = async (urlPearApi, authenticationMode) => {
+ try {
+ await authentication(authenticationMode);
+ const token = getToken();
+ return API.getSurveyUnits(urlPearApi)(token);
+ } catch (e) {
+ throw new Error(`Error during refreshToken : ${e}`);
+ }
+};
+
+export const getSurveyUnitById = (urlPearApi, authenticationMode) => async id => {
+ try {
+ await authentication(authenticationMode);
+ const token = getToken();
+ return API.getSurveyUnitById(urlPearApi)(id)(token);
+ } catch (e) {
+ throw new Error(`Error during refreshToken : ${e}`);
+ }
+};
+
+export const putDataSurveyUnitById = (urlPearApi, authenticationMode) => async (id, su) => {
+ try {
+ await authentication(authenticationMode);
+ const token = getToken();
+ return API.putDataSurveyUnitById(urlPearApi)(id)(token)(su);
+ } catch (e) {
+ throw new Error(`Error during refreshToken : ${e}`);
+ }
+};
+
+export const putSurveyUnitToTempZone = (urlPearApi, authenticationMode) => async (id, su) => {
+ try {
+ await authentication(authenticationMode);
+ const token = getToken();
+ return API.putToTempZone(urlPearApi)(id)(token)(su);
+ } catch (e) {
+ throw new Error(`Error during refreshToken : ${e}`);
+ }
+};
diff --git a/src/common-tools/api/utils.js b/src/utils/api/utils.js
similarity index 84%
rename from src/common-tools/api/utils.js
rename to src/utils/api/utils.js
index 1ae01a730..833cb1ca3 100644
--- a/src/common-tools/api/utils.js
+++ b/src/utils/api/utils.js
@@ -1,5 +1,7 @@
-import { ANONYMOUS, JSON_UTF8_HEADER, KEYCLOAK, PEARL_USER_KEY } from 'common-tools/constants';
-import { kc, keycloakAuthentication, refreshToken } from 'common-tools/keycloak';
+import { ANONYMOUS, JSON_UTF8_HEADER, KEYCLOAK, PEARL_USER_KEY } from 'utils/constants';
+import { kc, keycloakAuthentication, refreshToken } from 'utils/keycloak';
+
+export const getToken = () => kc.token;
export const getSecureHeader = token =>
token
@@ -33,7 +35,7 @@ export const getHeader = mode => {
};
}
return {
- ...getSecureHeader(kc.token),
+ ...getSecureHeader(getToken()),
Accept: JSON_UTF8_HEADER,
};
default:
diff --git a/src/utils/api/utilsAPI.js b/src/utils/api/utilsAPI.js
new file mode 100644
index 000000000..1f381a973
--- /dev/null
+++ b/src/utils/api/utilsAPI.js
@@ -0,0 +1,22 @@
+import { authentication, getToken } from './utils';
+import { API } from './requests';
+
+export const sendMail = (urlPearApi, authenticationMode) => async (subject, content) => {
+ try {
+ await authentication(authenticationMode);
+ const token = getToken();
+ return API.sendMail(urlPearApi)(subject)(content)(token);
+ } catch (e) {
+ throw new Error(`Error during refreshToken : ${e}`);
+ }
+};
+
+export const healthCheck = async (urlPearApi, authenticationMode) => {
+ try {
+ await authentication(authenticationMode);
+ const token = getToken();
+ return API.healthCheck(urlPearApi)(token);
+ } catch (e) {
+ throw new Error(`Error during refreshToken : ${e}`);
+ }
+};
diff --git a/src/utils/auth/initAuth.js b/src/utils/auth/initAuth.js
new file mode 100644
index 000000000..b704152ce
--- /dev/null
+++ b/src/utils/auth/initAuth.js
@@ -0,0 +1,72 @@
+import { GUEST_PEARL_USER, PEARL_USER_KEY } from 'utils/constants';
+import { getTokenInfo, keycloakAuthentication } from 'utils/keycloak';
+import { useContext, useEffect, useState } from 'react';
+import { AppContext } from 'Root';
+
+export const useAuth = () => {
+ const [authenticated, setAuthenticated] = useState(false);
+ const configuration = useContext(AppContext);
+
+ const interviewerRoles = ['pearl-interviewer', 'uma_authorization', 'Guest'];
+
+ const accessAuthorized = () => {
+ setAuthenticated(true);
+ };
+
+ const accessDenied = () => {
+ setAuthenticated(false);
+ };
+
+ const isAuthorized = roles => roles.filter(r => interviewerRoles.includes(r)).length > 0;
+
+ const isLocalStorageTokenValid = () => {
+ const interviewer = JSON.parse(window.localStorage.getItem(PEARL_USER_KEY));
+ if (interviewer && interviewer.roles) {
+ const { roles } = interviewer;
+ if (isAuthorized(roles)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ useEffect(() => {
+ const { PEARL_AUTHENTICATION_MODE } = configuration;
+ switch (PEARL_AUTHENTICATION_MODE) {
+ case 'anonymous':
+ window.localStorage.setItem(PEARL_USER_KEY, JSON.stringify(GUEST_PEARL_USER));
+ accessAuthorized();
+ break;
+
+ case 'keycloak':
+ if (!authenticated) {
+ keycloakAuthentication({
+ onLoad: 'login-required',
+ checkLoginIframe: false,
+ })
+ .then(auth => {
+ if (auth) {
+ const interviewerInfos = getTokenInfo();
+ const { roles } = interviewerInfos;
+ if (isAuthorized(roles)) {
+ window.localStorage.setItem(PEARL_USER_KEY, JSON.stringify(interviewerInfos));
+ accessAuthorized();
+ } else {
+ accessDenied();
+ }
+ // offline mode
+ } else if (isLocalStorageTokenValid()) {
+ accessAuthorized();
+ } else {
+ accessDenied();
+ }
+ })
+ .catch(() => (isLocalStorageTokenValid() ? accessAuthorized() : accessDenied()));
+ }
+ break;
+ default:
+ }
+ });
+
+ return { authenticated };
+};
diff --git a/src/common-tools/constants/index.js b/src/utils/constants/index.js
similarity index 83%
rename from src/common-tools/constants/index.js
rename to src/utils/constants/index.js
index 7e7ddf61b..4e1e0230c 100644
--- a/src/common-tools/constants/index.js
+++ b/src/utils/constants/index.js
@@ -1,5 +1,8 @@
-import contactAttemptEnum from 'common-tools/enum/ContactAttemptEnum';
-import surveyUnitStateEnum from 'common-tools/enum/SUStateEnum';
+import contactAttemptEnum from 'utils/enum/ContactAttemptEnum';
+import surveyUnitStateEnum from 'utils/enum/SUStateEnum';
+
+export const NOTIFICATION_TYPE_SYNC = 'synchronization';
+export const NOTIFICATION_TYPE_MANAGEMENT = 'management';
export const KEYCLOAK = 'keycloak';
export const ANONYMOUS = 'anonymous';
@@ -9,7 +12,7 @@ export const PEARL_URL = window.localStorage.getItem('PEARL_URL') || '';
export const PEARL_USER_KEY = 'pearl-user';
export const GUEST_PEARL_USER = {
lastName: 'Guest',
- firstName: 'Guest',
+ firstName: 'Amazing',
id: 'Guest',
roles: ['Guest'],
};
diff --git a/src/common-tools/enum/ContactAttemptEnum.js b/src/utils/enum/ContactAttemptEnum.js
similarity index 100%
rename from src/common-tools/enum/ContactAttemptEnum.js
rename to src/utils/enum/ContactAttemptEnum.js
diff --git a/src/common-tools/enum/ContactOutcomEnum.js b/src/utils/enum/ContactOutcomEnum.js
similarity index 100%
rename from src/common-tools/enum/ContactOutcomEnum.js
rename to src/utils/enum/ContactOutcomEnum.js
diff --git a/src/common-tools/enum/QuestionnaireStateEnum.js b/src/utils/enum/QuestionnaireStateEnum.js
similarity index 100%
rename from src/common-tools/enum/QuestionnaireStateEnum.js
rename to src/utils/enum/QuestionnaireStateEnum.js
diff --git a/src/common-tools/enum/SUStateEnum.js b/src/utils/enum/SUStateEnum.js
similarity index 100%
rename from src/common-tools/enum/SUStateEnum.js
rename to src/utils/enum/SUStateEnum.js
diff --git a/src/common-tools/enum/SUToDoEnum.js b/src/utils/enum/SUToDoEnum.js
similarity index 100%
rename from src/common-tools/enum/SUToDoEnum.js
rename to src/utils/enum/SUToDoEnum.js
diff --git a/src/common-tools/enum/formEnum.js b/src/utils/enum/formEnum.js
similarity index 100%
rename from src/common-tools/enum/formEnum.js
rename to src/utils/enum/formEnum.js
diff --git a/src/common-tools/functions/convertSUStateInToDo.js b/src/utils/functions/convertSUStateInToDo.js
similarity index 89%
rename from src/common-tools/functions/convertSUStateInToDo.js
rename to src/utils/functions/convertSUStateInToDo.js
index f7b52bca1..7913ebf52 100644
--- a/src/common-tools/functions/convertSUStateInToDo.js
+++ b/src/utils/functions/convertSUStateInToDo.js
@@ -1,5 +1,5 @@
-import suStateEnum from 'common-tools/enum/SUStateEnum';
-import toDoEnum from 'common-tools/enum/SUToDoEnum';
+import suStateEnum from 'utils/enum/SUStateEnum';
+import toDoEnum from 'utils/enum/SUToDoEnum';
export const convertSUStateInToDo = suState => {
if (
diff --git a/src/common-tools/functions/index.js b/src/utils/functions/index.js
similarity index 100%
rename from src/common-tools/functions/index.js
rename to src/utils/functions/index.js
diff --git a/src/common-tools/functions/sortOnColumn.js b/src/utils/functions/sortOnColumn.js
similarity index 92%
rename from src/common-tools/functions/sortOnColumn.js
rename to src/utils/functions/sortOnColumn.js
index 65366cb05..ccbdaa6b4 100644
--- a/src/common-tools/functions/sortOnColumn.js
+++ b/src/utils/functions/sortOnColumn.js
@@ -1,4 +1,4 @@
-import { intervalInDays } from 'common-tools/functions/surveyUnitFunctions';
+import { intervalInDays } from 'utils/functions/surveyUnitFunctions';
export const sortOnColumnCompareFunction = criteria => {
let compareFunction;
diff --git a/src/common-tools/functions/surveyUnitFunctions.js b/src/utils/functions/surveyUnitFunctions.js
similarity index 97%
rename from src/common-tools/functions/surveyUnitFunctions.js
rename to src/utils/functions/surveyUnitFunctions.js
index d0432ec63..929d941dc 100644
--- a/src/common-tools/functions/surveyUnitFunctions.js
+++ b/src/utils/functions/surveyUnitFunctions.js
@@ -1,6 +1,6 @@
-import { CONTACT_RELATED_STATES, CONTACT_SUCCESS_LIST } from 'common-tools/constants';
-import surveyUnitStateEnum from 'common-tools/enum/SUStateEnum';
-import { convertSUStateInToDo } from 'common-tools/functions/convertSUStateInToDo';
+import { CONTACT_RELATED_STATES, CONTACT_SUCCESS_LIST } from 'utils/constants';
+import surveyUnitStateEnum from 'utils/enum/SUStateEnum';
+import { convertSUStateInToDo } from 'utils/functions/convertSUStateInToDo';
import { differenceInYears, formatDistanceStrict } from 'date-fns';
import D from 'i18n';
import surveyUnitDBService from 'indexedbb/services/surveyUnit-idb-service';
@@ -197,11 +197,11 @@ export const updateStateWithDates = surveyUnit => {
return result;
};
-export const isQuestionnaireAvailable = su => {
+export const isQuestionnaireAvailable = su => inaccessible => {
const { collectionEndDate, collectionStartDate } = su;
const now = new Date().getTime();
- return now >= collectionStartDate && now <= collectionEndDate;
+ return !inaccessible && now >= collectionStartDate && now <= collectionEndDate;
};
export const applyFilters = (surveyUnits, filters) => {
diff --git a/src/utils/hooks/configuration.js b/src/utils/hooks/configuration.js
new file mode 100644
index 000000000..5c175a407
--- /dev/null
+++ b/src/utils/hooks/configuration.js
@@ -0,0 +1,18 @@
+import { useState, useEffect } from 'react';
+
+export const useConfiguration = () => {
+ const [configuration, setConfiguration] = useState(null);
+
+ useEffect(() => {
+ if (!configuration) {
+ const loadConfiguration = async () => {
+ const response = await fetch(`${window.location.origin}/configuration.json`);
+ const configurationResponse = await response.json();
+ setConfiguration(configurationResponse);
+ };
+ loadConfiguration();
+ }
+ }, [configuration]);
+
+ return { configuration };
+};
diff --git a/src/common-tools/hooks/listenQueen.js b/src/utils/hooks/listenQueen.js
similarity index 91%
rename from src/common-tools/hooks/listenQueen.js
rename to src/utils/hooks/listenQueen.js
index 4c71af0c1..3f3394159 100644
--- a/src/common-tools/hooks/listenQueen.js
+++ b/src/utils/hooks/listenQueen.js
@@ -1,6 +1,6 @@
-import questionnaireEnum from 'common-tools/enum/QuestionnaireStateEnum';
-import suStateEnum from 'common-tools/enum/SUStateEnum';
-import { addNewState } from 'common-tools/functions/surveyUnitFunctions';
+import questionnaireEnum from 'utils/enum/QuestionnaireStateEnum';
+import suStateEnum from 'utils/enum/SUStateEnum';
+import { addNewState } from 'utils/functions/surveyUnitFunctions';
import surveyUnitDBService from 'indexedbb/services/surveyUnit-idb-service';
import { useEffect } from 'react';
diff --git a/src/common-tools/hooks/useAsyncState.js b/src/utils/hooks/useAsyncState.js
similarity index 100%
rename from src/common-tools/hooks/useAsyncState.js
rename to src/utils/hooks/useAsyncState.js
diff --git a/src/common-tools/hooks/useCounter.js b/src/utils/hooks/useCounter.js
similarity index 100%
rename from src/common-tools/hooks/useCounter.js
rename to src/utils/hooks/useCounter.js
diff --git a/src/utils/hooks/useQueenFromConfig.js b/src/utils/hooks/useQueenFromConfig.js
new file mode 100644
index 000000000..99ef023b4
--- /dev/null
+++ b/src/utils/hooks/useQueenFromConfig.js
@@ -0,0 +1,14 @@
+import { useEffect } from 'react';
+
+export const useQueenFromConfig = configuration => {
+ const importQueenScript = configuration => {
+ const { QUEEN_URL } = configuration;
+ const script = document.createElement('script');
+ script.src = `${QUEEN_URL}/entry.js`;
+ document.body.appendChild(script);
+ };
+
+ useEffect(() => {
+ if (configuration) importQueenScript(configuration);
+ }, [configuration]);
+};
diff --git a/src/utils/hooks/useServiceWorker.js b/src/utils/hooks/useServiceWorker.js
new file mode 100644
index 000000000..d1a062f2f
--- /dev/null
+++ b/src/utils/hooks/useServiceWorker.js
@@ -0,0 +1,89 @@
+import { useState, useEffect, useContext } from 'react';
+import { AppContext } from 'Root';
+import * as serviceWorker from 'serviceWorkerRegistration';
+
+const SW_UPDATE_KEY = 'installing-update';
+
+export const useServiceWorker = authenticated => {
+ const { QUEEN_URL } = useContext(AppContext);
+ const [isInstallingServiceWorker, setIsInstallingServiceWorker] = useState(false);
+ const [waitingServiceWorker, setWaitingServiceWorker] = useState(null);
+ const [isUpdateAvailable, setUpdateAvailable] = useState(false);
+ const [isServiceWorkerInstalled, setServiceWorkerInstalled] = useState(false);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [isUpdateInstalled, setIsUpdateInstalled] = useState(
+ window.localStorage.getItem(SW_UPDATE_KEY)
+ );
+ const [isInstallationFailed, setIsInstallationFailed] = useState(false);
+
+ const uninstall = () => {
+ serviceWorker.unregister({
+ onUnregister: () => {},
+ });
+ };
+
+ useEffect(() => {
+ if (authenticated && QUEEN_URL) {
+ serviceWorker.register({
+ QUEEN_URL,
+ onInstalling: installing => {
+ setIsInstallingServiceWorker(installing);
+ },
+ onUpdate: registration => {
+ setWaitingServiceWorker(registration.waiting);
+ setUpdateAvailable(true);
+ },
+ onWaiting: waiting => {
+ setWaitingServiceWorker(waiting);
+ setUpdateAvailable(true);
+ },
+ onSuccess: registration => {
+ setIsInstallingServiceWorker(false);
+ setServiceWorkerInstalled(!!registration);
+ },
+ onError: () => {
+ setIsInstallationFailed(true);
+ },
+ });
+ }
+ }, [QUEEN_URL, authenticated]);
+
+ const updateAssets = () => {
+ if (waitingServiceWorker) {
+ waitingServiceWorker.postMessage({ type: 'SKIP_WAITING' });
+ }
+ };
+
+ const updateApp = () => {
+ window.localStorage.setItem(SW_UPDATE_KEY, true);
+ setIsUpdating(true);
+ updateAssets();
+ };
+
+ const clearUpdating = () => {
+ setIsUpdateInstalled(false);
+ window.localStorage.removeItem(SW_UPDATE_KEY);
+ };
+
+ useEffect(() => {
+ if (waitingServiceWorker) {
+ waitingServiceWorker.addEventListener('statechange', event => {
+ if (event.target.state === 'activated') {
+ window.location.reload();
+ }
+ });
+ }
+ }, [waitingServiceWorker]);
+
+ return {
+ isUpdating,
+ isUpdateInstalled,
+ isInstallingServiceWorker,
+ isUpdateAvailable,
+ isServiceWorkerInstalled,
+ isInstallationFailed,
+ updateApp,
+ clearUpdating,
+ uninstall,
+ };
+};
diff --git a/src/common-tools/hooks/useTimer.js b/src/utils/hooks/useTimer.js
similarity index 100%
rename from src/common-tools/hooks/useTimer.js
rename to src/utils/hooks/useTimer.js
diff --git a/src/common-tools/icons/SyncIcon.js b/src/utils/icons/SyncIcon.js
similarity index 100%
rename from src/common-tools/icons/SyncIcon.js
rename to src/utils/icons/SyncIcon.js
diff --git a/src/common-tools/icons/materialIcons.js b/src/utils/icons/materialIcons.js
similarity index 100%
rename from src/common-tools/icons/materialIcons.js
rename to src/utils/icons/materialIcons.js
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 000000000..6f17dfa94
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,6 @@
+import { fr, enUS } from 'date-fns/locale';
+import { getLang } from 'i18n/build-dictionary';
+
+export { default as addOnlineStatusObserver } from './online-status-observer';
+
+export const dateFnsLocal = getLang() === 'fr' ? fr : enUS;
diff --git a/src/common-tools/keycloak/index.js b/src/utils/keycloak/index.js
similarity index 100%
rename from src/common-tools/keycloak/index.js
rename to src/utils/keycloak/index.js
diff --git a/src/common-tools/keycloak/keycloak.js b/src/utils/keycloak/keycloak.js
similarity index 95%
rename from src/common-tools/keycloak/keycloak.js
rename to src/utils/keycloak/keycloak.js
index e31afe00d..5dafd3968 100644
--- a/src/common-tools/keycloak/keycloak.js
+++ b/src/utils/keycloak/keycloak.js
@@ -1,5 +1,5 @@
import Keycloak from 'keycloak-js';
-import { PEARL_URL } from 'common-tools/constants';
+import { PEARL_URL } from 'utils/constants';
export const kc = Keycloak(`${PEARL_URL}/keycloak.json`);
export const keycloakAuthentication = params =>
diff --git a/src/common-tools/online-status-observer.js b/src/utils/online-status-observer.js
similarity index 100%
rename from src/common-tools/online-status-observer.js
rename to src/utils/online-status-observer.js
diff --git a/src/utils/synchronize/check.js b/src/utils/synchronize/check.js
new file mode 100644
index 000000000..0620088d5
--- /dev/null
+++ b/src/utils/synchronize/check.js
@@ -0,0 +1,175 @@
+import surveyUnitMissingIdbService from 'indexedbb/services/surveyUnitMissing-idb-service';
+import surveyUnitIdbService from 'indexedbb/services/surveyUnit-idb-service';
+import notificationIdbService from 'indexedbb/services/notification-idb-service';
+import syncReportIdbService from 'indexedbb/services/syncReport-idb-service';
+import { NOTIFICATION_TYPE_SYNC, PEARL_USER_KEY } from 'utils/constants';
+import * as api from 'utils/api';
+import D from 'i18n';
+
+export const checkSyncResult = (pearlSuccess, queenSuccess) => {
+ if (pearlSuccess && queenSuccess) {
+ const queenMissing = pearlSuccess.reduce((_, pearlSU) => {
+ if (!queenSuccess.includes(pearlSU)) return [..._, pearlSU];
+ return _;
+ }, []);
+
+ const pearlMissing = queenSuccess.reduce((_, queenSU) => {
+ if (!pearlSuccess.includes(queenSU)) return [..._, queenSU];
+ return _;
+ }, []);
+
+ return { queenMissing, pearlMissing };
+ }
+ return {};
+};
+
+export const getNotifFromResult = (result, nowDate) => {
+ const { state, messages } = result;
+ return {
+ date: nowDate || new Date().getTime(),
+ type: NOTIFICATION_TYPE_SYNC,
+ title: D.titleSync(state),
+ messages,
+ state,
+ read: false,
+ detail: `report-${nowDate}`,
+ };
+};
+
+export const getReportFromResult = (result, nowDate = 0) => {
+ const { details, state } = result;
+ if (state !== 'error') {
+ const { transmittedSurveyUnits, loadedSurveyUnits } = details;
+ return {
+ id: `report-${nowDate}`,
+ transmittedSurveyUnits,
+ loadedSurveyUnits,
+ };
+ }
+ return { id: `report-${nowDate}` };
+};
+
+const getResult = (
+ pearlError,
+ queenError,
+ pearlMissing = [],
+ queenMissing = [],
+ pearlSurveyUnits = [],
+ pearlTempZone = [],
+ queenTempZone = [],
+ transmittedSurveyUnits = {},
+ loadedSurveyUnits = {}
+) => {
+ const messages = [];
+ if (pearlError || queenError) {
+ return {
+ state: 'error',
+ messages: [D.syncStopOnError, D.warningOrErrorEndMessage, D.syncPleaseTryAgain],
+ };
+ }
+ if (
+ pearlTempZone.length > 0 ||
+ queenTempZone.length > 0 ||
+ pearlMissing.length > 0 ||
+ queenMissing.length > 0
+ ) {
+ if (pearlTempZone.length > 0 || queenTempZone.length > 0) messages.push(D.syncTempZone);
+ if (queenMissing.length > 0) messages.push(D.syncQueenMissing);
+ if (pearlMissing.length > 0) {
+ if (pearlSurveyUnits.length === 0) messages.push(D.syncNoPearlData);
+ else messages.push(D.syncPearlMissing);
+ }
+
+ return {
+ state: 'warning',
+ messages: [...messages, D.warningOrErrorEndMessage, D.syncYouCanStillWork],
+ details: { transmittedSurveyUnits, loadedSurveyUnits },
+ };
+ }
+ return {
+ state: 'success',
+ messages: [D.syncSuccessMessage],
+ details: { transmittedSurveyUnits, loadedSurveyUnits },
+ };
+};
+
+export const analyseResult = async (PEARL_API_URL, PEARL_AUTHENTICATION_MODE) => {
+ const { id: userId } = JSON.parse(window.localStorage.getItem(PEARL_USER_KEY)) || {};
+ const pearlSus = await surveyUnitIdbService.getAll();
+ const pearlSurveyUnitsArray = pearlSus.map(({ id }) => id);
+ const {
+ error: pearlError,
+ surveyUnitsInTempZone: pearlTempZone,
+ transmittedSurveyUnits,
+ loadedSurveyUnits,
+ } = getSavedSyncPearlData() || {};
+ const {
+ error: queenError,
+ surveyUnitsSuccess: queenSurveyUnitsArray,
+ surveyUnitsInTempZone: queenTempZone,
+ } = getSavedSyncQueenData() || {};
+
+ if (pearlTempZone && pearlTempZone.length > 0) {
+ const mailSubjectToSend = D.subjectTempZone;
+
+ const mailBodyToSend = D.bodyTempZonePearl(userId)(pearlTempZone);
+ await api.sendMail(PEARL_API_URL, PEARL_AUTHENTICATION_MODE)(mailSubjectToSend, mailBodyToSend);
+ }
+ if (queenTempZone && queenTempZone.length > 0) {
+ const mailSubjectToSend = D.subjectTempZone;
+
+ const mailBodyToSend = D.bodyTempZoneQueen(userId)(queenTempZone);
+ await api.sendMail(PEARL_API_URL, PEARL_AUTHENTICATION_MODE)(mailSubjectToSend, mailBodyToSend);
+ }
+
+ const { pearlMissing, queenMissing } = checkSyncResult(
+ pearlSurveyUnitsArray,
+ queenSurveyUnitsArray
+ );
+
+ if (pearlMissing && pearlMissing.length > 0) {
+ const mailSubject = D.subjectPearlMissingUnits;
+ const mailContent = D.bodyPearlMissingUnits(userId)(pearlMissing);
+ // send Mail to assistance (too many units in queen database)
+ await api.sendMail(PEARL_API_URL, PEARL_AUTHENTICATION_MODE)(mailSubject, mailContent);
+ }
+ if (queenMissing && queenMissing.length > 0) {
+ const mailSubject = D.subjectQueenMissingUnits;
+ const mailContent = D.bodyQueenMissingUnits(userId)(queenMissing);
+ // send Mail to assistance (not enough units in queen database)
+ await api.sendMail(PEARL_API_URL, PEARL_AUTHENTICATION_MODE)(mailSubject, mailContent);
+ // save missing (exist in Pearl, not in Queen) units in database
+ await surveyUnitMissingIdbService.addAll(
+ queenMissing.map(id => {
+ return { id };
+ })
+ );
+ }
+
+ const result = getResult(
+ pearlError,
+ queenError,
+ pearlMissing,
+ queenMissing,
+ pearlSurveyUnitsArray,
+ pearlTempZone,
+ queenTempZone,
+ transmittedSurveyUnits,
+ loadedSurveyUnits
+ );
+
+ const nowDate = new Date().getTime();
+ const notification = getNotifFromResult(result, nowDate);
+ await notificationIdbService.addOrUpdateNotif(notification);
+ const report = getReportFromResult(result, nowDate);
+ await syncReportIdbService.addOrUpdateReport(report);
+
+ return result;
+};
+
+export const saveSyncPearlData = data =>
+ window.localStorage.setItem('PEARL_SYNC_RESULT', JSON.stringify(data));
+export const getSavedSyncPearlData = () =>
+ JSON.parse(window.localStorage.getItem('PEARL_SYNC_RESULT') || '{}');
+export const getSavedSyncQueenData = () =>
+ JSON.parse(window.localStorage.getItem('QUEEN_SYNC_RESULT') || '{}');
diff --git a/src/utils/synchronize/index.js b/src/utils/synchronize/index.js
new file mode 100644
index 000000000..7bc11b4e5
--- /dev/null
+++ b/src/utils/synchronize/index.js
@@ -0,0 +1,203 @@
+import * as api from 'utils/api';
+import { getLastState } from 'utils/functions';
+import surveyUnitDBService from 'indexedbb/services/surveyUnit-idb-service';
+import surveyUnitMissingIdbService from 'indexedbb/services/surveyUnitMissing-idb-service';
+import { useCallback, useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import surveyUnitState from 'utils/enum/SUStateEnum';
+
+export const useQueenSynchronisation = () => {
+ const waitTime = 5000;
+
+ const [queenError, setQueenError] = useState(false);
+ const [queenReady, setQueenReady] = useState(null);
+ const history = useHistory();
+
+ const checkQueen = () => {
+ setQueenReady(null);
+ const tooLateErrorThrower = setTimeout(() => {
+ setQueenError(true);
+ setQueenReady(true);
+ }, waitTime);
+
+ const handleQueenEvent = async event => {
+ const { type, command, state } = event.detail;
+ if (type === 'QUEEN' && command === 'HEALTH_CHECK') {
+ clearTimeout(tooLateErrorThrower);
+ if (state === 'READY') {
+ setQueenError(false);
+ setQueenReady(true);
+ console.log('Queen is ready');
+ } else {
+ setQueenError(true);
+ setQueenReady(true);
+ console.log('Queen is not ready');
+ }
+ }
+ };
+ const removeQueenEventListener = () => {
+ window.removeEventListener('QUEEN', handleQueenEvent);
+ };
+
+ window.addEventListener('QUEEN', handleQueenEvent);
+
+ const data = { type: 'PEARL', command: 'HEALTH_CHECK' };
+ const event = new CustomEvent('PEARL', { detail: data });
+ window.dispatchEvent(event);
+ setTimeout(() => removeQueenEventListener(), waitTime);
+ };
+
+ const synchronizeQueen = useCallback(() => {
+ history.push(`/queen/synchronize`);
+ }, [history]);
+
+ return { checkQueen, synchronizeQueen, queenReady, queenError };
+};
+
+const sendData = async (urlPearlApi, authenticationMode) => {
+ const surveyUnitsInTempZone = [];
+ const surveyUnits = await surveyUnitDBService.getAll();
+ await Promise.all(
+ surveyUnits.map(async surveyUnit => {
+ const lastState = getLastState(surveyUnit);
+ const { id } = surveyUnit;
+ const body = {
+ ...surveyUnit,
+ lastState,
+ };
+ const { error, status } = await api.putDataSurveyUnitById(urlPearlApi, authenticationMode)(
+ id,
+ body
+ );
+ if (error && [400, 403, 404, 500].includes(status)) {
+ const { error: tempZoneError } = await api.putSurveyUnitToTempZone(
+ urlPearlApi,
+ authenticationMode
+ )(id, body);
+ if (!tempZoneError) surveyUnitsInTempZone.push(id);
+ else throw new Error('Server is not responding');
+ } else if (error && ![400, 403, 404, 500].includes(status))
+ throw new Error('Server is not responding');
+ })
+ );
+ return surveyUnitsInTempZone;
+};
+
+const putSurveyUnitInDataBase = async su => {
+ await surveyUnitDBService.addOrUpdate(su);
+};
+
+const clean = async () => {
+ await surveyUnitDBService.deleteAll();
+ await surveyUnitMissingIdbService.deleteAll();
+};
+
+const validateSU = su => {
+ const { states, comments } = su;
+ if (Array.isArray(states) && states.length === 0) {
+ su.states.push(su.lastState);
+ }
+ if (Array.isArray(comments) && comments.length === 0) {
+ const interviewerComment = { type: 'INTERVIEWER', value: '' };
+ const managementComment = { type: 'MANAGEMENT', value: '' };
+ su.comments.push(interviewerComment);
+ su.comments.push(managementComment);
+ }
+
+ return su;
+};
+
+const getData = async (pearlApiUrl, pearlAuthenticationMode) => {
+ const surveyUnitsSuccess = [];
+ const surveyUnitsFailed = [];
+ const { data: surveyUnits, error, status } = await api.getSurveyUnits(
+ pearlApiUrl,
+ pearlAuthenticationMode
+ );
+
+ if (!error) {
+ await Promise.all(
+ surveyUnits.map(async su => {
+ const { data: surveyUnit, error: getSuError } = await api.getSurveyUnitById(
+ pearlApiUrl,
+ pearlAuthenticationMode
+ )(su.id);
+ if (!getSuError) {
+ const mergedSurveyUnit = { ...surveyUnit, ...su };
+ const validSurveyUnit = validateSU(mergedSurveyUnit);
+ await putSurveyUnitInDataBase(validSurveyUnit);
+ surveyUnitsSuccess.push({ id: mergedSurveyUnit.id, campaign: mergedSurveyUnit.campaign });
+ } else if ([400, 403, 404, 500].includes(status)) surveyUnitsFailed.push(su.id);
+ else throw new Error('Server is not responding');
+ })
+ );
+ } else if (![400, 403, 404, 500].includes(status)) throw new Error('Server is not responding');
+
+ return { surveyUnitsSuccess, surveyUnitsFailed };
+};
+
+const getWFSSurveyUnitsSortByCampaign = async () => {
+ const allSurveyUnits = await surveyUnitDBService.getAll();
+ return allSurveyUnits.reduce((wfs, su) => {
+ const { campaign, id } = su;
+ const lastState = getLastState(su);
+ if (lastState?.type === surveyUnitState.WAITING_FOR_SYNCHRONIZATION.type)
+ return { ...wfs, [campaign]: [...(wfs[campaign] || []), id] };
+ return wfs;
+ }, {});
+};
+
+const getAllSurveyUnitsByCampaign = async () => {
+ const allSurveyUnits = await surveyUnitDBService.getAll();
+ return allSurveyUnits.reduce((wfs, su) => {
+ const { campaign, id } = su;
+ return { ...wfs, [campaign]: [...(wfs[campaign] || []), id] };
+ }, {});
+};
+
+const getNewSurveyUnitsByCampaign = async (newSurveyUnits = [], oldSurveyUnits = {}) => {
+ const newSurveyUnitsByCampaign = newSurveyUnits.reduce((_, su) => {
+ const { campaign, id } = su;
+ return { ..._, [campaign]: [...(_[campaign] || []), id] };
+ }, {});
+
+ return Object.entries(newSurveyUnitsByCampaign).reduce((_, [campaign, surveyUnitIds]) => {
+ const newIds = surveyUnitIds.reduce((_, id) => {
+ const oldSus = oldSurveyUnits[campaign] || [];
+ if (!oldSus.includes(id)) return [..._, id];
+ return _;
+ }, []);
+ return { ..._, [campaign]: newIds };
+ }, {});
+};
+
+export const synchronizePearl = async (PEARL_API_URL, PEARL_AUTHENTICATION_MODE) => {
+ var transmittedSurveyUnits = {};
+ var loadedSurveyUnits = {};
+
+ var surveyUnitsInTempZone;
+ var surveyUnitsSuccess;
+ const allOldSurveyUnitsByCampaign = await getAllSurveyUnitsByCampaign();
+ try {
+ surveyUnitsInTempZone = await sendData(PEARL_API_URL, PEARL_AUTHENTICATION_MODE);
+ transmittedSurveyUnits = await getWFSSurveyUnitsSortByCampaign();
+
+ await clean();
+
+ const { surveyUnitsSuccess: susSuccess } = await getData(
+ PEARL_API_URL,
+ PEARL_AUTHENTICATION_MODE
+ );
+ surveyUnitsSuccess = susSuccess.map(({ id }) => id);
+ loadedSurveyUnits = await getNewSurveyUnitsByCampaign(susSuccess, allOldSurveyUnitsByCampaign);
+ return {
+ error: false,
+ surveyUnitsSuccess,
+ surveyUnitsInTempZone,
+ transmittedSurveyUnits,
+ loadedSurveyUnits,
+ };
+ } catch (e) {
+ return { error: true, surveyUnitsSuccess, surveyUnitsInTempZone };
+ }
+};
diff --git a/src/common-tools/token.js b/src/utils/token.js
similarity index 100%
rename from src/common-tools/token.js
rename to src/utils/token.js
diff --git a/yarn.lock b/yarn.lock
index afbc6ff8c..1bb6a2255 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1619,7 +1619,7 @@
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
-"@material-ui/pickers@^3.2.10":
+"@material-ui/pickers@^3.3.10":
version "3.3.10"
resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.3.10.tgz#f1b0f963348cc191645ef0bdeff7a67c6aa25485"
integrity sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w==
@@ -2919,13 +2919,6 @@ axe-core@^4.0.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.3.tgz#64a4c85509e0991f5168340edc4bedd1ceea6966"
integrity sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==
-axios@^0.19.2:
- version "0.19.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
- integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
- dependencies:
- follow-redirects "1.5.10"
-
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -4401,13 +4394,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@=3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
- integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
- dependencies:
- ms "2.0.0"
-
debug@^3.1.1:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -5732,13 +5718,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
-follow-redirects@1.5.10:
- version "1.5.10"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
- integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
- dependencies:
- debug "=3.1.0"
-
follow-redirects@^1.0.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"