diff --git a/package.json b/package.json index 65c1e5cd1..700a8b271 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,16 @@ { "name": "pearl", - "version": "0.4.0", + "version": "0.5.0", "private": true, "dependencies": { - "@testing-library/jest-dom": "^5.11.9", - "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^11.2.5", "@date-io/date-fns": "1.x", "@material-ui/core": "^4.11.2", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.57", - "@material-ui/pickers": "^3.2.10", - "axios": "^0.19.2", + "@material-ui/pickers": "^3.3.10", + "@testing-library/jest-dom": "^5.11.9", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^11.2.5", "clsx": "^1.1.1", "date-fns": "^2.18.0", "dexie": "^2.0.4", @@ -95,7 +94,6 @@ ] }, "devDependencies": { - "@types/react": "^16.9.23", "@types/react-router-dom": "^5.1.3", "copy-and-watch": "^0.1.4", @@ -107,8 +105,8 @@ "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-react": "^7.16.0", - "jest-sonar-reporter": "^2.0.0", "eslint-plugin-react-hooks": "^4.2.0", + "jest-sonar-reporter": "^2.0.0", "prettier": "^1.19.1" } } diff --git a/src/App.js b/src/App.js index 939a80d64..0afad7658 100644 --- a/src/App.js +++ b/src/App.js @@ -1,32 +1,42 @@ -import { useAuth } from 'common-tools/auth/initAuth'; -import useServiceWorker from 'common-tools/hooks/useServiceWorker'; +import { useAuth } from 'utils/auth/initAuth'; +import { useServiceWorker } from 'utils/hooks/useServiceWorker'; import Preloader from 'components/common/loader'; import Notification from 'components/common/Notification'; +import { ThemeProvider, CssBaseline } from '@material-ui/core'; +import theme from './theme'; import Palette from 'components/common/palette'; import Home from 'components/panel-body/home'; -import TrainingPage from 'components/panel-body/training'; import D from 'i18n'; import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, useLocation } from 'react-router-dom'; +import SynchronizeWrapper from 'components/sychronizeWrapper'; +import { NotificationWrapper } from 'components/notificationWrapper'; +import { ResetData } from 'components/panel-body/resetData'; function App() { + const { pathname } = useLocation(); const { authenticated } = useAuth(); const serviceWorkerInfo = useServiceWorker(authenticated); return ( - <> + +
{!authenticated && } {authenticated && ( - <> - } /> - - - + + + {!pathname.startsWith('/support') && ( + } /> + )} + + + + )}
- +
); } diff --git a/src/AppRooter.js b/src/AppRooter.js new file mode 100644 index 000000000..dffd19077 --- /dev/null +++ b/src/AppRooter.js @@ -0,0 +1,17 @@ +import App from 'App'; +import QueenContainer from 'components/panel-body/queen-container'; +import React from 'react'; +import { Route, Switch, useLocation } from 'react-router-dom'; + +function AppRooter() { + const { pathname } = useLocation(); + + return ( + + } /> + {!pathname.startsWith('/queen') && } + + ); +} + +export default AppRooter; diff --git a/src/Root.js b/src/Root.js index 3f9fa01f5..f0c1b7f75 100644 --- a/src/Root.js +++ b/src/Root.js @@ -1,23 +1,36 @@ -import { CssBaseline, ThemeProvider } from '@material-ui/core'; -import App from 'App'; -import { useQueenFromConfig } from 'common-tools/hooks/useQueenFromConfig'; -import QueenContainer from 'components/panel-body/queen-container'; -import React from 'react'; -import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; -import theme from './theme'; +import { useQueenFromConfig } from 'utils/hooks/useQueenFromConfig'; +import React, { useEffect, useState } from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { useConfiguration } from 'utils/hooks/configuration'; +import { addOnlineStatusObserver } from 'utils'; +import AppRouter from 'AppRooter'; + +export const AppContext = React.createContext(); function Root() { - useQueenFromConfig(`${window.location.origin}/configuration.json`); + const { configuration } = useConfiguration(); + useQueenFromConfig(configuration); + + const [online, setOnline] = useState(navigator.onLine); + + useEffect(() => { + addOnlineStatusObserver(s => { + setOnline(s); + }); + }, []); + + const context = { ...configuration, online }; + return ( - - - } /> - - - - - - + <> + {configuration && ( + + + + + + )} + ); } diff --git a/src/common-tools/api/surveyUnitAPI.js b/src/common-tools/api/surveyUnitAPI.js deleted file mode 100644 index 5fc1497d1..000000000 --- a/src/common-tools/api/surveyUnitAPI.js +++ /dev/null @@ -1,45 +0,0 @@ -import Axios from 'axios'; -import { authentication, getHeader } from './utils'; - -export const getSurveyUnits = (urlPearApi, authenticationMode) => - new Promise((resolve, reject) => { - authentication(authenticationMode) - .then(() => { - Axios.get(`${urlPearApi}/api/survey-units`, { - headers: getHeader(authenticationMode), - }) - .then(res => resolve(res)) - .catch(e => { - reject(new Error(`Failed to fetch survey-units : ${e.response.data.error.message}`)); - }); - }) - .catch(e => { - reject(new Error(`Error during refreshToken : ${e.response.data.error.message}`)); - }); - }); - -export const getSurveyUnitById = (urlPearApi, authenticationMode) => id => - new Promise((resolve, reject) => { - authentication(authenticationMode) - .then(() => { - Axios.get(`${urlPearApi}/api/survey-unit/${id}`, { - headers: getHeader(authenticationMode), - }) - .then(res => resolve(res)) - .catch(e => reject(new Error(`Failed to fetch survey-unit (id:${id}) : ${e.message}`))); - }) - .catch(e => reject(new Error(`Error during refreshToken : ${e.message}`))); - }); - -export const putDataSurveyUnitById = (urlPearApi, authenticationMode) => (id, su) => - new Promise((resolve, reject) => { - authentication(authenticationMode) - .then(() => { - Axios.put(`${urlPearApi}/api/survey-unit/${id}`, su, { - headers: getHeader(authenticationMode), - }) - .then(res => resolve(res)) - .catch(e => reject(new Error(`Failed to put survey-unit (id:${id}) : ${e.message}`))); - }) - .catch(e => reject(new Error(`Error during refreshToken : ${e.message}`))); - }); diff --git a/src/common-tools/auth/initAuth.js b/src/common-tools/auth/initAuth.js deleted file mode 100644 index 0d50ca51d..000000000 --- a/src/common-tools/auth/initAuth.js +++ /dev/null @@ -1,74 +0,0 @@ -import { GUEST_PEARL_USER, PEARL_USER_KEY } from 'common-tools/constants'; -import { getTokenInfo, keycloakAuthentication } from 'common-tools/keycloak'; -import { useEffect, useState } from 'react'; - -export const useAuth = () => { - const [authenticated, setAuthenticated] = useState(false); - - 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 configURL = `${window.location.origin}/configuration.json`; - fetch(configURL) - .then(response => response.json()) - .then(data => { - switch (data.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/hooks/useQueenFromConfig.js b/src/common-tools/hooks/useQueenFromConfig.js deleted file mode 100644 index 017fab9bf..000000000 --- a/src/common-tools/hooks/useQueenFromConfig.js +++ /dev/null @@ -1,15 +0,0 @@ -import { useEffect } from 'react'; - -export const useQueenFromConfig = url => { - const importQueenScript = async configurationUrl => { - const response = await fetch(configurationUrl); - const { QUEEN_URL } = await response.json(); - const script = document.createElement('script'); - script.src = `${QUEEN_URL}/entry.js`; - document.body.appendChild(script); - }; - - useEffect(() => { - importQueenScript(url); - }, [url]); -}; diff --git a/src/common-tools/hooks/useServiceWorker.js b/src/common-tools/hooks/useServiceWorker.js deleted file mode 100644 index 9705f2c18..000000000 --- a/src/common-tools/hooks/useServiceWorker.js +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useState } from 'react'; -import * as serviceWorker from 'serviceWorkerRegistration'; - -const useServiceWorker = authenticated => { - const [installingServiceWorker, setInstallingServiceWorker] = useState(false); - const [waitingServiceWorker, setWaitingServiceWorker] = useState(null); - const [isUpdateAvailable, setUpdateAvailable] = useState(false); - const [isServiceWorkerInstalled, setServiceWorkerInstalled] = useState(false); - - useEffect(() => { - const install = async () => { - const configuration = await fetch(`${window.location.origin}/configuration.json`); - const { QUEEN_URL } = await configuration.json(); - serviceWorker.register({ - QUEEN_URL, - onInstalling: installing => { - setInstallingServiceWorker(installing); - }, - onUpdate: registration => { - setWaitingServiceWorker(registration.waiting); - setUpdateAvailable(true); - }, - onWaiting: waiting => { - setWaitingServiceWorker(waiting); - setUpdateAvailable(true); - }, - onSuccess: registration => { - setInstallingServiceWorker(false); - setServiceWorkerInstalled(!!registration); - }, - }); - }; - if (authenticated) install(); - }, [authenticated]); - - return { - installingServiceWorker, - waitingServiceWorker, - isUpdateAvailable, - isServiceWorkerInstalled, - }; -}; -export default useServiceWorker; diff --git a/src/common-tools/index.js b/src/common-tools/index.js deleted file mode 100644 index fbaef09fb..000000000 --- a/src/common-tools/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as addOnlineStatusObserver } from './online-status-observer'; diff --git a/src/common-tools/synchronize/index.js b/src/common-tools/synchronize/index.js deleted file mode 100644 index 88586d57a..000000000 --- a/src/common-tools/synchronize/index.js +++ /dev/null @@ -1,105 +0,0 @@ -import * as api from 'common-tools/api'; -import { getLastState } from 'common-tools/functions'; -import surveyUnitDBService from 'indexedbb/services/surveyUnit-idb-service'; - -export const synchronizeQueen = async history => { - // 5 seconds limit before throwing error - const tooLateErrorThrower = setTimeout(() => { - throw new Error('Queen service worker not responding'); - }, 5000); - - const handleQueenEvent = async event => { - const { type, command, state } = event.detail; - if (type === 'QUEEN' && command === 'HEALTH_CHECK') { - if (state === 'READY') { - clearTimeout(tooLateErrorThrower); - history.push(`/queen/synchronize`); - } - } - }; - 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(), 2000); -}; - -const getConfiguration = async () => { - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - const response = await fetch(`${publicUrl.origin}/configuration.json`); - const configuration = await response.json(); - return configuration; -}; - -const sendData = async (urlPearlApi, authenticationMode) => { - console.log('SEND DATA'); - const surveyUnits = await surveyUnitDBService.getAll(); - await Promise.all( - surveyUnits.map(async surveyUnit => { - const lastState = getLastState(surveyUnit); - const { id } = surveyUnit; - await api.putDataSurveyUnitById(urlPearlApi, authenticationMode)(id, { - ...surveyUnit, - lastState, - }); - }) - ); -}; - -const putSurveyUnitInDataBase = async su => { - await surveyUnitDBService.addOrUpdate(su); -}; - -const clean = async () => { - console.log('CLEAN DATA'); - await surveyUnitDBService.deleteAll(); -}; - -const validateSU = async 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) => { - console.log('GET DATA'); - const surveyUnitsResponse = await api.getSurveyUnits(pearlApiUrl, pearlAuthenticationMode); - const surveyUnits = await surveyUnitsResponse.data; - await Promise.all( - surveyUnits.map(async su => { - const surveyUnitResponse = await api.getSurveyUnitById( - pearlApiUrl, - pearlAuthenticationMode - )(su.id); - const surveyUnit = await surveyUnitResponse.data; - const mergedSurveyUnit = { ...surveyUnit, ...su }; - const validSurveyUnit = await validateSU(mergedSurveyUnit); - await putSurveyUnitInDataBase(validSurveyUnit); - }) - ); -}; - -export const synchronizePearl = async () => { - const { PEARL_API_URL, PEARL_AUTHENTICATION_MODE } = await getConfiguration(); - - await sendData(PEARL_API_URL, PEARL_AUTHENTICATION_MODE); - - await clean(); - - await getData(PEARL_API_URL, PEARL_AUTHENTICATION_MODE); -}; diff --git a/src/components/common/IconStatus.js b/src/components/common/IconStatus.js new file mode 100644 index 000000000..1e20a1cbd --- /dev/null +++ b/src/components/common/IconStatus.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { CheckCircleOutline, Warning, Clear } from '@material-ui/icons'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + success: { + color: theme.palette.success.main, + }, + failure: { + color: theme.palette.error.main, + }, + warning: { + color: theme.palette.warning.main, + }, +})); + +export const IconStatus = ({ type, ...other }) => { + const classes = useStyles(); + if (type === 'success') + return ; + if (type === 'error') return ; + if (type === 'warning') + return ; + return <>; +}; diff --git a/src/components/common/Notification/index.js b/src/components/common/Notification/index.js index 8440fb003..be7ce2e6c 100644 --- a/src/components/common/Notification/index.js +++ b/src/components/common/Notification/index.js @@ -1,89 +1,102 @@ -import { Button, CircularProgress, makeStyles, Slide, Snackbar } from '@material-ui/core'; -import useTimer from 'common-tools/hooks/useTimer'; +import { Box, Button, makeStyles, Slide, Snackbar } from '@material-ui/core'; import D from 'i18n'; -import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; +import { Alert } from '@material-ui/lab'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ root: { - top: '0', + position: 'absolute', + top: 0, + minWidth: '80%', }, - content: { - backgroundColor: 'rgba(46, 139, 166, 0.9)', + buttonParrent: { + marginTop: theme.spacing(1), + display: 'flex', + justifyContent: 'center', }, })); const SlideTransition = props => ; const Notification = ({ serviceWorkerInfo }) => { - const [open, setOpen] = useState(true); - const [progress, setProgress] = useTimer(setOpen); + const classes = useStyles(); + const [message, setMessage] = useState(null); const { - installingServiceWorker, - waitingServiceWorker, + isUpdating, + isUpdateInstalled, + isInstallingServiceWorker, isUpdateAvailable, isServiceWorkerInstalled, + isInstallationFailed, + updateApp, + clearUpdating, } = serviceWorkerInfo; + const [open, setOpen] = useState(false); + const [severityType, setSeverityType] = useState('info'); - const updateAssets = () => { - if (waitingServiceWorker) { - waitingServiceWorker.postMessage({ type: 'SKIP_WAITING' }); - } - }; + useEffect(() => { + if (message) setOpen(true); + }, [message]); useEffect(() => { - if (waitingServiceWorker) { - waitingServiceWorker.addEventListener('statechange', event => { - if (event.target.state === 'activated') { - window.location.reload(); - } - }); - } - }, [waitingServiceWorker]); + if (isUpdating || isInstallingServiceWorker || isUpdateAvailable) setSeverityType('info'); + if (isInstallationFailed) setSeverityType('error'); + if (isUpdateInstalled || isServiceWorkerInstalled) setSeverityType('success'); + }, [ + isUpdating, + isInstallingServiceWorker, + isUpdateAvailable, + isInstallationFailed, + isUpdateInstalled, + isServiceWorkerInstalled, + ]); - const getMessage = () => { - if (isUpdateAvailable) return D.updateAvailable; - if (isServiceWorkerInstalled) return D.appReadyOffline; - if (installingServiceWorker) return D.appInstalling; - return ''; - }; + useEffect(() => { + if (isUpdating) setMessage(D.updating); + else if (isUpdateInstalled) setMessage(D.updateInstalled); + else if (isUpdateAvailable) setMessage(D.updateAvailable); + else if (isServiceWorkerInstalled) setMessage(D.appReadyOffline); + else if (isInstallingServiceWorker) setMessage(D.appInstalling); + else if (isInstallationFailed) setMessage(D.installError); + else setMessage(null); + }, [ + isUpdating, + isInstallingServiceWorker, + isUpdateAvailable, + isInstallationFailed, + isUpdateInstalled, + isServiceWorkerInstalled, + ]); - const classes = useStyles(); + const close = () => { + setOpen(false); + if (isUpdateInstalled) setTimeout(() => clearUpdating(), 1000); + }; return ( setOpen(false)} - onEntering={() => setProgress(1)} - action={ - // eslint-disable-next-line react/jsx-wrap-multilines - - } - /> + > + + {message} + {isUpdateAvailable && !isUpdating && ( + + + + )} + + ); }; export default Notification; -Notification.propTypes = { - serviceWorkerInfo: PropTypes.shape({ - installingServiceWorker: PropTypes.shape({}).isRequired, - waitingServiceWorker: PropTypes.shape({ - postMessage: PropTypes.shape({}), - addEventListener: PropTypes.shape({}), - }).isRequired, - isUpdateAvailable: PropTypes.shape({}).isRequired, - isServiceWorkerInstalled: PropTypes.shape({}).isRequired, - }).isRequired, -}; diff --git a/src/components/common/Notification/notificationItem.js b/src/components/common/Notification/notificationItem.js new file mode 100644 index 000000000..bb25f9e8b --- /dev/null +++ b/src/components/common/Notification/notificationItem.js @@ -0,0 +1,73 @@ +import { Link, makeStyles, Typography } from '@material-ui/core'; +import { FiberManualRecord } from '@material-ui/icons'; +import { SynchronizeWrapperContext } from 'components/sychronizeWrapper'; +import { formatDistance } from 'date-fns'; +import React, { useContext } from 'react'; +import { dateFnsLocal } from 'utils'; +import { NOTIFICATION_TYPE_MANAGEMENT, NOTIFICATION_TYPE_SYNC } from 'utils/constants'; +import { NavigationContext } from '../navigation/component'; +import D from 'i18n'; +import { NotificationWrapperContext } from 'components/notificationWrapper'; +import syncReportIdbService from 'indexedbb/services/syncReport-idb-service'; + +const useStyles = makeStyles(theme => ({ + root: { padding: '1em' }, + titleWrapper: { + display: 'flex', + }, + title: { + color: 'black', + fontSize: 'larger', + textAlign: 'left', + }, + details: { + fontSize: '0.8em', + textTransform: 'uppercase', + color: 'grey', + }, + readIcon: { marginLeft: 'auto', color: '#0f417a' }, +})); + +export const NotificationItem = ({ data }) => { + const { markNotifAsRead } = useContext(NotificationWrapperContext); + const { setOpen } = useContext(NavigationContext); + const { setSyncResult } = useContext(SynchronizeWrapperContext); + + const { id, date, title, type, messages, read, state, detail } = data; + + const typeOfNotif = { + [NOTIFICATION_TYPE_SYNC]: D.simpleSync, + [NOTIFICATION_TYPE_MANAGEMENT]: D.notifManagement, + }; + const finalType = typeOfNotif[type]; + + const finalDate = `${formatDistance(date || 0, new Date(), { + addSuffix: true, + locale: dateFnsLocal, + })}`; + + const classes = useStyles(); + + const markAsRead = async () => { + if (!read) { + markNotifAsRead(id); + } + if (type === NOTIFICATION_TYPE_SYNC) { + const report = (await syncReportIdbService.getById(detail)) || {}; + setOpen(false); + setSyncResult({ date: finalDate, state, messages, details: report }); + } + }; + + return ( +
+
+ + {title} + + {!read && } +
+ {`${finalDate} - ${finalType}`} +
+ ); +}; diff --git a/src/components/common/Notification/notificationsRoot.js b/src/components/common/Notification/notificationsRoot.js new file mode 100644 index 000000000..765c7468b --- /dev/null +++ b/src/components/common/Notification/notificationsRoot.js @@ -0,0 +1,110 @@ +import React, { useContext } from 'react'; +import D from 'i18n'; +import { + Divider, + FormControl, + FormHelperText, + IconButton, + makeStyles, + NativeSelect, + Paper, + Tooltip, + Typography, +} from '@material-ui/core'; +import { NotificationItem } from './notificationItem'; +import { NotificationWrapperContext } from 'components/notificationWrapper'; +import { Delete, Drafts } from '@material-ui/icons'; +import { NOTIFICATION_TYPE_MANAGEMENT, NOTIFICATION_TYPE_SYNC } from 'utils/constants'; + +const useStyles = makeStyles(theme => ({ + paperNotif: { + borderRadius: '10px', + minWidth: '400px', + boxShadow: '3px 0 0.8em grey, -3px 0 0.8em grey', + }, + paperNotifTitleWrapper: { display: 'flex', paddingTop: '10px' }, + iconButtons: { marginLeft: 'auto' }, + paperNotifTitle: { + padding: '1em', + fontWeight: 'bold', + }, + noNotif: { padding: '1em' }, + paperNotifContent: { + padding: 0, + maxHeight: '400px', + maxWidth: '400px', + overflowY: 'auto', + }, +})); + +export const NotificationsRoot = () => { + const { + notifications, + unReadNotificationsNumberFilterd, + deleteAll, + markAllAsRead, + filterType, + setFilterType, + } = useContext(NotificationWrapperContext); + + const changeFilter = e => { + setFilterType(e.target.value); + }; + + const classes = useStyles(); + + return ( + +
+ {D.notifications} +
+ + + + + + + {D.notificationsType} + +
+ +
+ {unReadNotificationsNumberFilterd > 0 && ( + + + + + + )} + {notifications.length > 0 && ( + + + + + + )} +
+
+ + +
+ {notifications.length > 0 && + notifications.map(notif => ( + + + + + ))} + {notifications.length === 0 && ( + {D.noNotification} + )} +
+
+ ); +}; diff --git a/src/components/common/loader/index.js b/src/components/common/loader/index.js index 2ad56b86b..575f2a1bf 100644 --- a/src/components/common/loader/index.js +++ b/src/components/common/loader/index.js @@ -11,14 +11,13 @@ const useStyles = makeStyles(theme => ({ display: 'flex', flexDirection: 'column', }, - img: { marginTop: '10%' }, })); const Preloader = ({ message }) => { const classes = useStyles(); return ( - waiting... + waiting...

{D.pleaseWait}

{message}

diff --git a/src/components/common/navigation/component.js b/src/components/common/navigation/component.js index d6934a2c5..092fbf6a0 100644 --- a/src/components/common/navigation/component.js +++ b/src/components/common/navigation/component.js @@ -1,20 +1,35 @@ -import { Badge, Card, CardMedia, IconButton, Tooltip } from '@material-ui/core'; +import { + Badge, + Card, + CardMedia, + ClickAwayListener, + Fade, + IconButton, + Popper, + Tooltip, +} from '@material-ui/core'; import AppBar from '@material-ui/core/AppBar'; import { makeStyles } from '@material-ui/core/styles'; import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; import MenuIcon from '@material-ui/icons/Menu'; -import { PEARL_USER_KEY } from 'common-tools/constants'; +import { PEARL_USER_KEY } from 'utils/constants'; import Synchronize from 'components/common/synchronize'; import InfoTile from 'components/panel-body/UEpage/infoTile'; import D from 'i18n'; import PropTypes from 'prop-types'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { NavLink, Route } from 'react-router-dom'; import OnlineStatus from '../online-status'; import SearchBar from '../search/component'; +import { Notifications } from '@material-ui/icons'; +import { NotificationsRoot } from '../Notification/notificationsRoot'; +import { NotificationWrapperContext } from 'components/notificationWrapper'; + +export const NavigationContext = React.createContext(); const Navigation = ({ location, textSearch, setTextSearch, setOpenDrawer, refresh }) => { + const { unReadNotificationsNumber } = useContext(NotificationWrapperContext); const [disabled, setDisable] = useState(location.pathname.startsWith('/queen')); useEffect(() => { @@ -65,12 +80,28 @@ const Navigation = ({ location, textSearch, setTextSearch, setOpenDrawer, refres backgroundColor: theme.palette.primary.main, }, }, + notif: { + zIndex: 1200, + }, })); const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + const [open, setOpen] = React.useState(false); + + const handleClickAway = () => { + setOpen(false); + }; + const handleClick = event => { + setAnchorEl(event.currentTarget); + setOpen(o => !o); + }; + + const context = { setOpen }; + return ( - <> + @@ -79,13 +110,9 @@ const Navigation = ({ location, textSearch, setTextSearch, setOpenDrawer, refres edge="start" color="inherit" aria-label="open notifications" + onClick={() => setOpenDrawer(true)} > - - setOpenDrawer(true)} - /> - + @@ -110,6 +137,32 @@ const Navigation = ({ location, textSearch, setTextSearch, setOpenDrawer, refres render={routeProps => } /> +
+ +
+ + + + + + + + + {({ TransitionProps }) => ( + + + + )} + +
+
+
@@ -119,7 +172,7 @@ const Navigation = ({ location, textSearch, setTextSearch, setOpenDrawer, refres - + ); }; export default Navigation; diff --git a/src/components/common/online-status/index.js b/src/components/common/online-status/index.js index e76cbf8e4..818dd7ba9 100644 --- a/src/components/common/online-status/index.js +++ b/src/components/common/online-status/index.js @@ -1,8 +1,8 @@ import { makeStyles } from '@material-ui/core/styles'; import WifiIcon from '@material-ui/icons/Wifi'; import clsx from 'clsx'; -import { addOnlineStatusObserver } from 'common-tools'; -import React, { useEffect, useState } from 'react'; +import React, { useContext } from 'react'; +import { AppContext } from 'Root'; const useStyles = makeStyles(theme => ({ red: { @@ -20,20 +20,11 @@ const useStyles = makeStyles(theme => ({ })); const OnlineStatus = () => { - const [init, setInit] = useState(false); - const [status, setStatus] = useState(navigator.onLine); - useEffect(() => { - if (!init) { - addOnlineStatusObserver(s => { - setStatus(s); - }); - setInit(true); - } - }, [init]); + const { online } = useContext(AppContext); const { icon, green, red } = useStyles(); - return ; + return ; }; export default OnlineStatus; diff --git a/src/components/common/synchronize/component.js b/src/components/common/synchronize/component.js index 231a73331..f183060d6 100644 --- a/src/components/common/synchronize/component.js +++ b/src/components/common/synchronize/component.js @@ -1,13 +1,11 @@ -import { Dialog, makeStyles, Typography } from '@material-ui/core'; +import { makeStyles, Tooltip } from '@material-ui/core'; import IconButton from '@material-ui/core/IconButton'; -import { addOnlineStatusObserver } from 'common-tools'; -import SyncIcon from 'common-tools/icons/SyncIcon'; -import { synchronizePearl, synchronizeQueen } from 'common-tools/synchronize'; -import D from 'i18n'; +import SyncIcon from 'utils/icons/SyncIcon'; import PropTypes from 'prop-types'; -import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import Loader from '../loader'; +import React, { useContext } from 'react'; +import { AppContext } from 'Root'; +import D from 'i18n'; +import { SynchronizeWrapperContext } from 'components/sychronizeWrapper'; const useStyles = makeStyles(theme => ({ dialogPaper: { @@ -23,125 +21,25 @@ const useStyles = makeStyles(theme => ({ })); const Synchronize = ({ materialClass }) => { - const history = useHistory(); - const [loading, setLoading] = useState(false); - const [syncResult, setSyncResult] = useState(undefined); - const [pearlSync, setPearlSync] = useState(undefined); - const [status, setStatus] = useState(navigator.onLine); - - useEffect(() => { - addOnlineStatusObserver(s => { - setStatus(s); - }); - }, []); - - useEffect(() => { - const pearlSynchResult = window.localStorage.getItem('PEARL_SYNC_RESULT'); - const queenSynchResult = window.localStorage.getItem('QUEEN_SYNC_RESULT'); - - if (pearlSync) { - if (pearlSync === 'FAILURE') { - window.localStorage.removeItem('SYNCHRONIZE'); - setLoading(false); - setSyncResult({ state: false, message: D.syncFailure }); - } - } else if (pearlSynchResult !== null && queenSynchResult !== null) { - window.localStorage.removeItem('SYNCHRONIZE'); - if (pearlSynchResult === 'SUCCESS' && queenSynchResult === 'SUCCESS') { - setSyncResult({ state: true, message: D.syncSuccess }); - } else { - setSyncResult({ state: false, message: D.syncFailure }); - } - } - }, [pearlSync, history]); - - const syncFunction = () => { - window.localStorage.removeItem('PEARL_SYNC_RESULT'); - window.localStorage.removeItem('QUEEN_SYNC_RESULT'); - window.localStorage.setItem('SYNCHRONIZE', true); - - const launchSynchronize = async () => { - try { - setPearlSync(undefined); - setLoading(true); - - await synchronizePearl() - .catch(e => { - console.log('error in synchronizePearl()'); - throw e; - }) - .then(() => { - console.log('synchronize success'); - setPearlSync('SUCCESS'); - window.localStorage.setItem('PEARL_SYNC_RESULT', 'SUCCESS'); - }) - .catch(e => { - console.log('error during pearl Synchro'); - console.log(e); - setPearlSync('FAILURE'); - throw e; - }) - .then(async () => { - console.log('Pearl synchronization : ENDED !'); - await synchronizeQueen(history); - }) - .catch(e => { - console.log('Error in Queen Synchro'); - console.log(e); - }); - } catch (e) { - console.log('synch failure'); - console.log(e); - } - }; - launchSynchronize(); - }; - - const syncOnClick = () => { - if (!loading && status) { - syncFunction(); - } else { - console.log('offline'); - } - }; - - const close = async () => { - setSyncResult(undefined); - window.localStorage.removeItem('PEARL_SYNC_RESULT'); - }; + const { online } = useContext(AppContext); + const { syncFunction } = useContext(SynchronizeWrapperContext); const classes = useStyles(); return ( - <> - {loading && } - {!loading && syncResult && ( - - - {D.syncResult} - - {syncResult.message} - - )} - + syncOnClick()} + onClick={syncFunction} > - + ); }; diff --git a/src/components/notificationWrapper/index.js b/src/components/notificationWrapper/index.js new file mode 100644 index 000000000..e761afd40 --- /dev/null +++ b/src/components/notificationWrapper/index.js @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import notificationIdbService from 'indexedbb/services/notification-idb-service'; +import syncReportIdbService from 'indexedbb/services/syncReport-idb-service'; +import { NOTIFICATION_TYPE_SYNC } from 'utils/constants'; + +export const NotificationWrapperContext = React.createContext(); + +export const NotificationWrapper = ({ children }) => { + const [notifications, setNotifications] = useState([]); + const [notificationsFiltered, setNotificatiosFiltered] = useState([]); + const [filterType, setFilterType] = useState(null); + + useEffect(() => { + const newNotif = filterType + ? notifications.filter(({ type }) => type === filterType) + : notifications; + setNotificatiosFiltered(newNotif); + }, [notifications, filterType]); + + useEffect(() => { + const load = async () => { + const notificationsDB = await notificationIdbService.getAll(); + const orderNotif = notificationsDB.sort((a, b) => { + if (a.date < b.date) return 1; + if (a.date > b.date) return -1; + return 0; + }); + setNotifications(orderNotif); + }; + load(); + }, []); + + const markNotifAsRead = async notifId => { + const notif = notifications.filter(({ id }) => id === notifId)[0]; + const newNotif = { ...notif, read: true }; + await notificationIdbService.addOrUpdateNotif(newNotif); + const newNotifs = notifications.map(n => { + if (n.id === notifId) return newNotif; + return n; + }); + setNotifications(newNotifs); + }; + + const markAllFilteredNotifAsRead = async () => { + const newNotifs = notifications.map(notif => { + const { type } = notif; + if (filterType) { + if (type === filterType) return { ...notif, read: true }; + return notif; + } + return { ...notif, read: true }; + }); + await Promise.all( + newNotifs.map(async notif => { + await notificationIdbService.addOrUpdateNotif(notif); + }) + ); + setNotifications(newNotifs); + }; + + const deleteAll = async () => { + if (filterType === NOTIFICATION_TYPE_SYNC) await syncReportIdbService.deleteAll(); + const idsToDelete = notificationsFiltered.map(({ id }) => id); + await notificationIdbService.deleteByIds(idsToDelete); + const newNotifs = notifications.filter(({ id }) => !idsToDelete.includes(id)); + setNotifications(newNotifs); + }; + + const unReadNotificationsNumber = notifications.filter(({ read }) => !read).length; + const unReadNotificationsNumberFilterd = notificationsFiltered.filter(({ read }) => !read).length; + + const context = { + markNotifAsRead, + deleteAll, + markAllAsRead: markAllFilteredNotifAsRead, + filterType, + setFilterType, + notifications: notificationsFiltered, + unReadNotificationsNumberFilterd, + unReadNotificationsNumber, + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/panel-body/UESpage/component.js b/src/components/panel-body/UESpage/component.js index 4781e4ac1..eb2efbaed 100644 --- a/src/components/panel-body/UESpage/component.js +++ b/src/components/panel-body/UESpage/component.js @@ -1,15 +1,12 @@ import { Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import { - applyFilters, - sortOnColumnCompareFunction, - updateStateWithDates, -} from 'common-tools/functions'; +import { applyFilters, sortOnColumnCompareFunction, updateStateWithDates } from 'utils/functions'; import surveyUnitDBService from 'indexedbb/services/surveyUnit-idb-service'; import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import FilterPanel from './filterPanel'; import SurveyUnitCard from './material/surveyUnitCard'; +import surveyUnitMissingIdbService from 'indexedbb/services/surveyUnitMissing-idb-service'; const UESPage = ({ textSearch }) => { const [surveyUnits, setSurveyUnits] = useState([]); @@ -26,9 +23,15 @@ const UESPage = ({ textSearch }) => { terminated: false, }); + const [inaccessibles, setInaccessibles] = useState([]); + useEffect(() => { if (!init) { setInit(true); + surveyUnitMissingIdbService + .getAll() + .then(units => setInaccessibles(units.map(({ id }) => id))); + surveyUnitDBService.getAll().then(units => { const initializedSU = units.map(su => ({ ...su, selected: false })); setCampaigns([...new Set(units.map(unit => unit.campaign))]); @@ -93,7 +96,7 @@ const UESPage = ({ textSearch }) => { {filteredSurveyUnits.map(su => ( - + ))} diff --git a/src/components/panel-body/UESpage/filterPanel/component.js b/src/components/panel-body/UESpage/filterPanel/component.js index e5d5e4c4c..8e4cfe99a 100644 --- a/src/components/panel-body/UESpage/filterPanel/component.js +++ b/src/components/panel-body/UESpage/filterPanel/component.js @@ -10,7 +10,7 @@ import RadioGroup from '@material-ui/core/RadioGroup'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import toDoEnum from 'common-tools/enum/SUToDoEnum'; +import toDoEnum from 'utils/enum/SUToDoEnum'; import D from 'i18n'; import PropTypes from 'prop-types'; import React, { useState } from 'react'; @@ -167,7 +167,7 @@ const FilterPanel = ({ aria-controls="panel4bh-content" id="sortAccordion-header" > - Trier par + {D.sortBy} - Enquêtes + {D.sortSurvey} @@ -242,7 +242,7 @@ const FilterPanel = ({ aria-controls="panel4bh-content" id="priorityFilterAccordion-header" > - Priorité + {D.priority} @@ -273,7 +273,7 @@ const FilterPanel = ({ aria-controls="panel4bh-content" id="toDoFilterAccordion-header" > - À faire + {D.sortToDo} @@ -306,7 +306,7 @@ const FilterPanel = ({ aria-controls="panel4bh-content" id="priorityFilterAccordion-header" > - Terminées + {D.sortCompleted} diff --git a/src/components/panel-body/UESpage/material/surveyUnitCard.js b/src/components/panel-body/UESpage/material/surveyUnitCard.js index 5a96c68c6..066d36839 100644 --- a/src/components/panel-body/UESpage/material/surveyUnitCard.js +++ b/src/components/panel-body/UESpage/material/surveyUnitCard.js @@ -3,22 +3,25 @@ import CardContent from '@material-ui/core/CardContent'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; +import WarningIcon from '@material-ui/icons/Warning'; import LocationOnIcon from '@material-ui/icons/LocationOn'; import PersonIcon from '@material-ui/icons/Person'; import RadioButtonUncheckedSharpIcon from '@material-ui/icons/RadioButtonUncheckedSharp'; import ScheduleIcon from '@material-ui/icons/Schedule'; -import { intervalInDays } from 'common-tools/functions'; -import { convertSUStateInToDo } from 'common-tools/functions/convertSUStateInToDo'; +import { intervalInDays } from 'utils/functions'; +import { convertSUStateInToDo } from 'utils/functions/convertSUStateInToDo'; import { getLastState, getprivilegedPerson, isSelectable, -} from 'common-tools/functions/surveyUnitFunctions'; +} from 'utils/functions/surveyUnitFunctions'; import PropTypes from 'prop-types'; import React from 'react'; import { useHistory } from 'react-router-dom'; +import { Tooltip } from '@material-ui/core'; +import D from 'i18n'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ root: { padding: 8, borderRadius: 15, @@ -33,12 +36,12 @@ const useStyles = makeStyles(() => ({ justifyContent: 'space-between', }, justifyStart: { + display: 'flex', justifyContent: 'flex-start', }, flexColumn: { display: 'flex', flexDirection: 'column', - justifyContent: 'flex-end', }, content: { flex: '1 0 auto', @@ -57,7 +60,6 @@ const useStyles = makeStyles(() => ({ '&:hover': { cursor: 'not-allowed' }, }, paddingTop: { - height: 'max-content', paddingTop: '10px', fontSize: 'xxx-large', }, @@ -79,9 +81,10 @@ const useStyles = makeStyles(() => ({ leftMargin: { marginLeft: '2px', }, + warning: { color: theme.palette.warning.main }, })); -const SurveyUnitCard = ({ surveyUnit }) => { +const SurveyUnitCard = ({ surveyUnit, inaccessible = false }) => { const classes = useStyles(); const history = useHistory(); @@ -127,16 +130,23 @@ const SurveyUnitCard = ({ surveyUnit }) => { {ssech} - - -
- - {firstName} - - - {lastName} - + +
+ +
+ + {firstName} + + + {lastName} + +
+ {inaccessible && ( + + + + )}
@@ -170,9 +180,9 @@ const SurveyUnitCard = ({ surveyUnit }) => { export default SurveyUnitCard; SurveyUnitCard.propTypes = { surveyUnit: PropTypes.shape({ - id: PropTypes.string.isRequired, - firstName: PropTypes.string.isRequired, - lastName: PropTypes.string.isRequired, + id: PropTypes.string, + firstName: PropTypes.string, + lastName: PropTypes.string, address: PropTypes.shape({ l6: PropTypes.string.isRequired }).isRequired, campaign: PropTypes.string.isRequired, states: PropTypes.arrayOf(PropTypes.shape({ type: PropTypes.string.isRequired })).isRequired, diff --git a/src/components/panel-body/UEpage/atomicInfoTile.js b/src/components/panel-body/UEpage/atomicInfoTile.js index 840f91847..95d1c14a4 100644 --- a/src/components/panel-body/UEpage/atomicInfoTile.js +++ b/src/components/panel-body/UEpage/atomicInfoTile.js @@ -1,6 +1,6 @@ import { Paper, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import MaterialIcons from 'common-tools/icons/materialIcons'; +import MaterialIcons from 'utils/icons/materialIcons'; import PropTypes from 'prop-types'; import React from 'react'; diff --git a/src/components/panel-body/UEpage/comments/comment/component.js b/src/components/panel-body/UEpage/comments/comment/component.js index cea8ba29b..c9b8477e9 100644 --- a/src/components/panel-body/UEpage/comments/comment/component.js +++ b/src/components/panel-body/UEpage/comments/comment/component.js @@ -1,5 +1,5 @@ import { makeStyles, Paper, TextareaAutosize } from '@material-ui/core'; -import { getCommentByType } from 'common-tools/functions/surveyUnitFunctions'; +import { getCommentByType } from 'utils/functions/surveyUnitFunctions'; import D from 'i18n'; import PropTypes from 'prop-types'; import React, { useContext, useState } from 'react'; @@ -21,7 +21,7 @@ const useStyles = makeStyles(() => ({ })); const Comment = ({ editable, save }) => { - const surveyUnit = useContext(SurveyUnitContext); + const { surveyUnit } = useContext(SurveyUnitContext); const value = editable ? getCommentByType('INTERVIEWER', surveyUnit) : getCommentByType('MANAGEMENT', surveyUnit); diff --git a/src/components/panel-body/UEpage/component.js b/src/components/panel-body/UEpage/component.js index ffc0cb293..1e1193a23 100644 --- a/src/components/panel-body/UEpage/component.js +++ b/src/components/panel-body/UEpage/component.js @@ -1,5 +1,5 @@ -import suStateEnum from 'common-tools/enum/SUStateEnum'; -import { addNewState, getLastState } from 'common-tools/functions'; +import suStateEnum from 'utils/enum/SUStateEnum'; +import { addNewState, getLastState } from 'utils/functions'; import D from 'i18n'; import surveyUnitDBService from 'indexedbb/services/surveyUnit-idb-service'; import PropTypes from 'prop-types'; @@ -7,19 +7,25 @@ import React, { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import Router from './router'; import { SurveyUnitProvider } from './UEContext'; +import surveyUnitMissingIdbService from 'indexedbb/services/surveyUnitMissing-idb-service'; const UEPage = ({ match, refresh: homeRefresh }) => { const [surveyUnit, setSurveyUnit] = useState(undefined); const [shouldRefresh, setShouldRefresh] = useState(true); + const [inaccessible, setInaccessible] = useState(false); + const [loading, setLoading] = useState(true); const history = useHistory(); const { id } = useParams(); useEffect(() => { const updateSurveyUnit = async () => { - await surveyUnitDBService.getById(id).then(ue => { - setSurveyUnit({ ...ue }); - }); + setLoading(true); + const ue = await surveyUnitDBService.getById(id); + if (ue) setSurveyUnit({ ...ue }); + const isMissing = await surveyUnitMissingIdbService.getById(id); + if (isMissing) setInaccessible(true); + setLoading(false); }; if (shouldRefresh) { updateSurveyUnit(); @@ -50,13 +56,13 @@ const UEPage = ({ match, refresh: homeRefresh }) => { return ( <> - {surveyUnit && ( - + {surveyUnit && !loading && ( + )} - {!surveyUnit && ( + {!surveyUnit && !loading && ( <>