diff --git a/api/src/controllers/consommation.js b/api/src/controllers/consommation.js index e7ce65c31..495bcea2d 100644 --- a/api/src/controllers/consommation.js +++ b/api/src/controllers/consommation.js @@ -308,4 +308,21 @@ router.get( }) ); +router.post( + "/find-missing-own-drink", + catchErrors(async (req, res) => { + const { drinkKey, matomoId } = req.body; + if (!matomoId) return res.status(400).json({ ok: false, error: "no matomo id" }); + + // find user with matomoId + let user = await prisma.user.findUnique({ where: { matomo_id: matomoId } }); + + const conso = await prisma.consommation.findFirst({ + where: { userId: user.id, drinkKey }, + }); + + return res.status(200).send({ ok: true, data: conso }); + }) +); + module.exports = router; diff --git a/expo/App.jsx b/expo/App.jsx index 1918367c9..23ba7f8fd 100644 --- a/expo/App.jsx +++ b/expo/App.jsx @@ -2,7 +2,7 @@ import "react-native-get-random-values"; import React, { useEffect, useState } from "react"; import * as Sentry from "@sentry/react-native"; import { SafeAreaProvider } from "react-native-safe-area-context"; -import { RecoilRoot } from "recoil"; +import { RecoilRoot, useSetRecoilState } from "recoil"; import dayjs from "dayjs"; import * as SplashScreen from "expo-splash-screen"; import * as Application from "expo-application"; @@ -26,7 +26,10 @@ import { migrateMissingDrinkKey, sendPreviousDrinksToDB, } from "./src/migrations"; -import { reconciliateDrinksToDB, reconciliateGoalToDB } from "./src/reconciliations"; +import { fixMissingDrinkKey, reconciliateDrinksToDB, reconciliateGoalToDB } from "./src/reconciliations"; +import { drinksState, ownDrinksCatalogState } from "./src/recoil/consos"; +import { drinksByWeekState, goalsState } from "./src/recoil/gains"; +import { getInitValueFromStorage } from "./src/recoil/utils"; dayjs.locale("fr"); dayjs.extend(isSameOrAfter); @@ -34,8 +37,7 @@ dayjs.extend(weekday); SplashScreen.preventAutoHideAsync(); -const release = - getBundleId() + "@" + Application.nativeApplicationVersion + "+" + Application.nativeBuildVersion; // ex : com.addicto.v1@1.18.0+198 +const release = getBundleId() + "@" + Application.nativeApplicationVersion + "+" + Application.nativeBuildVersion; // ex : com.addicto.v1@1.18.0+198 Sentry.init({ dsn: __DEV__ ? "" : "https://0ef6896e639948fd9ba54b861186360d@sentry.fabrique.social.gouv.fr/80", @@ -46,15 +48,14 @@ Sentry.init({ const App = () => { // sync everytime we open the app const [reconciliatedDrinksToDB, setReconciliatedDrinksToDB] = useState(false); + const [fixedMissingDrinkKey, setFixedMissingDrinkKey] = useState(false); const [reconciliatedGoalsToDB, setReconciliatedGoalsToDB] = useState(false); // migrate only once if not yet done // TODO: clean migrations when it's time - const [_hasSentPreviousDrinksToDB, setHasSentPreviousDrinksToDB] = - useState(hasSentPreviousDrinksToDB); + const [_hasSentPreviousDrinksToDB, setHasSentPreviousDrinksToDB] = useState(hasSentPreviousDrinksToDB); const [_hasCleanConsoAndCatalog, setHasCleanConsoAndCatalog] = useState(hasCleanConsoAndCatalog); - const [_hasMigrateMissingDrinkKey, sethasMigrateMissingDrinkKey] = - useState(hasMigrateMissingDrinkKey); + const [_hasMigrateMissingDrinkKey, sethasMigrateMissingDrinkKey] = useState(hasMigrateMissingDrinkKey); const [_hasMigrateFromDailyGoalToWeekly, sethasMigrateFromDailyGoalToWeekly] = useState( hasMigrateFromDailyGoalToWeekly ); @@ -65,6 +66,10 @@ const App = () => { await reconciliateDrinksToDB(); setReconciliatedDrinksToDB(true); } + if (!fixedMissingDrinkKey) { + await fixMissingDrinkKey(); + setFixedMissingDrinkKey(true); + } if (!reconciliatedGoalsToDB) { await reconciliateGoalToDB(); setReconciliatedGoalsToDB(true); @@ -92,6 +97,7 @@ const App = () => { if ( !reconciliatedDrinksToDB || + !fixedMissingDrinkKey || !reconciliatedGoalsToDB || !_hasSentPreviousDrinksToDB || !_hasCleanConsoAndCatalog || @@ -101,8 +107,13 @@ const App = () => { return null; } + return ; +}; + +function RecoiledApp() { return ( + @@ -110,6 +121,24 @@ const App = () => { ); -}; +} + +// Why this function ? +// because we have a FUCKING HARD TIME to manage how and when recoil is initiated +// the default value of recoil's atoms is called AT FIRST, before any migration or reconciliation +// so we need to re-init the atoms we want to be initiated once the migrations/reconciliations are done +function ResetRecoilStatesAfterMigrationsAndReconciliations() { + const resetOwnDrinks = useSetRecoilState(ownDrinksCatalogState); + const resetDrinks = useSetRecoilState(drinksState); + const resetDrinksByWeek = useSetRecoilState(drinksByWeekState); + const resetGoals = useSetRecoilState(goalsState); + useEffect(() => { + resetOwnDrinks(getInitValueFromStorage("@OwnDrinks", [])); + resetDrinks(getInitValueFromStorage("@Drinks", [])); + resetDrinksByWeek(getInitValueFromStorage("@StoredDetaileddrinksByWeekState", [])); + resetGoals(getInitValueFromStorage("goalsState", [])); + }, []); + return null; +} export default Sentry.wrap(App); diff --git a/expo/src/Router.js b/expo/src/Router.js index b0a9e8253..429be9e4c 100644 --- a/expo/src/Router.js +++ b/expo/src/Router.js @@ -71,6 +71,7 @@ const TabsNavigator = ({ navigation }) => { const showBootSplash = useRecoilValue(showBootSplashState); const [isInCraving, setIsInCraving] = useRecoilState(isInCravingKeyState); + return ( <> { +const DrinkPersonalisation = ({ updateDrinkKey, hide, quantitySelected, setQuantitySelected, setLocalDrinksState }) => { const route = useRoute(); const timestamp = route?.params?.timestamp; @@ -49,6 +43,7 @@ const DrinkPersonalisation = ({ const [isUpdateWanted, setIsUpdateWanted] = useState(true); const volumeNumber = quantitySelected?.volume ?? drink?.volume.split(" ")[0]; const saveDrink = async () => { + console.log("SAVE DRINK"); const formatedPrice = drinkPrice.replace(",", "."); const formatedAlcoolPercentage = drinkAlcoolPercentage.replace(",", "."); const formatedVolume = volumeNumber.replace(",", "."); @@ -58,8 +53,7 @@ const DrinkPersonalisation = ({ (catalogDrink) => catalogDrink.drinkKey === drinkName && catalogDrink.isDeleted === false ) ?? ownDrinksCatalog.find( - (catalogDrink) => - catalogDrink.drinkKey === updateDrinkKey && catalogDrink.isDeleted === false + (catalogDrink) => catalogDrink.drinkKey === updateDrinkKey && catalogDrink.isDeleted === false ); const kCal = ((formatedAlcoolPercentage * 0.8 * formatedVolume) / 10) * 7; const doses = Math.round((formatedAlcoolPercentage * 0.8 * formatedVolume) / 10) / 10; @@ -102,25 +96,29 @@ const DrinkPersonalisation = ({ if (!keepGoing) return; } newDrink.icon = quantitySelected?.icon ?? oldDrink.icon; - setOwnDrinksCatalog((oldState) => { - const tempState = [...oldState, newDrink]; - return tempState; - }); - setGlobalDrinksState((oldState) => { - return oldState.map((oldStateDrink) => - oldStateDrink.drinkKey === oldDrink.drinkKey - ? { ...oldStateDrink, drinkKey: drinkName } - : oldStateDrink - ); - }); - setOwnDrinksCatalog((tempState) => { - const cleanedNewState = tempState - .filter((tempStateDrink) => tempStateDrink.drinkKey !== newDrink.drinkKey) - .map((oldStateDrink) => + if (oldDrink.drinkKey === newDrink.drinkKey) { + setOwnDrinksCatalog((oldState) => { + return oldState.map((oldStateDrink) => oldStateDrink.drinkKey === oldDrink.drinkKey ? newDrink : oldStateDrink ); - return cleanedNewState; - }); + }); + } else { + setOwnDrinksCatalog((oldState) => { + const tempState = [...oldState, newDrink]; + return tempState; + }); + setGlobalDrinksState((oldState) => { + return oldState.map((oldStateDrink) => + oldStateDrink.drinkKey === oldDrink.drinkKey ? { ...oldStateDrink, drinkKey: drinkName } : oldStateDrink + ); + }); + setOwnDrinksCatalog((tempState) => { + const cleanedNewState = tempState + .filter((tempStateDrink) => tempStateDrink.drinkKey !== newDrink.drinkKey) + .map((oldStateDrink) => (oldStateDrink.drinkKey === oldDrink.drinkKey ? newDrink : oldStateDrink)); + return cleanedNewState; + }); + } const matomoId = storage.getString("@UserIdv2"); @@ -214,14 +212,16 @@ const DrinkPersonalisation = ({ {!quantitySelected?.volume && !drink ? ( setShowQuantityModal(true)}> + onPress={() => setShowQuantityModal(true)} + > Sélectionnez une quantité ) : ( setShowQuantityModal(true)}> + onPress={() => setShowQuantityModal(true)} + > {volumeNumber} )} @@ -281,10 +281,9 @@ const DrinkPersonalisation = ({ onPress={() => { setIsUpdateWanted(false); setShowModalUpdate(true); - }}> - - Supprimer ma boisson - + }} + > + Supprimer ma boisson )} diff --git a/expo/src/migrations.js b/expo/src/migrations.js index 2ce6b24b4..1c14b2c7c 100644 --- a/expo/src/migrations.js +++ b/expo/src/migrations.js @@ -143,8 +143,8 @@ function cleanCatalog(oldDrinkCatalog) { const alcoolPercentage = Number(oldDrink.categoryKey.split("-")[2]) ? Number(oldDrink.categoryKey.split("-")[2]) : oldDrink.alcoolPercentage - ? String(oldDrink.alcoolPercentage).replace(",", ".") - : 5; + ? String(oldDrink.alcoolPercentage).replace(",", ".") + : 5; // 4. create kcal and doses if they don't exist const kcal = Math.round(((Number(alcoolPercentage) * 0.8 * volume) / 10) * 7); @@ -172,8 +172,7 @@ function cleanCatalog(oldDrinkCatalog) { : "ownDrink"; // 9. we fix the bug of the new cocktails - const categoryKeyEvolution2 = - drinkKeyEvolution1 === "ownCocktail" ? "ownCocktail" : categoryKeyEvolution1; + const categoryKeyEvolution2 = drinkKeyEvolution1 === "ownCocktail" ? "ownCocktail" : categoryKeyEvolution1; newOwnDrinksCatalog.push({ drinkKey: drinkKeyEvolution2, @@ -193,9 +192,7 @@ function cleanCatalog(oldDrinkCatalog) { return newOwnDrinksCatalog; } -export const hasMigrateFromDailyGoalToWeekly = storage.getBoolean( - "hasMigrateFromDailyGoalToWeekly" -); +export const hasMigrateFromDailyGoalToWeekly = storage.getBoolean("hasMigrateFromDailyGoalToWeekly"); export async function migrateFromDailyGoalToWeekly() { try { const drinksByDrinkingDayString = storage.getString("@StoredDetailedDrinksByDrinkingDay"); @@ -216,9 +213,7 @@ export async function migrateFromDailyGoalToWeekly() { capture(e, { extra: { migration: "hasMigrateFromDailyGoalToWeekly", - "@StoredDetailedDrinksByDrinkingDay": storage.getString( - "@StoredDetailedDrinksByDrinkingDay" - ), + "@StoredDetailedDrinksByDrinkingDay": storage.getString("@StoredDetailedDrinksByDrinkingDay"), "@DaysWithGoalNoDrink": storage.getString("@DaysWithGoalNoDrink"), }, user: { diff --git a/expo/src/recoil/consos.js b/expo/src/recoil/consos.js index 08da18ece..7f7cb9e14 100644 --- a/expo/src/recoil/consos.js +++ b/expo/src/recoil/consos.js @@ -16,10 +16,7 @@ export const drinksState = atom({ onSet((newValue) => { storage.set("@Drinks", JSON.stringify(newValue)); Sentry.setExtra("drinks", newValue.slice(0, 50)); - Sentry.setExtra( - "all-drinks", - newValue.map(({ drinkKey, id }) => `${drinkKey}_${id}`).join("__") - ); + Sentry.setExtra("all-drinks", newValue.map(({ drinkKey, id }) => `${drinkKey}_${id}`).join("__")); }), ], }); diff --git a/expo/src/reconciliations.js b/expo/src/reconciliations.js index 3e1bdb5e8..554a2e5b9 100644 --- a/expo/src/reconciliations.js +++ b/expo/src/reconciliations.js @@ -1,4 +1,5 @@ import { getMaxDrinksPerWeek, getTotalDrinksByDrinkingDay } from "./helpers/gainsHelpers"; +import { alcoolQuantityCatalog } from "./scenes/AddDrink/alcoolQuantityCatalog"; import { drinksCatalog } from "./scenes/ConsoFollowUp/drinksCatalog"; import API from "./services/api"; import { capture } from "./services/sentry"; @@ -26,10 +27,7 @@ export async function reconciliateDrinksToDB() { }, }).then((response) => { if (response?.ok) { - storage.set( - "@Drinks", - JSON.stringify(drinks.map((drink) => ({ ...drink, isSyncedWithDB: true }))) - ); + storage.set("@Drinks", JSON.stringify(drinks.map((drink) => ({ ...drink, isSyncedWithDB: true })))); } }); } catch (e) { @@ -58,10 +56,7 @@ export async function reconciliateGoalToDB() { const daysWithGoalNoDrink = JSON.parse(storage.getString("@DaysWithGoalNoDrink") || "[]"); const drinksByWeek = JSON.parse(storage.getString("@StoredDetaileddrinksByWeekState") || "[]"); const maxDrinksPerWeek = getMaxDrinksPerWeek(drinksByWeek); - const totalDrinksByDrinkingDay = getTotalDrinksByDrinkingDay( - maxDrinksPerWeek, - daysWithGoalNoDrink - ); + const totalDrinksByDrinkingDay = getTotalDrinksByDrinkingDay(maxDrinksPerWeek, daysWithGoalNoDrink); await API.post({ path: "/goal/sync", @@ -90,3 +85,46 @@ export async function reconciliateGoalToDB() { }); } } + +export async function fixMissingDrinkKey() { + const drinks = JSON.parse(storage.getString("@Drinks")); + const ownDrinksCatalog = JSON.parse(storage.getString("@OwnDrinks") || "[]"); + const objectCatalog = {}; + for (const ownDrink of ownDrinksCatalog) { + objectCatalog[ownDrink.drinkKey] = ownDrink; + } + for (const catalogDrink of drinksCatalog) { + objectCatalog[catalogDrink.drinkKey] = catalogDrink; + } + for (const drink of drinks) { + if (!objectCatalog[drink.drinkKey]) { + const response = await API.post({ + path: "/consommation/find-missing-own-drink", + body: { + drinkKey: drink.drinkKey, + matomoId: storage.getString("@UserIdv2"), + }, + }); + if (response.ok && response.data) { + const missingDrink = { + categoryKey: "ownDrink", + drinkKey: drink.drinkKey, + displayFeed: drink.drinkKey, + displayDrinkModal: drink.drinkKey, + volume: response.data.volume, + price: Number(response.data.price), + doses: response.data.doses, + icon: alcoolQuantityCatalog.find((catalog) => catalog.volume === response.data.volume)?.icon, + // const doses = Math.round((formatedAlcoolPercentage * 0.8 * formatedVolume) / 10) / 10; + alcoolPercentage: (response.data.doses * 10 * 10) / (response.data.volume * 0.8), + kcal: response.data.kcal, + custom: true, + isDeleted: false, + }; + ownDrinksCatalog.push(missingDrink); + objectCatalog[drink.drinkKey] = missingDrink; + storage.set("@OwnDrinks", JSON.stringify(ownDrinksCatalog)); + } + } + } +} diff --git a/expo/src/scenes/ConsoFollowUp/ConsoFeedDisplay.js b/expo/src/scenes/ConsoFollowUp/ConsoFeedDisplay.js index 28e9026b3..e68a37354 100644 --- a/expo/src/scenes/ConsoFollowUp/ConsoFeedDisplay.js +++ b/expo/src/scenes/ConsoFollowUp/ConsoFeedDisplay.js @@ -48,9 +48,7 @@ const ConsoFeedDisplay = ({ ({drink.volume}) )} - {(isFirst(position) || isAlone(position)) && ( - {new Date(timestamp).getLocaleTime("fr")} - )} + {(isFirst(position) || isAlone(position)) && {new Date(timestamp).getLocaleTime("fr")}} ) : ( <> @@ -59,9 +57,7 @@ const ConsoFeedDisplay = ({ {quantity} {"Boisson inconnue"}{" "} - {(isFirst(position) || isAlone(position)) && ( - {new Date(timestamp).getLocaleTime("fr")} - )} + {(isFirst(position) || isAlone(position)) && {new Date(timestamp).getLocaleTime("fr")}} )} @@ -70,13 +66,7 @@ const ConsoFeedDisplay = ({ {showButtons && ( - + )} diff --git a/expo/src/scenes/ConsoFollowUp/drinksCatalog.js b/expo/src/scenes/ConsoFollowUp/drinksCatalog.js index dd14a6878..fb74fee2c 100644 --- a/expo/src/scenes/ConsoFollowUp/drinksCatalog.js +++ b/expo/src/scenes/ConsoFollowUp/drinksCatalog.js @@ -69,7 +69,7 @@ export const mapDrinkToDose = ({ drinkKey, quantity }, catalogObject) => { if (drinkKey === NO_CONSO) return 0; const drink = catalogObject[drinkKey]; if (!drink) { - capture(new Error("drink really not found"), { + capture(new Error("drink really really not found"), { extra: { drinkKey, catalogObject, function: "mapDrinkToDose" }, tags: { drinkKey }, }); @@ -83,7 +83,7 @@ export const mapDrinkToKcals = ({ drinkKey, quantity }, catalogObject) => { if (drinkKey === NO_CONSO) return 0; const drink = catalogObject[drinkKey]; if (!drink) { - capture(new Error("drink really not found"), { + capture(new Error("drink really really not found"), { extra: { drinkKey, catalogObject, function: "mapDrinkToKcals" }, tags: { drinkKey }, }); @@ -97,7 +97,7 @@ export const mapDrinkToPrice = ({ drinkKey, quantity }, catalogObject) => { if (drinkKey === NO_CONSO) return 0; const drink = catalogObject[drinkKey]; if (!drink) { - capture(new Error("drink really not found"), { + capture(new Error("drink really really not found"), { extra: { drinkKey, catalogObject, function: "mapDrinkToPrice" }, tags: { drinkKey }, }); @@ -111,7 +111,7 @@ export const getDisplayName = (drinkKey, quantity, catalogObject) => { try { const drink = catalogObject[drinkKey]; if (!drink) { - capture(new Error("drink really not found"), { + capture(new Error("drink really really not found"), { extra: { drinkKey, catalogObject, function: "getDisplayName" }, tags: { drinkKey }, }); @@ -131,7 +131,7 @@ export const getDisplayDrinksModalName = (drinkKey, catalogObject, quantity = 1) try { const drink = catalogObject[drinkKey]; if (!drink) { - capture(new Error("drink really not found"), { + capture(new Error("drink really really not found"), { extra: { drinkKey, catalogObject, function: "getDisplayDrinksModalName" }, tags: { drinkKey }, }); @@ -151,7 +151,7 @@ export const getDisplayDrinksModalName = (drinkKey, catalogObject, quantity = 1) export const getIcon = (iconName) => { const icon = mapIconNameToIcon[iconName]; if (!icon) { - capture(new Error("icon not found"), { extra: { iconName, function: "getIcon" } }); + capture(new Error("icon really really not found"), { extra: { iconName, function: "getIcon" } }); return HalfBeer; } return icon;