From da818c5805a948c18eff8b908a6d57ce94c80284 Mon Sep 17 00:00:00 2001 From: Arnaud AMBROSELLI Date: Tue, 3 Oct 2023 21:42:52 +0200 Subject: [PATCH] fix(dashboard): nouveau DataLoader --- dashboard/src/components/DataLoader.js | 756 +++++++++++-------------- dashboard/src/recoil/selectors.js | 1 + dashboard/src/scenes/auth/signin.js | 4 +- 3 files changed, 344 insertions(+), 417 deletions(-) diff --git a/dashboard/src/components/DataLoader.js b/dashboard/src/components/DataLoader.js index 534dafb451..0c22824121 100644 --- a/dashboard/src/components/DataLoader.js +++ b/dashboard/src/components/DataLoader.js @@ -1,5 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; +import { useEffect } from 'react'; import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { toast } from 'react-toastify'; @@ -27,361 +26,364 @@ import { groupsState } from '../recoil/groups'; // Update to flush cache. -const cacheEffect = ({ onSet }) => { - onSet(async (newValue) => { - await setCacheItem(dashboardCurrentCacheKey, newValue); - }); -}; - -const loaderTriggerState = atom({ key: 'loaderTriggerState', default: false }); const isLoadingState = atom({ key: 'isLoadingState', default: false }); const initialLoadState = atom({ key: 'isInitialLoadState', default: false }); const fullScreenState = atom({ key: 'fullScreenState', default: true }); -export const lastLoadState = atom({ key: 'lastLoadState', default: null, effects: [cacheEffect] }); +const progressState = atom({ key: 'progressState', default: null }); +const totalState = atom({ key: 'totalState', default: null }); export const initialLoadingTextState = 'En attente de chargement'; export const loadingTextState = atom({ key: 'loadingTextState', default: initialLoadingTextState }); +export const lastLoadState = atom({ + key: 'lastLoadState', + default: null, + effects: [ + ({ onSet }) => { + onSet(async (newValue) => { + await setCacheItem(dashboardCurrentCacheKey, newValue); + }); + }, + ], +}); export default function DataLoader() { - const [user, setUser] = useRecoilState(userState); - const { migrateData } = useDataMigrator(); - - const [persons, setPersons] = useRecoilState(personsState); - const [actions, setActions] = useRecoilState(actionsState); - const [consultations, setConsultations] = useRecoilState(consultationsState); - const [treatments, setTreatments] = useRecoilState(treatmentsState); - const [medicalFiles, setMedicalFiles] = useRecoilState(medicalFileState); - const [passages, setPassages] = useRecoilState(passagesState); - const [rencontres, setRencontres] = useRecoilState(rencontresState); - const [reports, setReports] = useRecoilState(reportsState); - const [territories, setTerritories] = useRecoilState(territoriesState); - const [places, setPlaces] = useRecoilState(placesState); - const [relsPersonPlace, setRelsPersonPlace] = useRecoilState(relsPersonPlaceState); - const [territoryObservations, setTerritoryObservations] = useRecoilState(territoryObservationsState); - const [comments, setComments] = useRecoilState(commentsState); - const [groups, setGroups] = useRecoilState(groupsState); - - const [loaderTrigger, setLoaderTrigger] = useRecoilState(loaderTriggerState); - const [lastLoad, setLastLoad] = useRecoilState(lastLoadState); - const [isLoading, setIsLoading] = useRecoilState(isLoadingState); - const [fullScreen, setFullScreen] = useRecoilState(fullScreenState); - const [loadingText, setLoadingText] = useRecoilState(loadingTextState); - const initialLoad = useRecoilValue(initialLoadState); - const [organisation, setOrganisation] = useRecoilState(organisationState); - - const [loadList, setLoadList] = useState({ list: [], offset: 0 }); - const [progressBuffer, setProgressBuffer] = useState(null); - const [progress, setProgress] = useState(null); - const [total, setTotal] = useState(null); + const isLoading = useRecoilValue(isLoadingState); + const fullScreen = useRecoilValue(fullScreenState); + const loadingText = useRecoilValue(loadingTextState); + const progress = useRecoilValue(progressState); + const total = useRecoilValue(totalState); - useEffect(() => { - initLoader(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [progress, total, loaderTrigger, loadList.list.length, isLoading]); - - useEffect(() => { - fetchData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadList]); - useEffect(() => { - updateProgress(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [progress, progressBuffer, loadList.list.length]); + if (!isLoading) return ; + if (!total && !fullScreen) return null; - const organisationId = organisation?._id; + if (fullScreen) { + return ( +
+
+ + +
+
+ ); + } - const serverDate = useRef(null); + return ( +
+ +
+ ); +} - // Loader initialization: get data from cache, check stats, init recoils states, and start loader. - function initLoader() { - if (loadList.list.length > 0) return; +export function useDataLoader(options = { refreshOnMount: false }) { + const [fullScreen, setFullScreen] = useRecoilState(fullScreenState); + const [isLoading, setIsLoading] = useRecoilState(isLoadingState); + const [initialLoad, setInitialLoad] = useRecoilState(initialLoadState); + const setLoadingText = useSetRecoilState(loadingTextState); + const setLastLoad = useSetRecoilState(lastLoadState); - const shouldStart = progress === null && total === null && loaderTrigger && isLoading; - const shouldStop = progress !== null && total !== null && isLoading; + const setUser = useSetRecoilState(userState); + const setOrganisation = useSetRecoilState(organisationState); + const { migrateData } = useDataMigrator(); - if (shouldStart) { - Promise.resolve() - .then(async () => { - /* - Refresh organisation (and user), to get the latest organisation fields - and the latest user roles - */ - const userResponse = await API.get({ path: '/user/me' }); - if (!userResponse.ok) return resetLoaderOnError(); - setOrganisation(userResponse.user.organisation); - setUser(userResponse.user); - // Get date from server at the very beginning of the loader. - const serverDateResponse = await API.get({ path: '/now' }); - serverDate.current = serverDateResponse.data; - }) - .then(() => (initialLoad ? migrateData() : Promise.resolve())) - .then(() => getCacheItem(dashboardCurrentCacheKey)) - .then((lastLoadValue) => { - setLastLoad(lastLoadValue || 0); - API.get({ - path: '/organisation/stats', - query: { - organisation: organisationId, - after: lastLoadValue || 0, - withDeleted: true, - // Medical data is never saved in cache so we always have to download all at every page reload. - withAllMedicalData: initialLoad, - }, - }).then(({ data: stats }) => { - if (!stats) return; - const newList = []; - let itemsCount = - 0 + - stats.persons + - stats.consultations + - stats.actions + - stats.treatments + - stats.medicalFiles + - stats.passages + - stats.rencontres + - stats.reports + - stats.territories + - stats.places + - stats.relsPersonPlace + - stats.territoryObservations + - stats.comments + - stats.groups; + const setPersons = useSetRecoilState(personsState); + const setActions = useSetRecoilState(actionsState); + const setConsultations = useSetRecoilState(consultationsState); + const setTreatments = useSetRecoilState(treatmentsState); + const setMedicalFiles = useSetRecoilState(medicalFileState); + const setPassages = useSetRecoilState(passagesState); + const setRencontres = useSetRecoilState(rencontresState); + const setReports = useSetRecoilState(reportsState); + const setTerritories = useSetRecoilState(territoriesState); + const setPlaces = useSetRecoilState(placesState); + const setRelsPersonPlace = useSetRecoilState(relsPersonPlaceState); + const setTerritoryObservations = useSetRecoilState(territoryObservationsState); + const setComments = useSetRecoilState(commentsState); + const setGroups = useSetRecoilState(groupsState); + const setProgress = useSetRecoilState(progressState); + const setTotal = useSetRecoilState(totalState); - if (stats.persons) newList.push('person'); - if (stats.groups) newList.push('group'); - if (stats.consultations) newList.push('consultation'); - if (['admin', 'normal'].includes(user.role)) { - if (stats.treatments) newList.push('treatment'); - if (stats.medicalFiles) newList.push('medicalFile'); - } - if (stats.reports) newList.push('report'); - if (stats.passages) newList.push('passage'); - if (stats.rencontres) newList.push('rencontre'); - if (stats.actions) newList.push('action'); - if (stats.territories) newList.push('territory'); - if (stats.places) newList.push('place'); - if (stats.relsPersonPlace) newList.push('relsPersonPlace'); - if (stats.territoryObservations) newList.push('territoryObservation'); - if (stats.comments) newList.push('comment'); + useEffect(function refreshOnMountEffect() { + if (options.refreshOnMount && !isLoading) loadOrRefreshData(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - // In case this is not the initial load, we don't have to load from cache again. - if (!initialLoad) { - startLoader(newList, itemsCount); - return; - } + async function loadOrRefreshData(isStartingInitialLoad) { + setIsLoading(true); + setFullScreen(isStartingInitialLoad ? true : false); + setInitialLoad(isStartingInitialLoad ? true : false); + setLoadingText(setInitialLoad ? 'Chargement des données' : 'Mise à jour des données'); + + /* + Refresh organisation (and user), to get the latest organisation fields + and the latest user roles + */ + const userResponse = await API.get({ path: '/user/me' }); + if (!userResponse.ok) return resetLoaderOnError(); + const latestOrganisation = userResponse.user.organisation; + const latestUser = userResponse.user; + const organisationId = latestOrganisation._id; + setOrganisation(latestOrganisation); + setUser(latestUser); + // Get date from server at the very beginning of the loader. + const serverDateResponse = await API.get({ path: '/now' }); + const serverDate = serverDateResponse.data; + if (initialLoad) { + await migrateData(); + } + const lastLoadValueCached = await getCacheItem(dashboardCurrentCacheKey); + const lastLoadValue = lastLoadValueCached || 0; + setLastLoad(lastLoadValue); + + const statsResponse = await API.get({ + path: '/organisation/stats', + query: { + organisation: organisationId, + after: lastLoadValue, + withDeleted: true, + // Medical data is never saved in cache so we always have to download all at every page reload. + withAllMedicalData: initialLoad, + }, + }); - setLoadingText('Récupération des données dans le cache'); - Promise.resolve() - .then(() => getCacheItemDefaultValue('person', [])) - .then((persons) => setPersons([...persons])) - .then(() => getCacheItemDefaultValue('group', [])) - .then((groups) => setGroups([...groups])) - .then(() => getCacheItemDefaultValue('report', [])) - .then((reports) => setReports([...reports])) - .then(() => getCacheItemDefaultValue('passage', [])) - .then((passages) => setPassages([...passages])) - .then(() => getCacheItemDefaultValue('rencontre', [])) - .then((rencontres) => setRencontres([...rencontres])) - .then(() => getCacheItemDefaultValue('action', [])) - .then((actions) => setActions([...actions])) - .then(() => getCacheItemDefaultValue('territory', [])) - .then((territories) => setTerritories([...territories])) - .then(() => getCacheItemDefaultValue('place', [])) - .then((places) => setPlaces([...places])) - .then(() => getCacheItemDefaultValue('relPersonPlace', [])) - .then((relsPersonPlace) => setRelsPersonPlace([...relsPersonPlace])) - .then(() => getCacheItemDefaultValue('territory-observation', [])) - .then((territoryObservations) => setTerritoryObservations([...territoryObservations])) - .then(() => getCacheItemDefaultValue('comment', [])) - .then((comments) => setComments([...comments])) - .then(() => startLoader(newList, itemsCount)); - }); - }); - } else if (shouldStop) stopLoader(); - } + if (!statsResponse.ok) return false; + const stats = statsResponse.data; + let itemsCount = + 0 + + stats.persons + + stats.consultations + + stats.actions + + stats.treatments + + stats.medicalFiles + + stats.passages + + stats.rencontres + + stats.reports + + stats.territories + + stats.places + + stats.relsPersonPlace + + stats.territoryObservations + + stats.comments + + stats.groups; - // Fetch data from API, handle loader progress. - async function fetchData() { - if (loadList.list.length === 0) return; + setProgress(0); + setTotal(itemsCount); - const [current] = loadList.list; const query = { organisation: organisationId, limit: String(10000), - page: String(loadList.offset), - after: lastLoad, - withDeleted: Boolean(lastLoad), + after: lastLoadValue, + withDeleted: Boolean(lastLoadValue), }; - function handleMore(hasMore) { - if (hasMore) setLoadList({ list: loadList.list, offset: loadList.offset + 1 }); - else setLoadList({ list: loadList.list.slice(1), offset: 0 }); - } - - if (current === 'person') { + if (stats.persons > 0) { setLoadingText('Chargement des personnes'); - const res = await API.get({ path: '/person', query }); - if (!res.data) return resetLoaderOnError(); - setPersons( - res.hasMore - ? mergeItems(persons, res.decryptedData) - : mergeItems(persons, res.decryptedData) - .map((p) => ({ ...p, followedSince: p.followedSince || p.createdAt })) - .sort((p1, p2) => (p1.name || '').localeCompare(p2.name || '')) - ); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'group') { + const cachedPersons = await getCacheItemDefaultValue('person', []); + setPersons([...cachedPersons]); + async function loadPersons(page = 0) { + const res = await API.get({ path: '/person', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setPersons((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadPersons(page + 1); + return true; + } + const personSuccess = await loadPersons(0); + if (!personSuccess) return false; + } + if (stats.groups > 0) { setLoadingText('Chargement des familles'); - const res = await API.get({ path: '/group', query }); - if (!res.data) return resetLoaderOnError(); - setGroups(() => { - const mergedItems = mergeItems(groups, res.decryptedData); - if (res.hasMore) return mergedItems; - if (mergedItems.length > groups.length) { - return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - } - return mergedItems; - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'consultation') { - setLoadingText('Chargement des consultations'); - const res = await API.get({ path: '/consultation', query: { ...query, after: initialLoad ? 0 : lastLoad } }); - if (!res.data) return resetLoaderOnError(); - setConsultations( - res.hasMore ? mergeItems(consultations, res.decryptedData) : mergeItems(consultations, res.decryptedData).map(formatConsultation) - ); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'treatment') { - setLoadingText('Chargement des traitements'); - const res = await API.get({ path: '/treatment', query: { ...query, after: initialLoad ? 0 : lastLoad } }); - if (!res.data) return resetLoaderOnError(); - setTreatments(mergeItems(treatments, res.decryptedData)); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'medicalFile') { - setLoadingText('Chargement des fichiers médicaux'); - const res = await API.get({ path: '/medical-file', query: { ...query, after: initialLoad ? 0 : lastLoad } }); - if (!res.data) return resetLoaderOnError(); - setMedicalFiles(mergeItems(medicalFiles, res.decryptedData)); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'report') { - setLoadingText('Chargement des rapports'); - const res = await API.get({ path: '/report', query }); - if (!res.data) return resetLoaderOnError(); - setReports( - res.hasMore - ? mergeItems(reports, res.decryptedData) - : mergeItems(reports, res.decryptedData) - // This line should be removed when `clean-reports-with-no-team-nor-date` migration has run on all organisations. - .filter((r) => !!r.team && !!r.date) - ); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'passage') { + const cachedGroups = await getCacheItemDefaultValue('group', []); + setGroups([...cachedGroups]); + async function loadGroups(page = 0) { + const res = await API.get({ path: '/group', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setGroups((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadGroups(page + 1); + return true; + } + const groupsSuccess = await loadGroups(0); + if (!groupsSuccess) return false; + } + if (stats.reports > 0) { + setLoadingText('Chargement des comptes-rendus'); + const cachedReports = await getCacheItemDefaultValue('report', []); + setReports([...cachedReports]); + async function loadReports(page = 0) { + const res = await API.get({ path: '/report', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setReports((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadReports(page + 1); + return true; + } + const reportsSuccess = await loadReports(0); + if (!reportsSuccess) return false; + } + if (stats.passages > 0) { setLoadingText('Chargement des passages'); - const res = await API.get({ path: '/passage', query }); - if (!res.data) return resetLoaderOnError(); - setPassages(() => { - const mergedItems = mergeItems(passages, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'rencontre') { + const cachedPassages = await getCacheItemDefaultValue('passage', []); + setPassages([...cachedPassages]); + async function loadPassages(page = 0) { + const res = await API.get({ path: '/passage', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setPassages((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadPassages(page + 1); + return true; + } + const passagesSuccess = await loadPassages(0); + if (!passagesSuccess) return false; + } + if (stats.rencontres > 0) { setLoadingText('Chargement des rencontres'); - const res = await API.get({ path: '/rencontre', query }); - if (!res.data) return resetLoaderOnError(); - setRencontres(() => { - const mergedItems = mergeItems(rencontres, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'action') { - setFullScreen(false); + const cachedRencontres = await getCacheItemDefaultValue('rencontre', []); + setRencontres([...cachedRencontres]); + async function loadRencontres(page = 0) { + const res = await API.get({ path: '/rencontre', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setRencontres((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadRencontres(page + 1); + return true; + } + const rencontresSuccess = await loadRencontres(0); + if (!rencontresSuccess) return false; + } + if (stats.actions > 0) { setLoadingText('Chargement des actions'); - const res = await API.get({ path: '/action', query }); - if (!res.data) return resetLoaderOnError(); - setActions(mergeItems(actions, res.decryptedData)); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'territory') { + const cachedActions = await getCacheItemDefaultValue('action', []); + setActions([...cachedActions]); + async function loadActions(page = 0) { + const res = await API.get({ path: '/action', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setActions((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadActions(page + 1); + return true; + } + const actionsSuccess = await loadActions(0); + if (!actionsSuccess) return false; + } + if (stats.territories > 0) { setLoadingText('Chargement des territoires'); - const res = await API.get({ path: '/territory', query }); - if (!res.data) return resetLoaderOnError(); - setTerritories(() => { - const mergedItems = mergeItems(territories, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'place') { + const cachedTerritories = await getCacheItemDefaultValue('territory', []); + setTerritories([...cachedTerritories]); + async function loadTerritories(page = 0) { + const res = await API.get({ path: '/territory', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setTerritories((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadTerritories(page + 1); + return true; + } + const territoriesSuccess = await loadTerritories(0); + if (!territoriesSuccess) return false; + } + if (stats.places > 0) { setLoadingText('Chargement des lieux'); - const res = await API.get({ path: '/place', query }); - if (!res.data) return resetLoaderOnError(); - setPlaces(() => { - const mergedItems = mergeItems(places, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'relsPersonPlace') { + const cachedPlaces = await getCacheItemDefaultValue('place', []); + setPlaces([...cachedPlaces]); + async function loadPlaces(page = 0) { + const res = await API.get({ path: '/place', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setPlaces((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadPlaces(page + 1); + return true; + } + const placesSuccess = await loadPlaces(0); + if (!placesSuccess) return false; + } + if (stats.relsPersonPlace > 0) { setLoadingText('Chargement des relations personne-lieu'); - const res = await API.get({ path: '/relPersonPlace', query }); - if (!res.data) return resetLoaderOnError(); - setRelsPersonPlace(() => { - const mergedItems = mergeItems(relsPersonPlace, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'territoryObservation') { + const cachedRelPersonPlace = await getCacheItemDefaultValue('relPersonPlace', []); + setRelsPersonPlace([...cachedRelPersonPlace]); + async function loadRelPersonPlaces(page = 0) { + const res = await API.get({ path: '/relPersonPlace', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setRelsPersonPlace((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadRelPersonPlaces(page + 1); + return true; + } + const relsPersonPlacesSuccess = await loadRelPersonPlaces(0); + if (!relsPersonPlacesSuccess) return false; + } + if (stats.territoryObservations > 0) { setLoadingText('Chargement des observations de territoire'); - const res = await API.get({ path: '/territory-observation', query }); - if (!res.data) return resetLoaderOnError(); - setTerritoryObservations(() => { - const mergedItems = mergeItems(territoryObservations, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.observedAt || b.createdAt) - new Date(a.observedAt || a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); - } else if (current === 'comment') { + const cachedObservations = await getCacheItemDefaultValue('territory-observation', []); + setTerritoryObservations([...cachedObservations]); + async function loadObservations(page = 0) { + const res = await API.get({ path: '/territory-observation', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setTerritoryObservations((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadObservations(page + 1); + return true; + } + const territoryObservationsSuccess = await loadObservations(0); + if (!territoryObservationsSuccess) return false; + } + if (stats.comments > 0) { setLoadingText('Chargement des commentaires'); - const res = await API.get({ path: '/comment', query }); - if (!res.data) return resetLoaderOnError(); - setComments(() => { - const mergedItems = mergeItems(comments, res.decryptedData); - if (res.hasMore) return mergedItems; - return mergedItems.sort((a, b) => new Date(b.date || b.createdAt) - new Date(a.date || a.createdAt)); - }); - handleMore(res.hasMore); - setProgressBuffer(res.data.length); + const cachedComments = await getCacheItemDefaultValue('comment', []); + setComments([...cachedComments]); + async function loadComments(page = 0) { + const res = await API.get({ path: '/comment', query: { ...query, page: String(page) } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setComments((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadComments(page + 1); + return true; + } + const commentsSuccess = await loadComments(0); + if (!commentsSuccess) return false; + } + if (stats.consultations > 0) { + setLoadingText('Chargement des consultations'); + async function loadConsultations(page = 0) { + const res = await API.get({ path: '/consultation', query: { ...query, page: String(page), after: initialLoad ? 0 : lastLoadValue } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setConsultations((items) => mergeItems(items, res.decryptedData, formatConsultation)); + if (res.hasMore) return loadConsultations(page + 1); + return true; + } + const consultationsSuccess = await loadConsultations(0); + if (!consultationsSuccess) return false; + } + if (stats.treatments > 0) { + setLoadingText('Chargement des traitements'); + async function loadTreatments(page = 0) { + const res = await API.get({ path: '/treatment', query: { ...query, page: String(page), after: initialLoad ? 0 : lastLoadValue } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setTreatments((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadTreatments(page + 1); + return true; + } + const treatmentsSuccess = await loadTreatments(0); + if (!treatmentsSuccess) return false; + } + if (stats.medicalFiles > 0) { + setLoadingText('Chargement des fichiers médicaux'); + async function loadMedicalFiles(page = 0) { + const res = await API.get({ path: '/medical-file', query: { ...query, page: String(page), after: initialLoad ? 0 : lastLoadValue } }); + if (!res.ok || !res.data.length) return resetLoaderOnError(); + setProgress((p) => p + res.data.length); + setMedicalFiles((items) => mergeItems(items, res.decryptedData)); + if (res.hasMore) return loadMedicalFiles(page + 1); + return true; + } + const medicalFilesSuccess = await loadMedicalFiles(0); + if (!medicalFilesSuccess) return false; } - } - - function startLoader(list, itemsCount) { - setLoadList({ list, offset: 0 }); - setLoaderTrigger(false); - setProgress(0); - setTotal(itemsCount); - } - function stopLoader() { setIsLoading(false); - setLastLoad(serverDate.current); + setLastLoad(serverDate); setLoadingText('En attente de chargement'); - setProgressBuffer(null); setProgress(null); setTotal(null); + return true; } async function resetLoaderOnError() { @@ -393,64 +395,7 @@ export default function DataLoader() { onClose: () => window.location.replace('/auth'), autoClose: 5000, }); - } - - function updateProgress() { - if (!loadList.list.length) return; - - if (progressBuffer !== null) { - setProgress((progress || 0) + progressBuffer); - setProgressBuffer(null); - } - } - - if (!isLoading) return ; - if (!total && !fullScreen) return null; - - if (fullScreen) { - return ( - - - - - - - ); - } - - return ( - - - - ); -} - -export function useDataLoader(options = { refreshOnMount: false }) { - const [fullScreen, setFullScreen] = useRecoilState(fullScreenState); - const [isLoading, setIsLoading] = useRecoilState(isLoadingState); - const setLoaderTrigger = useSetRecoilState(loaderTriggerState); - const setInitialLoad = useSetRecoilState(initialLoadState); - const setLoadingText = useSetRecoilState(loadingTextState); - const setLastLoad = useSetRecoilState(lastLoadState); - - useEffect(function refreshOnMountEffect() { - if (options.refreshOnMount && !isLoading) refresh(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - function refresh() { - setIsLoading(true); - setFullScreen(false); - setInitialLoad(false); - setLoaderTrigger(true); - setLoadingText('Mise à jour des données'); - } - function load() { - setIsLoading(true); - setFullScreen(true); - setInitialLoad(true); - setLoaderTrigger(true); - setLoadingText('Chargement des données'); + return false; } async function resetCache() { @@ -459,54 +404,35 @@ export function useDataLoader(options = { refreshOnMount: false }) { } return { - refresh, - load, + refresh: () => loadOrRefreshData(false), + startInitialLoad: () => loadOrRefreshData(true), resetCache, isLoading: Boolean(isLoading), isFullScreen: Boolean(fullScreen), }; } -export const mergeItems = (oldItems, newItems = []) => { +export function mergeItems(oldItems, newItems = [], formatNewItemsFunction) { + const newItemsCleanedAndFormatted = []; const newItemIds = {}; + for (const newItem of newItems) { newItemIds[newItem._id] = true; + if (newItem.deletedAt) continue; + if (formatNewItemsFunction) { + newItemsCleanedAndFormatted.push(formatNewItemsFunction(newItem)); + } else { + newItemsCleanedAndFormatted.push(newItem); + } } - const oldItemsPurged = oldItems.filter((item) => !newItemIds[item._id] && !item.deletedAt); - return [...oldItemsPurged, ...newItems.filter((item) => !item.deletedAt)]; -}; - -const FullScreenContainer = styled.div` - width: 100%; - z-index: 1000; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - box-sizing: border-box; - background-color: #fff; - display: flex; - justify-content: center; - align-items: center; -`; -const InsideContainer = styled.div` - display: flex; - flex-direction: column; - width: 50vw; - max-width: 50vh; - height: 50vh; - max-height: 50vw; - justify-content: center; - align-items: center; -`; + const oldItemsPurged = []; + for (const oldItem of oldItems) { + if (oldItem.deletedAt) continue; + if (!newItemIds[oldItem._id]) { + oldItemsPurged.push(oldItem); + } + } -const Container = styled.div` - width: 100%; - z-index: 1000; - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; -`; + return [...oldItemsPurged, ...newItemsCleanedAndFormatted]; +} diff --git a/dashboard/src/recoil/selectors.js b/dashboard/src/recoil/selectors.js index 5f8634fafa..61415935ca 100644 --- a/dashboard/src/recoil/selectors.js +++ b/dashboard/src/recoil/selectors.js @@ -102,6 +102,7 @@ export const itemsGroupedByPersonSelector = selector({ originalPersonsObject[person._id] = { name: person.name, _id: person._id }; personsObject[person._id] = { ...person, + followedSince: person.followedSince || person.createdAt, userPopulated: usersObject[person.user], formattedBirthDate: formatBirthDate(person.birthdate), age: formatAge(person.birthdate), diff --git a/dashboard/src/scenes/auth/signin.js b/dashboard/src/scenes/auth/signin.js index 5f6087614a..3d12be70f8 100644 --- a/dashboard/src/scenes/auth/signin.js +++ b/dashboard/src/scenes/auth/signin.js @@ -28,7 +28,7 @@ const SignIn = () => { const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(true); const [authViaCookie, setAuthViaCookie] = useState(false); - const { load: runDataLoader, isLoading, resetCache } = useDataLoader(); + const { startInitialLoad, isLoading, resetCache } = useDataLoader(); const setToken = useSetRecoilState(authTokenState); const [signinForm, setSigninForm] = useState({ email: '', password: '', orgEncryptionKey: DEFAULT_ORGANISATION_KEY || '' }); @@ -45,7 +45,7 @@ const SignIn = () => { } }, [history, organisation, isLoading, isDesktop]); - const onSigninValidated = async () => runDataLoader(); + const onSigninValidated = () => startInitialLoad(); const onLogout = async () => { await API.logout();