diff --git a/src/components/Restaurant/AccountButton.tsx b/src/components/Restaurant/AccountButton.tsx index baed571c9..1cfdaf701 100644 --- a/src/components/Restaurant/AccountButton.tsx +++ b/src/components/Restaurant/AccountButton.tsx @@ -22,66 +22,8 @@ interface AccountButtonProps { } const AccountButton: React.FC = ({ account, isSelected, onPress, colors }) => { - const COLLAPSED_WIDTH = 47; - const [textMeasurement, setTextMeasurement] = React.useState(0); - const EXPANDED_WIDTH = React.useMemo(() => COLLAPSED_WIDTH + textMeasurement + 16, [textMeasurement]); // 16 pour le gap - - const width = useSharedValue(isSelected ? EXPANDED_WIDTH : COLLAPSED_WIDTH); - const textOpacity = useSharedValue(isSelected ? 1 : 0); - const textScale = useSharedValue(isSelected ? 1 : 0.7); - const textWidth = useSharedValue(isSelected ? textMeasurement : 0); - - const springConfig = { - damping: 15, - mass: 0.5, - stiffness: 120, - }; - - const animatedContainerStyle = useAnimatedStyle(() => ({ - width: withSpring(width.value, springConfig), - })); - - const animatedTextContainerStyle = useAnimatedStyle(() => ({ - width: withTiming(textWidth.value, { - duration: 150, - easing: Easing.bezier(0.4, 0.0, 0.2, 1), - }), - })); - - const animatedTextStyle = useAnimatedStyle(() => ({ - opacity: withTiming(textOpacity.value, { - duration: 150, - easing: Easing.ease, - }), - transform: [{ scale: withTiming(textScale.value, { duration: 150 }) }], - })); - - React.useEffect(() => { - if (textMeasurement > 0) { - width.value = isSelected ? EXPANDED_WIDTH : COLLAPSED_WIDTH; - textOpacity.value = isSelected ? 1 : 0; - textScale.value = isSelected ? 1 : 0.7; - textWidth.value = isSelected ? textMeasurement : 0; - } - }, [isSelected, textMeasurement]); - return ( <> - {/* Composant de mesure invisible */} - { - setTextMeasurement(width); - }} - > - {account.label} - - = ({ account, isSelected, onPr alignItems: "center", justifyContent: "center", backgroundColor: isSelected ? colors.primary + "20" : colors.card, - borderColor: colors.border, - borderWidth: isSelected ? 0 : 1, - paddingVertical: 12, - borderRadius: 15, - paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 10, + paddingHorizontal: 10, gap: 8, overflow: "hidden", - }, - animatedContainerStyle, + } ]} > = ({ account, isSelected, onPr )} - {isSelected && ( + {( = ({ account, isSelected, onPr style={[ { fontFamily: "semibold", - fontSize: 13, - color: colors.primary, - }, - animatedTextStyle, + fontSize: 15.5, + color: isSelected ? colors.primary : colors.text, + } ]} > {account.label} diff --git a/src/components/Restaurant/RestaurantCard.tsx b/src/components/Restaurant/RestaurantCard.tsx index 877b36f98..62a006c76 100644 --- a/src/components/Restaurant/RestaurantCard.tsx +++ b/src/components/Restaurant/RestaurantCard.tsx @@ -40,7 +40,7 @@ const RestaurantCard: React.FC = ({ solde, repas }) => { style={{ textAlign: "left", fontFamily: "semibold", - color: solde < 0 ? "D10000" : "#5CB21F", + color: solde < 0 ? "#D10000" : "#5CB21F", fontSize: 30, }} > diff --git a/src/router/helpers/types.ts b/src/router/helpers/types.ts index 88d5f50ec..bc0624c04 100644 --- a/src/router/helpers/types.ts +++ b/src/router/helpers/types.ts @@ -12,6 +12,7 @@ import type React from "react"; import type { School as SkolengoSchool} from "scolengo-api/types/models/School"; import {Information} from "@/services/shared/Information"; import { ImageSourcePropType } from "react-native"; +import {Client} from "pawrd"; export type RouteParameters = { // welcome.index @@ -137,7 +138,7 @@ export type RouteParameters = { ExternalArdLogin: undefined; ExternalIzlyLogin: undefined; IzlyActivation: { username: string, password: string }; - QrcodeAnswer: { accountID: string }; + PriceError: { account: Client, accountId: string }; QrcodeScanner: { accountID: string }; PriceDetectionOnboarding: { accountID: string }; PriceBeforeScan: { accountID: string }; diff --git a/src/router/screens/settings/index.ts b/src/router/screens/settings/index.ts index 290888bd0..958e51551 100644 --- a/src/router/screens/settings/index.ts +++ b/src/router/screens/settings/index.ts @@ -18,7 +18,7 @@ import ExternalTurboselfLogin from "@/views/settings/ExternalAccount/Turboself"; import ExternalArdLogin from "@/views/settings/ExternalAccount/ARD"; import SettingsDonorsList from "@/views/settings/SettingsDonorsList"; import ExternalAccountSelector from "@/views/settings/ExternalAccount/ServiceSelector"; -import QrcodeAnswer from "@/views/settings/ExternalAccount/QrcodeAnswer"; +import PriceError from "@/views/settings/ExternalAccount/PriceError"; import QrcodeScanner from "@/views/settings/ExternalAccount/QrcodeScanner"; import PriceDetectionOnboarding from "@/views/settings/ExternalAccount/PriceDetectionOnboarding"; import PriceBeforeScan from "@/views/settings/ExternalAccount/PriceBeforeScan"; @@ -76,7 +76,7 @@ const settingsScreens = [ headerTitle: "Configuration de la cantine", }), - createScreen("QrcodeAnswer", QrcodeAnswer, { + createScreen("PriceError", PriceError, { headerTitle: "Configuration de la cantine", }), diff --git a/src/services/ard/balance.ts b/src/services/ard/balance.ts index 52da4f087..cad320509 100644 --- a/src/services/ard/balance.ts +++ b/src/services/ard/balance.ts @@ -5,12 +5,13 @@ import { ErrorServiceUnauthenticated } from "../shared/errors"; export const balance = async (account: ARDAccount): Promise => { if (!account.instance) throw new ErrorServiceUnauthenticated("ARD"); const payments = await account.instance!.getOnlinePayments(); + const mealPrice = account.authentication.mealPrice; return payments.walletData.map(wallet => ({ amount: wallet.walletAmount / 100, currency: "€", - remaining: null, - label: wallet.walletName + remaining: Math.floor((wallet.walletAmount / mealPrice!)), + label: wallet.walletName[0].toUpperCase() + wallet.walletName.slice(1).toLowerCase() })); }; \ No newline at end of file diff --git a/src/services/ard/history.ts b/src/services/ard/history.ts index a419edc21..981b0212a 100644 --- a/src/services/ard/history.ts +++ b/src/services/ard/history.ts @@ -25,7 +25,7 @@ export const history = async (account: ARDAccount): Promise ({ - amount: item.amount / 100, + amount: -item.amount / 100, timestamp: item.consumptionDate * 1000, currency: "€", label: item.consumptionDescription diff --git a/src/stores/account/types.ts b/src/stores/account/types.ts index 9da0d54e8..6c220c301 100644 --- a/src/stores/account/types.ts +++ b/src/stores/account/types.ts @@ -182,7 +182,8 @@ export interface ARDAccount extends BaseExternalAccount { pid: string username: string password: string - schoolID: string + schoolID: string, + mealPrice: number } } diff --git a/src/views/account/Restaurant/Menu.tsx b/src/views/account/Restaurant/Menu.tsx index d65bbe253..e636cfb97 100644 --- a/src/views/account/Restaurant/Menu.tsx +++ b/src/views/account/Restaurant/Menu.tsx @@ -195,7 +195,7 @@ const Menu: Screen<"Menu"> = ({ route, navigation }) => { ) : ( <> - {allBalances?.map((account, index) => ( + {allBalances!.length > 1 && allBalances?.map((account, index) => ( = ({ route, navigation }) => { > )} diff --git a/src/views/account/Restaurant/Modals/History.tsx b/src/views/account/Restaurant/Modals/History.tsx index d3df1f59b..c020b671b 100644 --- a/src/views/account/Restaurant/Modals/History.tsx +++ b/src/views/account/Restaurant/Modals/History.tsx @@ -18,27 +18,31 @@ const RestaurantHistory = ({ route }: { route: NavigationProps }) => { return date.toLocaleDateString("fr-FR", { weekday: "long", month: "long", - day: "numeric", + day: "numeric" }).toUpperCase(); }; const groupedHistories = useMemo(() => { - const historyMap = new Map(); + const historyMap = new Map(); + + histories.sort((a, b) => b.timestamp - a.timestamp); + histories.forEach((history: ReservationHistory) => { const formattedDate = formatDate(history.timestamp); if (!historyMap.has(formattedDate)) { historyMap.set(formattedDate, []); } - historyMap.get(formattedDate).push(history); + historyMap.get(formattedDate)?.push(history); }); historyMap.forEach((value) => { - value.sort((a: { timestamp: number; }, b: { timestamp: number; }) => b.timestamp - a.timestamp); + value.sort((a, b) => b.timestamp - a.timestamp); }); - return Array.from(historyMap.entries()).sort(([a], [b]) => a - b);; + return Array.from(historyMap); }, [histories]); + return ( {histories === null ? ( diff --git a/src/views/settings/ExternalAccount/ARD.tsx b/src/views/settings/ExternalAccount/ARD.tsx index b82f51fed..6ed4cc305 100644 --- a/src/views/settings/ExternalAccount/ARD.tsx +++ b/src/views/settings/ExternalAccount/ARD.tsx @@ -1,11 +1,33 @@ import { useState } from "react"; -import { Authenticator } from "pawrd"; +import {Authenticator, Client, getOnlinePayments} from "pawrd"; import { AccountService, type ARDAccount } from "@/stores/account/types"; import uuid from "@/utils/uuid-v4"; import { useAccounts, useCurrentAccount } from "@/stores/account"; import LoginView from "@/components/Templates/LoginView"; import { Screen } from "@/router/helpers/types"; +export async function detectMealPrice (account: Client): Promise { + const uid = await account.getOnlinePayments().then((payment) => payment.user.uid); + const consumptionsHistory = await account.getConsumptionsHistory(uid); + + let mostFrequentAmount: number | null = null; + let maxCount = 0; + const amountCount: Record = {}; + + for (const consumption of consumptionsHistory) { + const amount = consumption.amount; + + amountCount[amount] = (amountCount[amount] || 0) + 1; + + if (amountCount[amount] > maxCount) { + maxCount = amountCount[amount]; + mostFrequentAmount = amount; + } + } + + return mostFrequentAmount || null; +} + const ExternalArdLogin: Screen<"ExternalArdLogin"> = ({ navigation }) => { const linkExistingExternalAccount = useCurrentAccount(store => store.linkExistingExternalAccount); const create = useAccounts(store => store.create); @@ -19,6 +41,7 @@ const ExternalArdLogin: Screen<"ExternalArdLogin"> = ({ navigation }) => { const schoolID = customFields["schoolID"]; const client = await authenticator.fromCredentials(schoolID, username, password); + const mealPrice = await detectMealPrice(client); const new_account: ARDAccount = { instance: client, @@ -28,7 +51,8 @@ const ExternalArdLogin: Screen<"ExternalArdLogin"> = ({ navigation }) => { schoolID, username, password, - pid: client.pid + pid: client.pid, + mealPrice: mealPrice ?? 100 }, isExternal: true, localID: uuid(), @@ -37,8 +61,10 @@ const ExternalArdLogin: Screen<"ExternalArdLogin"> = ({ navigation }) => { create(new_account); linkExistingExternalAccount(new_account); - - navigation.navigate("QrcodeAnswer", { accountID: new_account.localID }); + if (!mealPrice) return navigation.navigate("PriceError", { account: client, accountId: new_account.localID }); + navigation.pop(); + navigation.pop(); + navigation.pop(); } catch (error) { if (error instanceof Error) { setError(error.message); diff --git a/src/views/settings/ExternalAccount/PriceError.tsx b/src/views/settings/ExternalAccount/PriceError.tsx new file mode 100644 index 000000000..2834e1896 --- /dev/null +++ b/src/views/settings/ExternalAccount/PriceError.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import type { Screen } from "@/router/helpers/types"; +import { useTheme } from "@react-navigation/native"; +import { CircleHelp } from "lucide-react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import {View, StyleSheet, Text, Alert} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useAccounts } from "@/stores/account"; +import ButtonCta from "@/components/FirstInstallation/ButtonCta"; +import {ExternalAccount} from "@/stores/account/types"; +import {detectMealPrice} from "@/views/settings/ExternalAccount/ARD"; + +type Props = { + navigation: any; + route: { params: { accountID: string } }; +}; + +const PriceError: Screen<"PriceError"> = ({ navigation, route }) => { + const theme = useTheme(); + const { colors } = theme; + const insets = useSafeAreaInsets(); + const update = useAccounts(store => store.update); + const account = route.params?.account; + const accountId = route.params?.accountId; + + const manualInput = () => { + Alert.prompt( + "Entrez le prix d'un repas", + "", + [ + { text: "Annuler", onPress: () => {} }, + { text: "Soumettre", onPress: async (input) => { + if (input) { + const mealPrice = parseFloat(input.replace(",", ".")) * 100; + update(accountId, "authentication", {"mealPrice": mealPrice}); + navigation.pop(); + navigation.pop(); + navigation.pop(); + navigation.pop(); + } + }}, + ], + "plain-text", + "2.00" + ); + }; + + const reloadMealPrice = async () => { + const mealPrice = await detectMealPrice(account); + if (!mealPrice) return Alert.alert("Erreur", "Impossible de déterminer le prix d'un repas"); + update(accountId, "authentication", { "mealPrice": mealPrice }); + navigation.pop(); + navigation.pop(); + navigation.pop(); + navigation.pop(); + }; + + return ( + + + + + + Une erreur s'est produite + Nous n’avons pas réussi à déterminer le prix d’un repas + + + reloadMealPrice()} + /> + manualInput()} + /> + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + justifyContent: "flex-end", + gap: 20, + }, + + list: { + flex: 1, + width: "100%", + alignItems: "center", + gap: 9, + paddingHorizontal: 20, + }, + + buttons: { + width: "100%", + paddingHorizontal: 16, + marginBottom: 16, + }, + + image: { + width: 32, + height: 32, + borderRadius: 80, + }, +}); + + +export default PriceError; diff --git a/src/views/settings/ExternalAccount/QrcodeAnswer.tsx b/src/views/settings/ExternalAccount/QrcodeAnswer.tsx deleted file mode 100644 index 6151b3f26..000000000 --- a/src/views/settings/ExternalAccount/QrcodeAnswer.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React from "react"; -import type { Screen } from "@/router/helpers/types"; -import { useTheme } from "@react-navigation/native"; -import { CircleDashed, Star } from "lucide-react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Image, View, StyleSheet, StatusBar, ScrollView, Text } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; -import Reanimated, { LinearTransition, FlipInXDown } from "react-native-reanimated"; -import PapillonShineBubble from "@/components/FirstInstallation/PapillonShineBubble"; -import { - NativeItem, - NativeList, - NativeText, -} from "@/components/Global/NativeComponents"; -import { AccountService, ExternalAccount } from "@/stores/account/types"; -import { useAccounts, useCurrentAccount } from "@/stores/account"; -import DuoListPressable from "@/components/FirstInstallation/DuoListPressable"; -import ButtonCta from "@/components/FirstInstallation/ButtonCta"; - -type Props = { - navigation: any; - route: { params: { accountID: string } }; -}; - -const QrcodeAnswer: Screen<"QrcodeAnswer"> = ({ navigation, route }) => { - const theme = useTheme(); - const { colors } = theme; - const insets = useSafeAreaInsets(); - const update = useAccounts(store => store.update); - const accountID = route.params?.accountID; - - const [answer, setAnswer] = React.useState(false); - - return ( - - - - {accountID} - - - - setAnswer(true)} - /> - - - - setAnswer(false)} - /> - - - - - - { - update(accountID, "data", { "qr-enable": answer }); - if (answer) { - navigation.navigate("QrcodeScanner", { accountID, }); - } else { - navigation.pop(); - navigation.pop(); - } - } - } - /> - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: "center", - gap: 20, - }, - - list: { - flex: 1, - width: "100%", - alignItems: "center", - gap: 9, - paddingHorizontal: 20, - }, - - buttons: { - width: "100%", - paddingHorizontal: 16, - gap: 9, - marginBottom: 16, - }, - - image: { - width: 32, - height: 32, - borderRadius: 80, - }, -}); - - -export default QrcodeAnswer; diff --git a/src/views/settings/ExternalAccount/Turboself.tsx b/src/views/settings/ExternalAccount/Turboself.tsx index 9138d44ca..dbdc10d03 100644 --- a/src/views/settings/ExternalAccount/Turboself.tsx +++ b/src/views/settings/ExternalAccount/Turboself.tsx @@ -36,6 +36,7 @@ const ExternalTurboselfLogin: Screen<"ExternalTurboselfLogin"> = ({ navigation } navigation.pop(); navigation.pop(); + navigation.pop(); } catch (error) { if (error instanceof Error) { setError(error.message); diff --git a/src/widgets/Components/RestaurantBalance.tsx b/src/widgets/Components/RestaurantBalance.tsx index 2ae982510..56fdec455 100644 --- a/src/widgets/Components/RestaurantBalance.tsx +++ b/src/widgets/Components/RestaurantBalance.tsx @@ -97,31 +97,39 @@ const RestaurantBalanceWidget = forwardRef(({ fontSize: 37, lineHeight: 37, fontFamily: "semibold", - color: "#5CB21F", + color: (currentBalance?.remaining ?? 0) <= 0 + ? "#D10000" + : "#5CB21F", }} contentContainerStyle={{ paddingLeft: 6, }} /> - - - {currentBalance?.remaining ?? 0} {currentBalance?.remaining === 1 ? "repas restant" : "repas restants"} - - + {currentBalance?.remaining !== undefined && currentBalance?.remaining !== null && ( + + + {Math.max(0, currentBalance?.remaining ?? 0)} {currentBalance?.remaining === 1 ? "repas restant" : "repas restants"} + + + )} );