From a428756b82a72e9faab884d44c5ad5e2caf2b243 Mon Sep 17 00:00:00 2001 From: Dylan Decrulle <81740200+ddecrulle@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:35:28 +0100 Subject: [PATCH] checkpoint --- .../src/core/adapters/datastore/default.ts | 4 +- .../src/core/adapters/queenApi/default.ts | 46 ++--- .../queenApi/parserSchema/SurveyUnitSchema.ts | 7 +- drama-queen/src/core/ports/DataStore.ts | 4 +- drama-queen/src/core/ports/LocalStorage.ts | 0 drama-queen/src/core/ports/QueenApi.ts | 21 ++- drama-queen/src/core/setup.ts | 21 +++ .../src/core/usecases/downloadData/evt.ts | 23 --- .../core/usecases/downloadData/selectors.ts | 40 ----- .../src/core/usecases/downloadData/state.ts | 86 --------- .../src/core/usecases/downloadData/thunks.ts | 106 ----------- drama-queen/src/core/usecases/index.ts | 8 +- .../src/core/usecases/synchronizeData/evt.ts | 24 +++ .../index.ts | 0 .../usecases/synchronizeData/selectors.ts | 78 +++++++++ .../core/usecases/synchronizeData/state.ts | 130 ++++++++++++++ .../core/usecases/synchronizeData/thunks.ts | 165 ++++++++++++++++++ .../src/core/usecases/uploadData/evt.ts | 23 --- .../src/core/usecases/uploadData/index.ts | 4 - .../src/core/usecases/uploadData/selectors.ts | 24 --- .../src/core/usecases/uploadData/state.ts | 45 ----- .../src/core/usecases/uploadData/thunks.ts | 36 ---- .../src/ui/pages/synchronize/DownloadData.tsx | 63 ------- .../ui/pages/synchronize/LoadingDisplay.tsx | 4 +- .../ui/pages/synchronize/SynchronizeData.tsx | 72 ++++++++ .../src/ui/pages/synchronize/UploadData.tsx | 74 -------- drama-queen/src/ui/routing/routes.tsx | 5 +- 27 files changed, 544 insertions(+), 569 deletions(-) create mode 100644 drama-queen/src/core/ports/LocalStorage.ts delete mode 100644 drama-queen/src/core/usecases/downloadData/evt.ts delete mode 100644 drama-queen/src/core/usecases/downloadData/selectors.ts delete mode 100644 drama-queen/src/core/usecases/downloadData/state.ts delete mode 100644 drama-queen/src/core/usecases/downloadData/thunks.ts create mode 100644 drama-queen/src/core/usecases/synchronizeData/evt.ts rename drama-queen/src/core/usecases/{downloadData => synchronizeData}/index.ts (100%) create mode 100644 drama-queen/src/core/usecases/synchronizeData/selectors.ts create mode 100644 drama-queen/src/core/usecases/synchronizeData/state.ts create mode 100644 drama-queen/src/core/usecases/synchronizeData/thunks.ts delete mode 100644 drama-queen/src/core/usecases/uploadData/evt.ts delete mode 100644 drama-queen/src/core/usecases/uploadData/index.ts delete mode 100644 drama-queen/src/core/usecases/uploadData/selectors.ts delete mode 100644 drama-queen/src/core/usecases/uploadData/state.ts delete mode 100644 drama-queen/src/core/usecases/uploadData/thunks.ts delete mode 100644 drama-queen/src/ui/pages/synchronize/DownloadData.tsx create mode 100644 drama-queen/src/ui/pages/synchronize/SynchronizeData.tsx delete mode 100644 drama-queen/src/ui/pages/synchronize/UploadData.tsx diff --git a/drama-queen/src/core/adapters/datastore/default.ts b/drama-queen/src/core/adapters/datastore/default.ts index b3f95f4c..b23d3408 100644 --- a/drama-queen/src/core/adapters/datastore/default.ts +++ b/drama-queen/src/core/adapters/datastore/default.ts @@ -20,9 +20,9 @@ export function createDataStore(params: { return { updateSurveyUnit: (surveyUnit) => db.surveyUnit.put(surveyUnit), deleteSurveyUnit: (id) => db.surveyUnit.delete(id), - getAllSurveyUnit: () => db.surveyUnit.toArray(), + getAllSurveyUnits: () => db.surveyUnit.toArray(), getSurveyUnit: (id) => db.surveyUnit.get(id), - getAllParadata: () => db.paradata.toArray(), + getAllParadatas: () => db.paradata.toArray(), deleteParadata: (id) => db.paradata.delete(id), getParadata: (id) => db.paradata.get(id), }; diff --git a/drama-queen/src/core/adapters/queenApi/default.ts b/drama-queen/src/core/adapters/queenApi/default.ts index 573d1b33..4422a6d0 100644 --- a/drama-queen/src/core/adapters/queenApi/default.ts +++ b/drama-queen/src/core/adapters/queenApi/default.ts @@ -11,7 +11,6 @@ import { nomenclatureSchema, requiredNomenclaturesSchema, surveyUnitSchema, - surveyUnitWithIdSchema, } from "./parserSchema"; import { Campaign, @@ -32,7 +31,7 @@ export function createApiClient(params: { const axiosInstance = axios.create({ baseURL: apiUrl, timeout: 120_000 }); const onRequest = (config: AxiosRequestConfig) => { - console.info(`[request] [${JSON.stringify(config)}]`); + //console.info(`[request] [${JSON.stringify(config)}]`); return { ...(config as any), headers: { @@ -60,11 +59,11 @@ export function createApiClient(params: { }; const onResponse = (response: AxiosResponse): AxiosResponse => { - console.info(`[response] [${JSON.stringify(response)}]`); + //console.info(`[response] [${JSON.stringify(response)}]`); return response; }; - const onResponseError = (error: AxiosError): Promise => { + const onResponseError = (error: AxiosError) => { console.error(`[response error] [${JSON.stringify(error)}]`); return Promise.reject(error); }; @@ -87,35 +86,34 @@ export function createApiClient(params: { axiosInstance .get(`/api/survey-units/interviewer`) .then(({ data }) => - data.map((surveyUnit) => surveyUnitWithIdSchema.parse(surveyUnit)) + data.map((surveyUnit) => surveyUnitSchema.parse(surveyUnit)) ), { promise: true } ), getSurveyUnit: memoize( (idSurveyUnit) => axiosInstance - .get(`/api/survey-unit/${idSurveyUnit}`) - .then(({ data }) => ({ - id: idSurveyUnit, - ...surveyUnitSchema.parse(data), - })), + .get>(`/api/survey-unit/${idSurveyUnit}`) + .then(({ data }) => + surveyUnitSchema.parse({ id: idSurveyUnit, ...data }) + ), { promise: true } ), putSurveyUnit: (idSurveyUnit, surveyUnit) => - axiosInstance.put( - `api/survey-unit/${idSurveyUnit}`, - surveyUnit - ), + axiosInstance + .put(`api/survey-unit/${idSurveyUnit}`, surveyUnit) + .then(() => undefined), putSurveyUnitsData: (surveyUnitsData) => - axiosInstance.put( - `/api/survey-units/data`, - surveyUnitsData - ), + axiosInstance + .put(`/api/survey-units/data`, surveyUnitsData) + .then(() => undefined), postSurveyUnitInTemp: (idSurveyUnit, surveyUnit) => - axiosInstance.post( - `api/survey-unit/${idSurveyUnit}/temp-zone`, - surveyUnit - ), + axiosInstance + .post( + `api/survey-unit/${idSurveyUnit}/temp-zone`, + surveyUnit + ) + .then(() => undefined), getCampaigns: memoize( () => axiosInstance @@ -147,6 +145,8 @@ export function createApiClient(params: { { promise: true } ), postParadata: (paradata) => - axiosInstance.post(`/api/paradata`, paradata), + axiosInstance + .post(`/api/paradata`, paradata) + .then(() => undefined), }; } diff --git a/drama-queen/src/core/adapters/queenApi/parserSchema/SurveyUnitSchema.ts b/drama-queen/src/core/adapters/queenApi/parserSchema/SurveyUnitSchema.ts index 0f63dbdd..c9951709 100644 --- a/drama-queen/src/core/adapters/queenApi/parserSchema/SurveyUnitSchema.ts +++ b/drama-queen/src/core/adapters/queenApi/parserSchema/SurveyUnitSchema.ts @@ -13,13 +13,10 @@ const stateDataSchema = z.object({ }); export const surveyUnitSchema = z.object({ + id: z.string(), questionnaireId: z.string(), personalization: z.union([z.object({}).array(), z.object({})]), data: surveyUnitDataSchema, comment: z.object({}), // not implemented yet, only present in test data stateData: stateDataSchema.optional(), -}); - -export const surveyUnitWithIdSchema = surveyUnitSchema.extend({ - id: z.string(), -}); +}); \ No newline at end of file diff --git a/drama-queen/src/core/ports/DataStore.ts b/drama-queen/src/core/ports/DataStore.ts index 644b78e7..5e239676 100644 --- a/drama-queen/src/core/ports/DataStore.ts +++ b/drama-queen/src/core/ports/DataStore.ts @@ -3,9 +3,9 @@ import { Paradata, SurveyUnit } from "core/model"; export type DataStore = { updateSurveyUnit: (surveyUnit: SurveyUnit) => Promise; deleteSurveyUnit: (id: string) => Promise; - getAllSurveyUnit: () => Promise; + getAllSurveyUnits: () => Promise; getSurveyUnit: (id: string) => Promise; - getAllParadata: () => Promise; + getAllParadatas: () => Promise; deleteParadata: (id: string) => Promise; getParadata: (id: string) => Promise; }; diff --git a/drama-queen/src/core/ports/LocalStorage.ts b/drama-queen/src/core/ports/LocalStorage.ts new file mode 100644 index 00000000..e69de29b diff --git a/drama-queen/src/core/ports/QueenApi.ts b/drama-queen/src/core/ports/QueenApi.ts index 50c9c559..5e993df1 100644 --- a/drama-queen/src/core/ports/QueenApi.ts +++ b/drama-queen/src/core/ports/QueenApi.ts @@ -1,4 +1,12 @@ -import { Campaign, IdAndQuestionnaireId, Nomenclature, Paradata, Questionnaire, RequiredNomenclatures, SurveyUnit } from "core/model"; +import { + Campaign, + IdAndQuestionnaireId, + Nomenclature, + Paradata, + Questionnaire, + RequiredNomenclatures, + SurveyUnit, +} from "core/model"; export type QueenApi = { getSurveyUnitsIdsAndQuestionnaireIdsByCampaign: ( @@ -12,7 +20,10 @@ export type QueenApi = { */ getSurveyUnits: () => Promise; getSurveyUnit: (idSurveyUnit: string) => Promise; - putSurveyUnit: (idSurveyUnit: string, surveyUnit: SurveyUnit) => void; + putSurveyUnit: ( + idSurveyUnit: string, + surveyUnit: SurveyUnit + ) => Promise; /** * Endpoint in development * @param @@ -21,16 +32,16 @@ export type QueenApi = { */ putSurveyUnitsData: ( surveyUnitsData: Omit[] - ) => void; + ) => Promise; postSurveyUnitInTemp: ( idSurveyUnit: string, surveyUnit: SurveyUnit - ) => void; + ) => Promise; getCampaigns: () => Promise; getQuestionnaire: (idQuestionnaire: string) => Promise; getRequiredNomenclaturesByCampaign: ( idCampaign: string ) => Promise; getNomenclature: (idNomenclature: string) => Promise; - postParadata: (paradata: Paradata) => void; + postParadata: (paradata: Paradata) => Promise; }; diff --git a/drama-queen/src/core/setup.ts b/drama-queen/src/core/setup.ts index 55d79a3c..d4c72a40 100644 --- a/drama-queen/src/core/setup.ts +++ b/drama-queen/src/core/setup.ts @@ -53,11 +53,32 @@ export async function createCore(params: CoreParams) { }); })(); + const dataStore = await (async () => { + const { createDataStore } = await import("core/adapters/datastore/default"); + /** + * TODO : replace schema (There are impact on legacy queens) + schema: { + paradata: "idSU", + surveyUnit: "id", + } + version: 3, + */ + return createDataStore({ + name: "Queen", + schema: { + paradata: "++id,idSU,events", + surveyUnit: "id,data,stateData,personalization,comment,questionnaireId", + }, + version: 2, + }); + })(); + const core = createCoreFromUsecases({ thunksExtraArgument: { coreParams: params, oidc, queenApi, + dataStore, }, usecases, }); diff --git a/drama-queen/src/core/usecases/downloadData/evt.ts b/drama-queen/src/core/usecases/downloadData/evt.ts deleted file mode 100644 index bbb91f03..00000000 --- a/drama-queen/src/core/usecases/downloadData/evt.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CreateEvt } from "core/setup"; -import { Evt } from "evt"; -import { name } from "./state"; - -export const createEvt = (({ evtAction }) => { - const evt = Evt.create<{ - action: "redirect"; - }>(); - - evtAction - .pipe((action) => - action.sliceName === name && action.actionName === "completed" - ? [action] - : null - ) - .attach(() => { - evt.post({ - action: "redirect", - }); - }); - - return evt; -}) satisfies CreateEvt; diff --git a/drama-queen/src/core/usecases/downloadData/selectors.ts b/drama-queen/src/core/usecases/downloadData/selectors.ts deleted file mode 100644 index 1dfa62f6..00000000 --- a/drama-queen/src/core/usecases/downloadData/selectors.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { State as RootState } from "core/setup"; -import { name } from "./state"; -import { createSelector } from "@reduxjs/toolkit"; - -const state = (rootState: RootState) => rootState[name]; - -const runningState = createSelector(state, (state) => { - if (state.stateDescription === "not running") { - return undefined; - } - return state; -}); - -const isRunning = createSelector(runningState, (state) => state !== undefined); - -const surveyUnitProgress = createSelector(runningState, (state) => { - if (state === undefined) { - return undefined; - } - return state.surveyUnitProgress; -}); -const nomenclatureProgress = createSelector(runningState, (state) => { - if (state === undefined) { - return undefined; - } - return state.nomenclatureProgress; -}); -const surveyProgress = createSelector(runningState, (state) => { - if (state === undefined) { - return undefined; - } - return state.surveyProgress; -}); - -export const selectors = { - isRunning, - surveyUnitProgress, - nomenclatureProgress, - surveyProgress, -}; diff --git a/drama-queen/src/core/usecases/downloadData/state.ts b/drama-queen/src/core/usecases/downloadData/state.ts deleted file mode 100644 index 2ba268f7..00000000 --- a/drama-queen/src/core/usecases/downloadData/state.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { id } from "tsafe/id"; -import { assert } from "tsafe/assert"; - -export type State = State.NotRunning | State.Running; - -export namespace State { - export type NotRunning = { - stateDescription: "not running"; - }; - export type Running = { - stateDescription: "running"; - surveyUnitProgress: number; - nomenclatureProgress: number; - surveyProgress: number; - }; -} -export const name = "downloadData"; -export const { reducer, actions } = createSlice({ - name, - initialState: id( - id({ - stateDescription: "not running", - }) - ), - reducers: { - running: () => - id( - id({ - stateDescription: "running", - surveyProgress: 0, - nomenclatureProgress: 0, - surveyUnitProgress: 0, - }) - ), - progressSurveyUnit: ( - state, - { - payload, - }: PayloadAction<{ - surveyUnitProgress: number; - }> - ) => { - const { surveyUnitProgress } = payload; - assert(state.stateDescription === "running"); - return { - ...state, - surveyUnitProgress, - }; - }, - progressSurvey: ( - state, - { - payload, - }: PayloadAction<{ - surveyProgress: number; - }> - ) => { - assert(state.stateDescription === "running"); - const { surveyProgress } = payload; - return { - ...state, - surveyProgress, - }; - }, - progressNomenclature: ( - state, - { - payload, - }: PayloadAction<{ - nomenclatureProgress: number; - }> - ) => { - assert(state.stateDescription === "running"); - - const { nomenclatureProgress } = payload; - return { - ...state, - nomenclatureProgress, - }; - }, - completed: (_state) => { - return { stateDescription: "not running" }; - }, - }, -}); diff --git a/drama-queen/src/core/usecases/downloadData/thunks.ts b/drama-queen/src/core/usecases/downloadData/thunks.ts deleted file mode 100644 index e8469235..00000000 --- a/drama-queen/src/core/usecases/downloadData/thunks.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Thunks } from "core/setup"; -import { actions, name } from "./state"; -import { QueenApi } from "core/ports/QueenApi"; - -export const thunks = { - start: - () => - async (...args) => { - const [dispatch, getState, { queenApi }] = args; - - { - const state = getState()[name]; - - if (state.stateDescription === "running") { - return; - } - } - - dispatch(actions.running()); - - /** - * Pre-requis - */ - const campaigns = await queenApi.getCampaigns(); - - const campaignsIds = campaigns.map(({ id }) => id) ?? []; - const questionnaireIds = [ - ...new Set( - campaigns.map(({ questionnaireIds }) => questionnaireIds).flat() ?? [] - ), - ]; - - /* - * SurveyUnit - */ - - const prSurveyUnit = campaignsIds.map((campaignId) => - queenApi - .getSurveyUnitsIdsAndQuestionnaireIdsByCampaign(campaignId) - .then((arrayOfIds) => - Promise.all(arrayOfIds.map(({ id }) => queenApi.getSurveyUnit(id))) - ) - ); - - const surveyUnitsArrays = (await Promise.all(prSurveyUnit)).flat(); - console.log(surveyUnitsArrays); - - /* - * Survey - */ - - const questionnaire = await Promise.all( - questionnaireIds.map((questionnaireId) => - queenApi.getQuestionnaire(questionnaireId) - ) - ); - - console.log(questionnaire); - /* - * Nomenclature - */ - - const suggestersNames = deduplicate( - questionnaire - .map((q) => q.suggesters) - .flat() - .map((suggester) => suggester?.name) - ); - - // const nomenclatures = await Promise.all( - // suggestersNames.map((nomenclatureId) => - // queenApi.getNomenclature(nomenclatureId) - // ) - // ); - - console.log("ok"); - for (const progress of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - - dispatch( - actions.progressNomenclature({ - nomenclatureProgress: progress, - }) - ); - dispatch( - actions.progressSurvey({ - surveyProgress: progress, - }) - ); - dispatch( - actions.progressSurveyUnit({ - surveyUnitProgress: progress, - }) - ); - } - - dispatch(actions.completed()); - }, -} satisfies Thunks; - -/** - * Remove undefined values from an array and remove duplicates - */ -function deduplicate(items: (T | undefined)[]): T[] { - return [...new Set(items.filter((data) => !!data))] as T[]; -} diff --git a/drama-queen/src/core/usecases/index.ts b/drama-queen/src/core/usecases/index.ts index 8f8d0005..e411cf80 100644 --- a/drama-queen/src/core/usecases/index.ts +++ b/drama-queen/src/core/usecases/index.ts @@ -1,4 +1,6 @@ import * as userAuthentication from "./userAuthentication"; -import * as downloadData from "./downloadData"; -import * as uploadData from "./uploadData"; -export const usecases = { userAuthentication, downloadData, uploadData }; +import * as synchronizeData from "./synchronizeData"; +export const usecases = { + userAuthentication, + synchronizeData, +}; diff --git a/drama-queen/src/core/usecases/synchronizeData/evt.ts b/drama-queen/src/core/usecases/synchronizeData/evt.ts new file mode 100644 index 00000000..1291fe80 --- /dev/null +++ b/drama-queen/src/core/usecases/synchronizeData/evt.ts @@ -0,0 +1,24 @@ +import { CreateEvt } from "core/setup"; +import { Evt } from "evt"; +import { name } from "./state"; + +export const createEvt = (({ evtAction }) => { + const evt = Evt.create<{ + action: "redirect"; + }>(); + + evtAction + .pipe((action) => (action.sliceName === name ? [action] : null)) + .attach( + (action) => + action.actionName === "uploadError" || + action.actionName === "downloadCompleted", + () => { + evt.post({ + action: "redirect", + }); + } + ); + + return evt; +}) satisfies CreateEvt; diff --git a/drama-queen/src/core/usecases/downloadData/index.ts b/drama-queen/src/core/usecases/synchronizeData/index.ts similarity index 100% rename from drama-queen/src/core/usecases/downloadData/index.ts rename to drama-queen/src/core/usecases/synchronizeData/index.ts diff --git a/drama-queen/src/core/usecases/synchronizeData/selectors.ts b/drama-queen/src/core/usecases/synchronizeData/selectors.ts new file mode 100644 index 00000000..f2f1eafa --- /dev/null +++ b/drama-queen/src/core/usecases/synchronizeData/selectors.ts @@ -0,0 +1,78 @@ +import type { State as RootState } from "core/setup"; +import { name } from "./state"; +import { createSelector } from "@reduxjs/toolkit"; + +const state = (rootState: RootState) => rootState[name]; + +const runningState = createSelector(state, (state) => { + if (state.stateDescription === "not running") { + return undefined; + } + return state; +}); + +const uploadingState = createSelector(runningState, (state) => { + if (state === undefined || state.type !== "upload") { + return undefined; + } + return state; +}); + +const downloadingState = createSelector(runningState, (state) => { + if (state === undefined || state.type !== "download") { + return undefined; + } + return state; +}); + +const isRunning = createSelector(runningState, (state) => state !== undefined); + +const isUploading = createSelector( + uploadingState, + (state) => state !== undefined +); + +const isDownloading = createSelector( + downloadingState, + (state) => state !== undefined +); + +const surveyUnitProgress = createSelector(downloadingState, (state) => { + if (state === undefined) { + return undefined; + } + if (state.surveyUnitCompleted === 0 && state.totalSurveyUnit === 0) + return 100; + return (state.surveyUnitCompleted * 100) / state.totalSurveyUnit; +}); +const nomenclatureProgress = createSelector(downloadingState, (state) => { + if (state === undefined) { + return undefined; + } + if (state.nomenclatureCompleted === 0 && state.totalNomenclature === 0) + return 100; + return (state.nomenclatureCompleted * 100) / state.totalNomenclature; +}); +const surveyProgress = createSelector(downloadingState, (state) => { + if (state === undefined) { + return undefined; + } + if (state.surveyCompleted === 0 && state.totalSurvey === 0) return 100; + return (state.surveyCompleted * 100) / state.totalSurvey; +}); + +const uploadProgress = createSelector(uploadingState, (state) => { + if (state === undefined) return undefined; + if (state.total === 0 && state.surveyUnitCompleted === 0) return 100; + return (state.surveyUnitCompleted * 100) / state.total; +}); + +export const selectors = { + isRunning, + isDownloading, + isUploading, + surveyUnitProgress, + nomenclatureProgress, + surveyProgress, + uploadProgress, +}; diff --git a/drama-queen/src/core/usecases/synchronizeData/state.ts b/drama-queen/src/core/usecases/synchronizeData/state.ts new file mode 100644 index 00000000..bffd9f81 --- /dev/null +++ b/drama-queen/src/core/usecases/synchronizeData/state.ts @@ -0,0 +1,130 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { id } from "tsafe/id"; +import { assert } from "tsafe/assert"; + +export type State = State.NotRunning | State.Running; + +export namespace State { + export type NotRunning = { + stateDescription: "not running"; + }; + export type Running = { + stateDescription: "running"; + } & (Uploading | Downloading); + + type Uploading = { + type: "upload"; + total: number; + surveyUnitCompleted: number; + }; + + type Downloading = { + type: "download"; + totalSurveyUnit: number; + surveyUnitCompleted: number; + totalNomenclature: number; + nomenclatureCompleted: number; + totalSurvey: number; + surveyCompleted: number; + }; +} +export const name = "synchronizeData"; + +export const { reducer, actions } = createSlice({ + name, + initialState: id( + id({ + stateDescription: "not running", + }) + ), + reducers: { + runningDownload: () => + id( + id({ + stateDescription: "running", + type: "download", + totalSurveyUnit: Infinity, + surveyUnitCompleted: 0, + totalNomenclature: Infinity, + nomenclatureCompleted: 0, + totalSurvey: Infinity, + surveyCompleted: 0, + }) + ), + runningUpload: () => + id( + id({ + stateDescription: "running", + type: "upload", + total: Infinity, + surveyUnitCompleted: 0, + }) + ), + setDownloadTotalSurveyUnitAndResetSurveyCompleted: ( + state, + { payload }: PayloadAction<{ totalSurveyUnit: number }> + ) => { + const { totalSurveyUnit } = payload; + assert(state.stateDescription === "running" && state.type === "download"); + return { ...state, totalSurveyUnit, surveyCompleted: 0 }; + }, + downloadSurveyUnitCompleted: (state) => { + assert(state.stateDescription === "running" && state.type === "download"); + return { + ...state, + surveyUnitCompleted: state.surveyUnitCompleted + 1, + }; + }, + setDownloadTotalSurvey: ( + state, + { payload }: PayloadAction<{ totalSurvey: number }> + ) => { + const { totalSurvey } = payload; + assert(state.stateDescription === "running" && state.type === "download"); + return { ...state, totalSurvey }; + }, + downloadSurveyCompleted: (state) => { + assert(state.stateDescription === "running" && state.type === "download"); + return { + ...state, + surveyCompleted: state.surveyCompleted + 1, + }; + }, + setDownloadTotalNomenclature: ( + state, + { payload }: PayloadAction<{ totalNomenclature: number }> + ) => { + const { totalNomenclature } = payload; + assert(state.stateDescription === "running" && state.type === "download"); + return { ...state, totalNomenclature }; + }, + downloadNomenclatureCompleted: (state) => { + assert(state.stateDescription === "running" && state.type === "download"); + return { + ...state, + nomenclatureCompleted: state.nomenclatureCompleted + 1, + }; + }, + setUploadTotal: (state, { payload }: PayloadAction<{ total: number }>) => { + const { total } = payload; + assert(state.stateDescription === "running" && state.type === "upload"); + return { ...state, total }; + }, + uploadSurveyUnitCompleted: (state) => { + assert(state.stateDescription === "running" && state.type === "upload"); + return { + ...state, + surveyUnitCompleted: state.surveyUnitCompleted + 1, + }; + }, + uploadError: (_state) => { + return { stateDescription: "not running" }; + }, + uploadCompleted: (_state) => { + return { stateDescription: "not running" }; + }, + downloadCompleted: (state) => { + return state; + }, + }, +}); diff --git a/drama-queen/src/core/usecases/synchronizeData/thunks.ts b/drama-queen/src/core/usecases/synchronizeData/thunks.ts new file mode 100644 index 00000000..472a19bd --- /dev/null +++ b/drama-queen/src/core/usecases/synchronizeData/thunks.ts @@ -0,0 +1,165 @@ +import { Thunks } from "core/setup"; +import { actions, name } from "./state"; + +export const thunks = { + download: + () => + async (...args) => { + const [dispatch, getState, { queenApi, dataStore }] = args; + + { + const state = getState()[name]; + + if (state.stateDescription === "running") { + return; + } + } + + dispatch(actions.runningDownload()); + + /** + * Pre-requis + */ + const campaigns = await queenApi.getCampaigns(); + + const campaignsIds = campaigns.map(({ id }) => id) ?? []; + const questionnaireIds = [ + ...new Set( + campaigns.map(({ questionnaireIds }) => questionnaireIds).flat() ?? [] + ), + ]; + + dispatch( + actions.setDownloadTotalSurvey({ totalSurvey: questionnaireIds.length }) + ); + + /* + * SurveyUnit + */ + + const prSurveyUnit = campaignsIds.map((campaignId) => + queenApi + .getSurveyUnitsIdsAndQuestionnaireIdsByCampaign(campaignId) + .then((arrayOfIds) => { + dispatch( + actions.setDownloadTotalSurveyUnitAndResetSurveyCompleted({ + totalSurveyUnit: arrayOfIds.length, + }) + ); + return Promise.all( + arrayOfIds.map(({ id }) => + queenApi + .getSurveyUnit(id) + .then((surveyUnit) => dataStore.updateSurveyUnit(surveyUnit)) + .finally(() => + dispatch(actions.downloadSurveyUnitCompleted()) + ) + ) + ); + }) + ); + + const surveyUnitsArrays = (await Promise.all(prSurveyUnit)).flat(); + + /* + * Survey + */ + + const questionnaires = await Promise.all( + questionnaireIds.map((questionnaireId) => + queenApi.getQuestionnaire(questionnaireId).then((questionnaire) => { + dispatch(actions.downloadSurveyCompleted()); + return questionnaire; + }) + ) + ); + + /* + * Nomenclature + */ + + const suggestersNames = deduplicate( + questionnaires + .map((q) => q.suggesters) + .flat() + .map((suggester) => suggester?.name) + ); + + dispatch( + actions.setDownloadTotalNomenclature({ + totalNomenclature: suggestersNames.length, + }) + ); + + //We don't store the data, but instead, we simply initiate the request for the service worker to cache the response + await Promise.all( + suggestersNames.map((nomenclatureId) => + queenApi + .getNomenclature(nomenclatureId) + .catch((error) => { + //TODO Handle Errors + console.log(error); + }) + .finally(() => dispatch(actions.downloadNomenclatureCompleted())) + ) + ); + + dispatch(actions.downloadCompleted()); + }, + upload: + () => + async (...args) => { + const [dispatch, getState, { dataStore, queenApi }] = args; + + { + const state = getState()[name]; + + if (state.stateDescription === "running") { + return; + } + } + + dispatch(actions.runningUpload()); + + try { + const prSurveyUnits = dataStore.getAllSurveyUnits(); + const surveyUnits = await prSurveyUnits; + + if (!surveyUnits) { + return; + } + + dispatch(actions.setUploadTotal({ total: surveyUnits.length ?? 0 })); + + const surveyUnitPromises = surveyUnits.map((surveyUnit) => + queenApi + .putSurveyUnit(surveyUnit.id, surveyUnit) + .then(() => dataStore.deleteSurveyUnit(surveyUnit.id)) + .then(() => dispatch(actions.uploadSurveyUnitCompleted())) + .catch((error) => { + // TODO: Handle the error as needed -> Save LocalStorage + console.error(error); + dispatch(actions.uploadError()); + }) + ); + + await Promise.all(surveyUnitPromises); + } catch (error) { + // TODO : Handle errors from prSurveyUnits + console.error(error); + dispatch(actions.uploadError()); + } finally { + // Go to download + console.log("finally"); + dispatch(actions.uploadCompleted()); + dispatch(thunks.download()); + } + }, +} satisfies Thunks; + +/** + * Remove undefined values from an array and remove duplicates + */ +function deduplicate(items: (T | undefined)[]): T[] { + return [...new Set(items.filter((data) => !!data))] as T[]; +} diff --git a/drama-queen/src/core/usecases/uploadData/evt.ts b/drama-queen/src/core/usecases/uploadData/evt.ts deleted file mode 100644 index bbb91f03..00000000 --- a/drama-queen/src/core/usecases/uploadData/evt.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CreateEvt } from "core/setup"; -import { Evt } from "evt"; -import { name } from "./state"; - -export const createEvt = (({ evtAction }) => { - const evt = Evt.create<{ - action: "redirect"; - }>(); - - evtAction - .pipe((action) => - action.sliceName === name && action.actionName === "completed" - ? [action] - : null - ) - .attach(() => { - evt.post({ - action: "redirect", - }); - }); - - return evt; -}) satisfies CreateEvt; diff --git a/drama-queen/src/core/usecases/uploadData/index.ts b/drama-queen/src/core/usecases/uploadData/index.ts deleted file mode 100644 index a6ef5d73..00000000 --- a/drama-queen/src/core/usecases/uploadData/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./state"; -export * from "./selectors"; -export * from "./thunks"; -export * from "./evt"; diff --git a/drama-queen/src/core/usecases/uploadData/selectors.ts b/drama-queen/src/core/usecases/uploadData/selectors.ts deleted file mode 100644 index abfd97f2..00000000 --- a/drama-queen/src/core/usecases/uploadData/selectors.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { State as RootState } from "core/setup"; -import { name } from "./state"; -import { createSelector } from "@reduxjs/toolkit"; - -const state = (rootState: RootState) => rootState[name]; - -const runningState = createSelector(state, (state) => { - if (state.stateDescription === "not running") { - return undefined; - } - return state; -}); - -const isRunning = createSelector(runningState, (state) => state !== undefined); - -const uploadProgress = createSelector(runningState, (state) => { - if (state === undefined) return undefined; - return state.uploadProgress; -}); - -export const selectors = { - isRunning, - uploadProgress, -}; diff --git a/drama-queen/src/core/usecases/uploadData/state.ts b/drama-queen/src/core/usecases/uploadData/state.ts deleted file mode 100644 index e797a7d6..00000000 --- a/drama-queen/src/core/usecases/uploadData/state.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { id } from "tsafe"; - -export type State = State.NotRunning | State.Uploading; - -export namespace State { - export type NotRunning = { - stateDescription: "not running"; - }; - - export type Uploading = { - stateDescription: "running"; - uploadProgress: number; - }; -} - -export const name = "uploadData"; - -export const { reducer, actions } = createSlice({ - name, - initialState: id( - id({ - stateDescription: "not running", - }) - ), - reducers: { - progress: ( - _state, - { - payload, - }: PayloadAction<{ - uploadProgress: number; - }> - ) => { - const { uploadProgress } = payload; - return { - stateDescription: "running", - uploadProgress, - }; - }, - completed: (_state) => { - return { stateDescription: "not running" }; - }, - }, -}); diff --git a/drama-queen/src/core/usecases/uploadData/thunks.ts b/drama-queen/src/core/usecases/uploadData/thunks.ts deleted file mode 100644 index 9fb59a65..00000000 --- a/drama-queen/src/core/usecases/uploadData/thunks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Thunks } from "core/setup"; -import { actions, name, type State } from "./state"; - -export const thunks = { - start: - () => - async (...args) => { - const [dispatch, getState] = args; - - { - const state = getState()[name]; - - if (state.stateDescription === "running") { - return; - } - } - - dispatch( - actions.progress({ - uploadProgress: 0, - }) - ); - - for (const progress of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - - dispatch( - actions.progress({ - uploadProgress: progress, - }) - ); - } - - dispatch(actions.completed()); - }, -} satisfies Thunks; diff --git a/drama-queen/src/ui/pages/synchronize/DownloadData.tsx b/drama-queen/src/ui/pages/synchronize/DownloadData.tsx deleted file mode 100644 index 3651cb29..00000000 --- a/drama-queen/src/ui/pages/synchronize/DownloadData.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, Fragment } from "react"; -import { useCoreState, useCoreFunctions, useCoreEvts, selectors } from "core"; -import { useEvt } from "evt/hooks" -import { useTranslate } from "hooks/useTranslate"; -import { LoadingDisplay } from "./LoadingDisplay"; - - -export function DownloadPage() { - const { __ } = useTranslate(); - - /* refactor this when redux-clean-archi updated */ - const { isRunning: showProgress } = useCoreState(selectors.downloadData.isRunning); - const { nomenclatureProgress } = useCoreState(selectors.downloadData.nomenclatureProgress); - const { surveyProgress } = useCoreState(selectors.downloadData.surveyProgress); - const { surveyUnitProgress } = useCoreState(selectors.downloadData.surveyUnitProgress) - - const { downloadData } = useCoreFunctions(); - - useEffect( - () => { - downloadData.start(); - }, [] - ); - - const { evtDownloadData } = useCoreEvts(); - - useEvt( - ctx => { - evtDownloadData.$attach( - data => data.action === "redirect" ? [data] : null, - ctx, - () => { - console.log("redirect to " + window.location.href) - } - ); - }, - [] - ); - - if (!showProgress) { - return null; - } - - const progressBars = [{ - progress: surveyProgress, - label: __('sync.download.questionnaires') - }, - { - progress: nomenclatureProgress, - label: __('sync.download.nomenclatures') - }, - { - progress: surveyUnitProgress, - label: __('sync.download.surveyUnits') - }].filter(bar => bar.progress !== undefined) - - return ( - <> - - - - ) -} diff --git a/drama-queen/src/ui/pages/synchronize/LoadingDisplay.tsx b/drama-queen/src/ui/pages/synchronize/LoadingDisplay.tsx index 75b7296a..1946df64 100644 --- a/drama-queen/src/ui/pages/synchronize/LoadingDisplay.tsx +++ b/drama-queen/src/ui/pages/synchronize/LoadingDisplay.tsx @@ -15,9 +15,9 @@ type LoadingDisplayProps = { } export function LoadingDisplay(props: LoadingDisplayProps) { + const { syncStepTitle, progressBars } = props const { __ } = useTranslate(); const { classes } = useStyles(); - const { syncStepTitle, progressBars } = props return ( @@ -26,7 +26,7 @@ export function LoadingDisplay(props: LoadingDisplayProps) { {progressBars.map(bar => - + {bar.label} diff --git a/drama-queen/src/ui/pages/synchronize/SynchronizeData.tsx b/drama-queen/src/ui/pages/synchronize/SynchronizeData.tsx new file mode 100644 index 00000000..7f6c8917 --- /dev/null +++ b/drama-queen/src/ui/pages/synchronize/SynchronizeData.tsx @@ -0,0 +1,72 @@ +import { useEffect } from "react"; +import { useCoreState, useCoreFunctions, useCoreEvts, selectors } from "core"; +import { useEvt } from "evt/hooks" +import { useTranslate } from "hooks/useTranslate"; +import { LoadingDisplay } from "./LoadingDisplay"; + +export function SynchronizeData() { + const { __ } = useTranslate(); + + /* refactor this when redux-clean-archi updated */ + const { isRunning: showProgress } = useCoreState(selectors.synchronizeData.isRunning); + + const { isUploading } = useCoreState(selectors.synchronizeData.isUploading) + const { uploadProgress } = useCoreState(selectors.synchronizeData.uploadProgress); + const { isDownloading } = useCoreState(selectors.synchronizeData.isDownloading) + const { nomenclatureProgress } = useCoreState(selectors.synchronizeData.nomenclatureProgress); + const { surveyProgress } = useCoreState(selectors.synchronizeData.surveyProgress); + const { surveyUnitProgress } = useCoreState(selectors.synchronizeData.surveyUnitProgress) + + + const { synchronizeData } = useCoreFunctions(); + + useEffect(() => { + console.log("showProgress", showProgress) + }, [showProgress]); + + useEffect( + () => { + synchronizeData.upload(); + }, [] + ); + + const { evtSynchronizeData } = useCoreEvts(); + + useEvt( + ctx => { + evtSynchronizeData.$attach( + data => data.action === "redirect" ? [data] : null, ctx, () => { + console.log("we should redirect to ", window.location.href); + } + ) + }, + [] + ); + + if (!showProgress) { + return null; + } + + const uploadProgressBars = [{ + progress: uploadProgress, + }]; + + const progressBars = [{ + progress: surveyProgress, + label: __('sync.download.questionnaires') + }, + { + progress: nomenclatureProgress, + label: __('sync.download.nomenclatures') + }, + { + progress: surveyUnitProgress, + label: __('sync.download.surveyUnits') + }].filter(bar => bar.progress !== undefined); + + return ( + <> + {isUploading && } + {isDownloading && } + ) +} \ No newline at end of file diff --git a/drama-queen/src/ui/pages/synchronize/UploadData.tsx b/drama-queen/src/ui/pages/synchronize/UploadData.tsx deleted file mode 100644 index f90ed294..00000000 --- a/drama-queen/src/ui/pages/synchronize/UploadData.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, Fragment } from "react"; -import LinearProgress from '@mui/material/LinearProgress'; -import Typography from '@mui/material/Typography'; -import Stack from '@mui/material/Stack'; -import { useCoreState, useCoreFunctions, useCoreEvts, selectors } from "core"; -import { useEvt } from "evt/hooks" -import { tss } from "tss-react/mui"; -import { useTranslate } from "hooks/useTranslate"; -import { LoadingDisplay } from "./LoadingDisplay"; - -export function UploadData() { - const { __ } = useTranslate(); - const { classes } = useStyles(); - - /* refactor this when redux-clean-archi updated */ - const { isRunning: showProgress } = useCoreState(selectors.uploadData.isRunning); - const { uploadProgress } = useCoreState(selectors.uploadData.uploadProgress); - - const { uploadData } = useCoreFunctions(); - - useEffect( - () => { - uploadData.start(); - }, [] - ); - - const { evtUploadData } = useCoreEvts(); - - useEvt( - ctx => { - evtUploadData.$attach( - data => data.action === "redirect" ? [data] : null, - ctx, - () => { - console.log("redirect to " + window.location.href) - } - ); - }, - [] - ); - - if (!showProgress) { - return null; - } - - const progressBars = [{ - progress: uploadProgress, - }] - .filter(bar => bar.progress !== null) - return ( - <> - - - - ) -} - - -const useStyles = tss - .create(() => ({ - lightText: { - opacity: .75, - }, - spinner: { - width: 200, - height: 200 - }, - progressBar: { - maxWidth: 700, - width: '80vw', - height: 10, - borderRadius: 10 - } - })); diff --git a/drama-queen/src/ui/routing/routes.tsx b/drama-queen/src/ui/routing/routes.tsx index 6f1be918..161807fc 100644 --- a/drama-queen/src/ui/routing/routes.tsx +++ b/drama-queen/src/ui/routing/routes.tsx @@ -5,8 +5,7 @@ import type { RouteObject } from "react-router-dom"; import { SynchronizePage as SynchronizeOld } from "ui/pages/synchronize-old/SynchronizePage"; import { SurveyMapping } from "ui/pages/queenMapping/SuryveyMapping"; import { RequiresAuthentication } from "ui/auth"; -import { DownloadPage } from "ui/pages/synchronize/DownloadData"; -import { UploadData } from "ui/pages/synchronize/UploadData"; +import { SynchronizeData } from "ui/pages/synchronize/SynchronizeData"; //ReadOnly path is a bad pattern must be change (affects pearl,moog,queen) export const routes: RouteObject[] = [ @@ -32,6 +31,6 @@ export const routes: RouteObject[] = [ }, { path: "/synchronize", - element: + element: } ] \ No newline at end of file