From b96f2f676a7055d5bfb5b01839110da44f07f830 Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Sat, 17 May 2025 18:17:20 -0400
Subject: [PATCH 1/9] Creating new tabBar and doing fixes
---
src/app/(delivery-tabs)/_layout.tsx | 29 +---
src/app/(delivery-tabs)/deliverySupport.tsx | 6 -
src/app/(tabs)/_layout.tsx | 20 +--
src/app/(tabs)/support.tsx | 7 -
src/app/_layout.tsx | 50 +++----
src/components/TabBar.tsx | 140 ++++++++++++++++++
src/components/TabBarButton.tsx | 94 ++++++++++++
src/constants/icon.tsx | 22 +++
src/screens/ActiveOrdersScreen.tsx | 2 +-
src/screens/CartListScreen.tsx | 1 +
src/screens/DirectionChangeScreen.tsx | 2 +-
src/screens/DirectionCreateScreen.tsx | 2 +-
src/screens/DirectionScreen.tsx | 2 +-
.../LoggedInPasswordRecoveryScreen.tsx | 1 -
src/screens/MenuScreen.tsx | 1 -
src/screens/OrderDetailScreen.tsx | 2 +-
src/screens/OrdersScreen.tsx | 2 +-
src/screens/PasswordChangeScreen.tsx | 2 +-
src/screens/ProfileScreen.tsx | 2 +-
src/screens/SelectLocationScreen.tsx | 25 +++-
.../delivery/DeliveryHistoryScreen.tsx | 4 +
src/screens/delivery/DeliveryHomeScreen.tsx | 4 +
.../delivery/DeliverySupportScreen.tsx | 21 ---
src/screens/tab/BranchesScreen.tsx | 4 +
src/screens/tab/CategoriesScreen.tsx | 4 +
src/screens/tab/HomeScreen.tsx | 4 +
src/screens/tab/OffersScreen.tsx | 4 +
src/screens/tab/SupportScreen.tsx | 21 ---
src/styles/theme.ts | 4 +
29 files changed, 338 insertions(+), 144 deletions(-)
delete mode 100644 src/app/(delivery-tabs)/deliverySupport.tsx
delete mode 100644 src/app/(tabs)/support.tsx
create mode 100644 src/components/TabBar.tsx
create mode 100644 src/components/TabBarButton.tsx
create mode 100644 src/constants/icon.tsx
delete mode 100644 src/screens/delivery/DeliverySupportScreen.tsx
delete mode 100644 src/screens/tab/SupportScreen.tsx
diff --git a/src/app/(delivery-tabs)/_layout.tsx b/src/app/(delivery-tabs)/_layout.tsx
index 9e1243f..14b1f43 100644
--- a/src/app/(delivery-tabs)/_layout.tsx
+++ b/src/app/(delivery-tabs)/_layout.tsx
@@ -3,25 +3,17 @@ import { Tabs } from 'expo-router';
import { View, StyleSheet } from 'react-native';
import { Colors } from '../../styles/theme';
import PoppinsText from '../../components/PoppinsText';
-import {
- HomeIcon,
- ClockIcon,
- LifebuoyIcon,
-} from 'react-native-heroicons/outline';
+import { HomeIcon, ClockIcon } from 'react-native-heroicons/outline';
import TopBarDelivery from '../../components/TopBarDelivery';
import { AlertProvider } from '../../components/AlertProvider'; // Importar el AlertProvider
+import { TabBar } from '../../components/TabBar';
export default function DeliveryTabLayout() {
return (
-
+ }>
- (
-
- Soporte
-
- ),
- headerShown: false,
- title: 'Soporte',
- tabBarIcon: () => (
-
- ),
- }}
- />
diff --git a/src/app/(delivery-tabs)/deliverySupport.tsx b/src/app/(delivery-tabs)/deliverySupport.tsx
deleted file mode 100644
index fae268e..0000000
--- a/src/app/(delivery-tabs)/deliverySupport.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-import DeliverySupportScreen from '../../screens/delivery/DeliverySupportScreen';
-
-export default function DeliverySupport() {
- return ;
-}
diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx
index ae1c79b..27b9f64 100644
--- a/src/app/(tabs)/_layout.tsx
+++ b/src/app/(tabs)/_layout.tsx
@@ -3,22 +3,21 @@ import {
ListBulletIcon,
MapPinIcon,
TagIcon,
- LifebuoyIcon,
} from 'react-native-heroicons/outline';
import { Tabs } from 'expo-router';
import { Colors } from '../../styles/theme';
import PoppinsText from '../../components/PoppinsText';
import { View, StyleSheet } from 'react-native';
import TopBar from '../../components/TopBar';
+import { TabBar } from '../../components/TabBar';
export default function TabLayout() {
return (
} // Siempre renderiza la TabBar
screenOptions={{
- tabBarActiveTintColor: Colors.primary,
- tabBarStyle: { backgroundColor: Colors.secondaryWhite },
tabBarHideOnKeyboard: true,
tabBarVisibilityAnimationConfig: {
show: {
@@ -93,21 +92,6 @@ export default function TabLayout() {
),
}}
/>
- (
-
- Soporte
-
- ),
- title: 'Soporte',
- headerShown: false,
- tabBarIcon: () => (
-
- ),
- }}
- />
);
diff --git a/src/app/(tabs)/support.tsx b/src/app/(tabs)/support.tsx
deleted file mode 100644
index 600e4de..0000000
--- a/src/app/(tabs)/support.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-// app/(tabs)/support.tsx
-import React from 'react';
-import SupportScreen from '../../screens/tab/SupportScreen';
-
-export default function TabsHome() {
- return ;
-}
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index a9c0b94..7fe2879 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -55,6 +55,10 @@ export default function RootLayout() {
({
headerBackVisible: false,
+ headerStyle: {
+ backgroundColor: Colors.bgColor, // Color específico para esta pantalla
+ },
+ headerShadowVisible: false,
headerLeft: () =>
navigation.canGoBack() ? (
-
+
-
+
-
-
-
-
+
+
+
+
({
headerTitle: '',
- headerTransparent: true,
headerLeft: undefined,
headerRight: () => (
navigation.goBack()}>
@@ -149,10 +137,7 @@ export default function RootLayout() {
),
})}
/>
-
+
diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx
new file mode 100644
index 0000000..c34528f
--- /dev/null
+++ b/src/components/TabBar.tsx
@@ -0,0 +1,140 @@
+import { View, StyleSheet, LayoutChangeEvent } from 'react-native';
+import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
+import { Colors } from '../styles/theme';
+import { useState } from 'react';
+import TabBarButton from './TabBarButton';
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+
+export function TabBar({ state, descriptors, navigation }: BottomTabBarProps) {
+ const currentRoute = state.routes[state.index].name;
+
+ // Oculta la TabBar en pantallas específicas
+ const hiddenRoutes = ['menu', 'cart']; // Agrega aquí las rutas donde no quieres mostrar la TabBar
+ if (hiddenRoutes.includes(currentRoute)) {
+ return null; // No renderiza la TabBar
+ }
+
+ const [dimensions, setDimensions] = useState({ width: 10, height: 20 });
+
+ const buttonWidth = dimensions.width / state.routes.length;
+
+ const onTabBarLayout = (e: LayoutChangeEvent) => {
+ setDimensions({
+ width: e.nativeEvent.layout.width,
+ height: e.nativeEvent.layout.height,
+ });
+ };
+
+ const tabPositionX = useSharedValue(0);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ {
+ translateX: tabPositionX.value + 16,
+ },
+ ],
+ }));
+
+ return (
+
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label =
+ options.tabBarLabel !== undefined
+ ? options.tabBarLabel
+ : options.title !== undefined
+ ? options.title
+ : route.name;
+
+ const isFocused = state.index === index;
+
+ const onPress = () => {
+ tabPositionX.value = withTiming(buttonWidth * index, {
+ duration: 350,
+ });
+
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ canPreventDefault: true,
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ navigation.navigate(route.name, route.params);
+ }
+ };
+
+ const onLongPress = () => {
+ navigation.emit({
+ type: 'tabLongPress',
+ target: route.key,
+ });
+ };
+
+ return (
+
+ label({
+ ...props,
+ position: props.position as 'beside-icon' | 'below-icon',
+ })
+ : label
+ }
+ />
+ );
+ })}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tabBar: {
+ position: 'absolute',
+ flexDirection: 'row',
+ bottom: 0,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ borderColor: Colors.gray_100,
+ borderTopWidth: 1,
+ backgroundColor: Colors.menuWhite,
+ paddingVertical: 6,
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 10,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 10,
+ },
+ // tabBarItem: {
+ // flex: 1,
+ // justifyContent: 'center',
+ // alignItems: 'center',
+ // },
+});
diff --git a/src/components/TabBarButton.tsx b/src/components/TabBarButton.tsx
new file mode 100644
index 0000000..d11c631
--- /dev/null
+++ b/src/components/TabBarButton.tsx
@@ -0,0 +1,94 @@
+import { Pressable, StyleSheet, GestureResponderEvent } from 'react-native';
+import Animated from 'react-native-reanimated';
+import React, { useEffect } from 'react';
+import { icon } from '../constants/icon';
+import {
+ interpolate,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
+import { Colors } from '../styles/theme';
+
+const TabBarButton = ({
+ onPress,
+ onLongPress,
+ isFocused,
+ routeName,
+ label,
+}: {
+ onPress: (event: GestureResponderEvent) => void;
+ onLongPress: (event: GestureResponderEvent) => void;
+ isFocused: boolean;
+ routeName: keyof typeof icon;
+ color: string;
+ label:
+ | string
+ | ((args: {
+ focused: boolean;
+ color: string;
+ position: string;
+ children: string;
+ }) => React.ReactNode);
+}) => {
+ const scale = useSharedValue(0);
+
+ useEffect(() => {
+ scale.value = withSpring(
+ typeof isFocused === 'boolean' ? (isFocused ? 1 : 0) : isFocused,
+ { duration: 350 },
+ );
+ }, [scale, isFocused]);
+
+ const animatedIconStyle = useAnimatedStyle(() => {
+ const scaleValue = interpolate(scale.value, [0, 1], [1, 1.2]);
+
+ const top = interpolate(scale.value, [0, 1], [0, 9]);
+ return {
+ transform: [{ scale: scaleValue }],
+ top,
+ };
+ });
+
+ const animatedTextStyle = useAnimatedStyle(() => {
+ const opacity = interpolate(scale.value, [0, 1], [1, 0]);
+ return {
+ opacity,
+ };
+ });
+ return (
+
+
+ {icon[routeName]({
+ color: isFocused ? Colors.menuWhite : Colors.primary,
+ })}
+
+
+
+ {typeof label === 'function'
+ ? label({
+ focused: isFocused,
+ color: isFocused ? Colors.primary : Colors.primary,
+ position: 'beside-icon',
+ children: '',
+ })
+ : label}
+
+
+ );
+};
+
+export default TabBarButton;
+
+const styles = StyleSheet.create({
+ tabBarItem: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: 6,
+ },
+});
diff --git a/src/constants/icon.tsx b/src/constants/icon.tsx
new file mode 100644
index 0000000..45386e5
--- /dev/null
+++ b/src/constants/icon.tsx
@@ -0,0 +1,22 @@
+import { Feather } from '@expo/vector-icons';
+import type { ComponentProps } from 'react';
+
+type FeatherIconProps = Omit, 'name'>;
+
+export const icon = {
+ index: (props: FeatherIconProps) => (
+
+ ),
+ categories: (props: FeatherIconProps) => (
+
+ ),
+ branches: (props: FeatherIconProps) => (
+
+ ),
+ offers: (props: FeatherIconProps) => (
+
+ ),
+ deliveryHistory: (props: FeatherIconProps) => (
+
+ ),
+};
diff --git a/src/screens/ActiveOrdersScreen.tsx b/src/screens/ActiveOrdersScreen.tsx
index 51bd3eb..a5fc103 100644
--- a/src/screens/ActiveOrdersScreen.tsx
+++ b/src/screens/ActiveOrdersScreen.tsx
@@ -171,6 +171,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
},
alertContainer: {
position: 'absolute',
@@ -183,7 +184,6 @@ const styles = StyleSheet.create({
},
orderHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
orderInfo: {
diff --git a/src/screens/CartListScreen.tsx b/src/screens/CartListScreen.tsx
index c2151aa..b5a6f51 100644
--- a/src/screens/CartListScreen.tsx
+++ b/src/screens/CartListScreen.tsx
@@ -153,6 +153,7 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: Colors.bgColor,
padding: 16,
+ paddingTop: -16,
},
header: {
fontSize: FontSizes.h5.size,
diff --git a/src/screens/DirectionChangeScreen.tsx b/src/screens/DirectionChangeScreen.tsx
index 26ebc2b..df541bd 100644
--- a/src/screens/DirectionChangeScreen.tsx
+++ b/src/screens/DirectionChangeScreen.tsx
@@ -251,6 +251,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
},
alertContainer: {
position: 'absolute',
@@ -263,7 +264,6 @@ const styles = StyleSheet.create({
},
addressHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
addressImage: {
diff --git a/src/screens/DirectionCreateScreen.tsx b/src/screens/DirectionCreateScreen.tsx
index 35f3ca8..8488348 100644
--- a/src/screens/DirectionCreateScreen.tsx
+++ b/src/screens/DirectionCreateScreen.tsx
@@ -207,6 +207,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
},
alertContainer: {
position: 'absolute',
@@ -219,7 +220,6 @@ const styles = StyleSheet.create({
},
addressHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
addressInfo: {
diff --git a/src/screens/DirectionScreen.tsx b/src/screens/DirectionScreen.tsx
index 8653b6b..998c23c 100644
--- a/src/screens/DirectionScreen.tsx
+++ b/src/screens/DirectionScreen.tsx
@@ -191,6 +191,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
},
alertContainer: {
position: 'absolute',
@@ -203,7 +204,6 @@ const styles = StyleSheet.create({
},
directionHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
directionInfo: {
diff --git a/src/screens/LoggedInPasswordRecoveryScreen.tsx b/src/screens/LoggedInPasswordRecoveryScreen.tsx
index e2b2096..880e6f5 100644
--- a/src/screens/LoggedInPasswordRecoveryScreen.tsx
+++ b/src/screens/LoggedInPasswordRecoveryScreen.tsx
@@ -296,7 +296,6 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
justifyContent: 'flex-start',
- paddingTop: 56,
paddingHorizontal: 20,
},
stepsWrapper: {
diff --git a/src/screens/MenuScreen.tsx b/src/screens/MenuScreen.tsx
index a7bc1ab..761691f 100644
--- a/src/screens/MenuScreen.tsx
+++ b/src/screens/MenuScreen.tsx
@@ -214,7 +214,6 @@ const styles = StyleSheet.create({
},
menuContent: {
flex: 1,
- marginTop: 40,
paddingHorizontal: 10,
justifyContent: 'space-between',
},
diff --git a/src/screens/OrderDetailScreen.tsx b/src/screens/OrderDetailScreen.tsx
index 2a4d39b..6dddebc 100644
--- a/src/screens/OrderDetailScreen.tsx
+++ b/src/screens/OrderDetailScreen.tsx
@@ -234,6 +234,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
paddingBottom: 0,
},
alertContainer: {
@@ -253,7 +254,6 @@ const styles = StyleSheet.create({
},
orderHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
orderImage: {
diff --git a/src/screens/OrdersScreen.tsx b/src/screens/OrdersScreen.tsx
index d7dfa26..73a5600 100644
--- a/src/screens/OrdersScreen.tsx
+++ b/src/screens/OrdersScreen.tsx
@@ -197,6 +197,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
},
alertContainer: {
position: 'absolute',
@@ -209,7 +210,6 @@ const styles = StyleSheet.create({
},
orderHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
orderInfo: {
diff --git a/src/screens/PasswordChangeScreen.tsx b/src/screens/PasswordChangeScreen.tsx
index 595e918..9e35277 100644
--- a/src/screens/PasswordChangeScreen.tsx
+++ b/src/screens/PasswordChangeScreen.tsx
@@ -207,7 +207,7 @@ const styles = StyleSheet.create({
backgroundColor: Colors.bgColor,
justifyContent: 'flex-start',
alignItems: 'center',
- paddingTop: 100,
+ paddingTop: 32,
paddingHorizontal: 20,
},
alertContainer: {
diff --git a/src/screens/ProfileScreen.tsx b/src/screens/ProfileScreen.tsx
index ee10956..1e60176 100644
--- a/src/screens/ProfileScreen.tsx
+++ b/src/screens/ProfileScreen.tsx
@@ -311,6 +311,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
backgroundColor: Colors.bgColor,
padding: 20,
+ paddingTop: -20,
},
alertContainer: {
position: 'absolute',
@@ -323,7 +324,6 @@ const styles = StyleSheet.create({
},
profileHeader: {
alignItems: 'center',
- marginTop: 30,
marginBottom: 15,
},
profileImage: {
diff --git a/src/screens/SelectLocationScreen.tsx b/src/screens/SelectLocationScreen.tsx
index a85b204..dfa43b3 100644
--- a/src/screens/SelectLocationScreen.tsx
+++ b/src/screens/SelectLocationScreen.tsx
@@ -1,5 +1,11 @@
import React, { useState, useRef, useEffect } from 'react';
-import { StyleSheet, View, TextInput, Alert } from 'react-native';
+import {
+ StyleSheet,
+ View,
+ TextInput,
+ Alert,
+ TouchableOpacity,
+} from 'react-native';
import MapView, { PROVIDER_GOOGLE, Region } from 'react-native-maps'; // Importa el tipo Region
import * as Location from 'expo-location';
import { useRouter } from 'expo-router';
@@ -8,10 +14,14 @@ import { MapPinIcon } from 'react-native-heroicons/solid';
import { Colors, FontSizes } from '../styles/theme';
import PoppinsText from '../components/PoppinsText';
import { Config } from '../config';
+import { ChevronLeftIcon } from 'react-native-heroicons/outline';
+import { useNavigation } from '@react-navigation/native';
const SelectLocationScreen = () => {
console.log('Clave de API utilizada:', Config.googleMapsApiKey);
+ const navigation = useNavigation();
+
const [selectedLocation, setSelectedLocation] = useState<{
latitude: number;
longitude: number;
@@ -129,6 +139,15 @@ const SelectLocationScreen = () => {
{/* Header con input para mostrar la dirección */}
+
+
+
{
/>
))
)}
+
);
@@ -172,6 +173,9 @@ const styles = StyleSheet.create({
padding: 16,
marginHorizontal: 4,
},
+ height: {
+ height: 64,
+ },
title: {
fontSize: FontSizes.h5.size,
lineHeight: FontSizes.h5.lineHeight,
diff --git a/src/screens/delivery/DeliveryHomeScreen.tsx b/src/screens/delivery/DeliveryHomeScreen.tsx
index 3f63b72..43e9d0f 100644
--- a/src/screens/delivery/DeliveryHomeScreen.tsx
+++ b/src/screens/delivery/DeliveryHomeScreen.tsx
@@ -205,6 +205,7 @@ export default function DeliveryHomeScreen() {
/>
))
)}
+
{/* Popup de confirmación para descartar orden */}
@@ -233,6 +234,9 @@ const styles = StyleSheet.create({
padding: 16,
marginHorizontal: 4,
},
+ height: {
+ height: 64,
+ },
title: {
fontSize: FontSizes.h5.size,
lineHeight: FontSizes.h5.lineHeight,
diff --git a/src/screens/delivery/DeliverySupportScreen.tsx b/src/screens/delivery/DeliverySupportScreen.tsx
deleted file mode 100644
index 5b841c7..0000000
--- a/src/screens/delivery/DeliverySupportScreen.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
-import PoppinsText from '../../components/PoppinsText';
-import { Colors } from '../../styles/theme';
-
-export default function SupportScreen() {
- return (
-
- Pantalla Soporte
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: Colors.bgColor,
- },
-});
diff --git a/src/screens/tab/BranchesScreen.tsx b/src/screens/tab/BranchesScreen.tsx
index f74e03d..9d70be8 100644
--- a/src/screens/tab/BranchesScreen.tsx
+++ b/src/screens/tab/BranchesScreen.tsx
@@ -144,6 +144,7 @@ export default function BranchesScreen() {
+
);
}
@@ -155,6 +156,9 @@ const styles = StyleSheet.create({
padding: 16,
paddingTop: -16,
},
+ height: {
+ height: 64,
+ },
title: {
fontSize: FontSizes.s1.size,
lineHeight: FontSizes.s1.lineHeight,
diff --git a/src/screens/tab/CategoriesScreen.tsx b/src/screens/tab/CategoriesScreen.tsx
index f446b72..7f5517f 100644
--- a/src/screens/tab/CategoriesScreen.tsx
+++ b/src/screens/tab/CategoriesScreen.tsx
@@ -80,6 +80,7 @@ export default function CategoriesScreen() {
showsVerticalScrollIndicator={true}
alwaysBounceVertical={true}
/>
+
);
}
@@ -89,6 +90,9 @@ const styles = StyleSheet.create({
backgroundColor: Colors.bgColor,
flex: 1,
},
+ height: {
+ height: 64,
+ },
alertContainer: {
position: 'absolute',
width: 326,
diff --git a/src/screens/tab/HomeScreen.tsx b/src/screens/tab/HomeScreen.tsx
index ad91bf1..8d1e2de 100644
--- a/src/screens/tab/HomeScreen.tsx
+++ b/src/screens/tab/HomeScreen.tsx
@@ -216,6 +216,7 @@ export default function HomeScreen() {
)}
+
Pantalla Ofertas
+
);
}
@@ -18,4 +19,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: Colors.bgColor,
},
+ height: {
+ height: 64,
+ },
});
diff --git a/src/screens/tab/SupportScreen.tsx b/src/screens/tab/SupportScreen.tsx
deleted file mode 100644
index ee0cf25..0000000
--- a/src/screens/tab/SupportScreen.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
-import PoppinsText from '../../components/PoppinsText';
-import { Colors } from '../../styles/theme';
-
-export default function SupportScreen() {
- return (
-
- Pantalla Ofertas
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: Colors.bgColor,
- },
-});
diff --git a/src/styles/theme.ts b/src/styles/theme.ts
index 22a60be..c41121f 100644
--- a/src/styles/theme.ts
+++ b/src/styles/theme.ts
@@ -36,6 +36,10 @@ export const Colors = {
gray_100: '#E7E7E6',
gray_500: '#6E6D6C',
+ tertiary_600: '#247D74',
+ tertiary_500: '#319B8F',
+ tertiary_400: '#4AB7A8',
+
secondary_300: '#8BEAB3',
primary_300: '#A2BEEE',
From 2d1347e590939b948213a5573cbcfb820ef2a536 Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Sun, 18 May 2025 18:37:06 -0400
Subject: [PATCH 2/9] Fixing some errors that appeared in delivery details
---
src/components/DeliveryMap.tsx | 96 +++++++++----
src/screens/delivery/DeliveryDetailScreen.tsx | 135 +++++++++++-------
2 files changed, 151 insertions(+), 80 deletions(-)
diff --git a/src/components/DeliveryMap.tsx b/src/components/DeliveryMap.tsx
index c987c23..fa3e7e4 100644
--- a/src/components/DeliveryMap.tsx
+++ b/src/components/DeliveryMap.tsx
@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import MapView, { Marker, Polyline } from 'react-native-maps';
-import { StyleSheet, View, Alert } from 'react-native';
+import { StyleSheet, View, Alert, ActivityIndicator } from 'react-native';
import * as Location from 'expo-location';
import { Config } from '../config';
+import { Colors } from '../styles/theme';
interface DeliveryMapProps {
deliveryState: number;
@@ -25,6 +26,7 @@ const DeliveryMap: React.FC = ({
const [customerRouteCoordinates, setCustomerRouteCoordinates] = useState<
{ latitude: number; longitude: number }[]
>([]);
+ const [isLoading, setIsLoading] = useState(true); // Estado de carga
// Solicitar permisos y obtener la ubicación del delivery
useEffect(() => {
@@ -48,15 +50,6 @@ const DeliveryMap: React.FC = ({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
});
-
- // Obtener la ruta desde la ubicación del delivery hasta la sucursal
- fetchRoute(
- location.coords.latitude,
- location.coords.longitude,
- branchLocation.latitude,
- branchLocation.longitude,
- setRouteCoordinates,
- );
} catch (error) {
console.error('Error al obtener la ubicación del delivery:', error);
Alert.alert(
@@ -96,11 +89,30 @@ const DeliveryMap: React.FC = ({
}
};
+ // Obtener la ruta hacia la sucursal
+ useEffect(() => {
+ if (
+ deliveryLocation &&
+ branchLocation.latitude !== 0 &&
+ branchLocation.longitude !== 0
+ ) {
+ fetchRoute(
+ deliveryLocation.latitude,
+ deliveryLocation.longitude,
+ branchLocation.latitude,
+ branchLocation.longitude,
+ setRouteCoordinates,
+ );
+ }
+ }, [deliveryLocation, branchLocation]);
+
// Obtener la ruta hacia el cliente cuando el estado sea el correspondiente
useEffect(() => {
if (
- deliveryState >= 3 && // Estado "Ir a destino de entrega" o posterior
- deliveryLocation
+ deliveryState >= 3 &&
+ deliveryLocation &&
+ customerLocation.latitude !== 0 &&
+ customerLocation.longitude !== 0
) {
fetchRoute(
deliveryLocation.latitude,
@@ -113,7 +125,20 @@ const DeliveryMap: React.FC = ({
// Limpiar la ruta hacia la sucursal
setRouteCoordinates([]);
}
- }, [deliveryState, deliveryLocation]);
+ }, [deliveryState, deliveryLocation, customerLocation]);
+
+ // Verificar si todos los datos necesarios están disponibles
+ useEffect(() => {
+ if (
+ deliveryLocation &&
+ branchLocation.latitude !== 0 &&
+ branchLocation.longitude !== 0 &&
+ customerLocation.latitude !== 0 &&
+ customerLocation.longitude !== 0
+ ) {
+ setIsLoading(false); // Finalizar la carga cuando todos los datos estén disponibles
+ }
+ }, [deliveryLocation, branchLocation, customerLocation]);
// Decodificar la polyline de Google Maps
const decodePolyline = (encoded: string) => {
@@ -154,13 +179,22 @@ const DeliveryMap: React.FC = ({
return points;
};
+ if (isLoading) {
+ // Mostrar un indicador de carga mientras se obtienen los datos
+ return (
+
+
+
+ );
+ }
+
return (
= ({
)}
{/* Mostrar la ubicación de la sucursal */}
-
+ {branchLocation.latitude !== 0 && branchLocation.longitude !== 0 && (
+
+ )}
{/* Mostrar la ubicación del cliente */}
-
+ {customerLocation.latitude !== 0 &&
+ customerLocation.longitude !== 0 && (
+
+ )}
{/* Mostrar la ruta hacia la sucursal */}
{routeCoordinates.length > 0 && (
@@ -197,7 +236,7 @@ const DeliveryMap: React.FC = ({
/>
)}
- {/* Mostrar la ruta hacia el cliente solo si el estado es el correspondiente */}
+ {/* Mostrar la ruta hacia el cliente */}
{deliveryState >= 3 && customerRouteCoordinates.length > 0 && (
{
const [order, setOrder] = useState(
undefined,
);
+ const [isOrderDetailsLoading, setIsOrderDetailsLoading] = useState(true);
+ const [isFetchingOrder, setIsFetchingOrder] = useState(false); // Nuevo estado de carga para fetchOrder
+ const [branchNames, setBranchNames] = useState<
+ Record
+ >({});
+ const [deliveryStateBadge, setDeliveryStateBadge] = useState(0);
+ const router = useRouter();
+
+ useEffect(() => {
+ const fetchOrderDetails = async () => {
+ try {
+ if (!orderDetails) {
+ const details = await DeliveryService.getOrderDetails(id as string);
+ dispatch(setOrderDetails({ id: id as string, details }));
+ }
+ } catch (error) {
+ console.error('Error al obtener los detalles del pedido:', error);
+ } finally {
+ setIsOrderDetailsLoading(false);
+ }
+ };
+
+ fetchOrderDetails();
+ }, [id, orderDetails, dispatch]);
+
+ useEffect(() => {
+ const fetchBranches = async () => {
+ try {
+ const branches = await BranchService.findAll({ page: 1, limit: 100 });
+ const branchMap = branches.results.reduce(
+ (
+ acc: Record<
+ string,
+ { name: string; latitude: number; longitude: number }
+ >,
+ branch,
+ ) => {
+ acc[branch.id] = {
+ name: branch.name,
+ latitude: branch.latitude,
+ longitude: branch.longitude,
+ };
+ return acc;
+ },
+ {},
+ );
+
+ setBranchNames(branchMap);
+ } catch (error) {
+ console.error('Error al obtener las sucursales:', error);
+ } finally {
+ setIsFetchingOrder(false);
+ }
+ };
+
+ if (orderDetails) {
+ fetchBranches();
+ }
+ }, [orderDetails]);
+
useEffect(() => {
const fetchOrder = async () => {
+ setIsFetchingOrder(true); // Iniciar el indicador de carga
+ let timerId: NodeJS.Timeout | null = null; // Identificador del temporizador
+
try {
if (!orderDetails) {
- console.error(
- 'No se encontraron detalles de la orden de tipo delivery.',
- );
+ // Agregar un temporizador de 2 segundos antes de mostrar el error
+ timerId = setTimeout(() => {
+ if (!orderDetails) {
+ console.error(
+ 'No se encontraron detalles de la orden de tipo delivery.',
+ );
+ }
+ }, 2000);
return;
}
- const orderId = orderDetails.orderId; // Extraer el ID de la orden
+ const orderId = orderDetails.orderId;
console.log('ID de la orden:', orderId);
- const order = await UserService.getOrder(orderId); // Usar el ID de la orden
+ const order = await UserService.getOrder(orderId);
if (order.success) {
- console.log('Datos del pedido:', order.data); // Log para verificar los datos
+ console.log('Datos del pedido:', order.data);
setOrder(order.data);
} else {
console.error('Error al obtener el pedido:', order.error);
@@ -65,7 +133,10 @@ const DeliveryDetailScreen: React.FC = () => {
} catch (error) {
console.error('Error en fetchOrder:', error);
} finally {
- setLoading(false);
+ setIsFetchingOrder(false); // Finalizar el indicador de carga
+ if (timerId) {
+ clearTimeout(timerId); // Cancelar el temporizador si los datos están disponibles
+ }
}
};
@@ -76,13 +147,6 @@ const DeliveryDetailScreen: React.FC = () => {
(state: RootState) => state.delivery.deliveryState[id as string] || 0,
);
- const [branchNames, setBranchNames] = useState<
- Record
- >({});
- const [loading, setLoading] = useState(true);
- const [deliveryStateBadge, setDeliveryStateBadge] = useState(0);
- const router = useRouter();
-
// Estados para las alertas
const [showAlert, setShowAlert] = useState(false);
const [alertType, setAlertType] = useState<'success' | 'error' | 'info'>(
@@ -115,45 +179,6 @@ const DeliveryDetailScreen: React.FC = () => {
longitude: orderDetails?.address?.longitude || 0,
};
- useEffect(() => {
- const fetchOrderDetails = async () => {
- if (!orderDetails) {
- const details = await DeliveryService.getOrderDetails(id as string);
- dispatch(setOrderDetails({ id: id as string, details }));
- }
-
- try {
- // Obtener todas las sucursales y mapear sus nombres y coordenadas
- const branches = await BranchService.findAll({ page: 1, limit: 100 });
- const branchMap = branches.results.reduce(
- (
- acc: Record<
- string,
- { name: string; latitude: number; longitude: number }
- >,
- branch,
- ) => {
- acc[branch.id] = {
- name: branch.name,
- latitude: branch.latitude,
- longitude: branch.longitude,
- };
- return acc;
- },
- {},
- );
-
- setBranchNames(branchMap);
- } catch (error) {
- console.error('Error al obtener los detalles del pedido:', error);
- } finally {
- setLoading(false);
- }
- };
-
- fetchOrderDetails();
- }, [id, orderDetails, dispatch]);
-
const handleNextState = async () => {
try {
if (deliveryState === buttonStates.length - 1) {
@@ -242,11 +267,13 @@ const DeliveryDetailScreen: React.FC = () => {
}
};
- if (loading) {
+ if (isOrderDetailsLoading || isFetchingOrder) {
+ // Mostrar un indicador de carga mientras se obtienen los detalles del pedido o la orden
return ;
}
if (!orderDetails) {
+ // Mostrar un mensaje de error si no se encuentran los detalles del pedido
return (
From 8632ed578fae0c3bdc5528b6ca9b80763e8689ac Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Sun, 18 May 2025 20:15:41 -0400
Subject: [PATCH 3/9] Adapting proper estimated time to order delivery cards
---
src/components/OrderCard.tsx | 12 +-
src/screens/delivery/DeliveryHomeScreen.tsx | 151 ++++++++++++++++----
2 files changed, 129 insertions(+), 34 deletions(-)
diff --git a/src/components/OrderCard.tsx b/src/components/OrderCard.tsx
index ca4451b..67d196c 100644
--- a/src/components/OrderCard.tsx
+++ b/src/components/OrderCard.tsx
@@ -59,7 +59,7 @@ const OrderCard: React.FC = ({
{/* Details */}
-
+
{address}
@@ -73,7 +73,7 @@ const OrderCard: React.FC = ({
- Hora de entrega: {estimatedTime}
+ Tiempo estimado: {estimatedTime}
@@ -135,18 +135,18 @@ const styles = StyleSheet.create({
color: Colors.primary,
marginBottom: 8,
},
- details: {
- marginBottom: 4,
- },
detailRow: {
flexDirection: 'row',
alignItems: 'center',
- marginBottom: 8,
+ marginBottom: 10,
},
detailText: {
fontSize: FontSizes.b3.size,
color: Colors.textMain,
marginLeft: 8,
+ flexWrap: 'wrap',
+ maxWidth: '90%',
+ textAlign: 'left',
},
button: {
marginBottom: 8,
diff --git a/src/screens/delivery/DeliveryHomeScreen.tsx b/src/screens/delivery/DeliveryHomeScreen.tsx
index 43e9d0f..4136972 100644
--- a/src/screens/delivery/DeliveryHomeScreen.tsx
+++ b/src/screens/delivery/DeliveryHomeScreen.tsx
@@ -17,17 +17,63 @@ import { DeliveryService } from '../../services/delivery';
import { BranchService } from '../../services/branches';
import {
OrderDeliveryDetailedResponse,
- BranchResponse,
OrderDeliveryStatus,
} from '@pharmatech/sdk';
import { useAlert } from '../../components/AlertProvider'; // Importar el hook useAlert
+import * as Location from 'expo-location'; // Importar Location para obtener la ubicación actual
+import { Config } from '../../config'; // Importar la configuración de la API de Google Maps
+
+// Definir el tipo para un leg de la API de Google Maps Directions
+interface GoogleMapsLeg {
+ duration: {
+ value: number; // Duración en segundos
+ };
+}
+
+const calculateTravelTime = async (
+ origin: { latitude: number; longitude: number },
+ destination: { latitude: number; longitude: number },
+): Promise => {
+ try {
+ const response = await fetch(
+ `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&key=${Config.googleMapsApiKey}`,
+ );
+ const data = await response.json();
+
+ if (data.routes && data.routes.length > 0) {
+ // Extraer el tiempo estimado en segundos
+ const durationInSeconds = data.routes[0].legs.reduce(
+ (total: number, leg: GoogleMapsLeg) => total + leg.duration.value,
+ 0,
+ );
+
+ // Convertir a minutos
+ return Math.ceil(durationInSeconds / 60);
+ } else {
+ console.error('No se pudo calcular el tiempo estimado.');
+ return 0;
+ }
+ } catch (error) {
+ console.error('Error al calcular el tiempo estimado:', error);
+ return 0;
+ }
+};
+
+// Extender el tipo OrderDeliveryDetailedResponse
+type ExtendedOrderDeliveryDetailedResponse = OrderDeliveryDetailedResponse & {
+ formattedEstimatedTime: string; // Nueva propiedad para el tiempo formateado
+};
export default function DeliveryHomeScreen() {
const router = useRouter();
const { showAlert } = useAlert();
- const [orders, setOrders] = useState([]);
- const [branchNames, setBranchNames] = useState>({});
+ const [orders, setOrders] = useState(
+ [],
+ );
+ const [branchNames, setBranchNames] = useState<
+ Record
+ >({});
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [selectedOrder, setSelectedOrder] =
@@ -44,30 +90,85 @@ export default function DeliveryHomeScreen() {
const payload = JSON.parse(atob(jwt.split('.')[1]));
const employeeId = payload.sub;
+ // Obtener las órdenes asignadas
const response = await DeliveryService.getAssignedOrders(employeeId);
- const detailedOrders = await Promise.all(
- response.results.map(async (order) => {
- const details = await DeliveryService.getOrderDetails(order.id);
- return details;
- }),
- );
-
- const branchIds = [
- ...new Set(detailedOrders.map((order) => order.branchId)),
- ];
+ // Obtener las sucursales
const branches = await BranchService.findAll({ page: 1, limit: 100 });
const branchMap = branches.results.reduce(
- (acc: Record, branch: BranchResponse) => {
- if (branchIds.includes(branch.id)) {
- acc[branch.id] = branch.name;
- }
+ (
+ acc: Record<
+ string,
+ { name: string; latitude: number; longitude: number }
+ >,
+ branch,
+ ) => {
+ acc[branch.id] = {
+ name: branch.name,
+ latitude: branch.latitude,
+ longitude: branch.longitude,
+ };
return acc;
},
{},
);
-
setBranchNames(branchMap);
+
+ // Procesar las órdenes con cálculo de tiempo estimado
+ const detailedOrders: ExtendedOrderDeliveryDetailedResponse[] =
+ await Promise.all(
+ response.results.map(async (order) => {
+ const details = await DeliveryService.getOrderDetails(order.id);
+
+ // Obtener la ubicación actual del repartidor
+ const location = await Location.getCurrentPositionAsync({});
+ const deliveryLocation = {
+ latitude: location.coords.latitude,
+ longitude: location.coords.longitude,
+ };
+
+ // Obtener la ubicación de la sucursal
+ const branchLocation = {
+ latitude: branchMap[details.branchId]?.latitude || 0,
+ longitude: branchMap[details.branchId]?.longitude || 0,
+ };
+
+ // Obtener la ubicación del cliente
+ const customerLocation = {
+ latitude: details.address.latitude,
+ longitude: details.address.longitude,
+ };
+
+ // Calcular tiempos de viaje
+ const timeToBranch = await calculateTravelTime(
+ deliveryLocation,
+ branchLocation,
+ );
+ const timeToCustomer = await calculateTravelTime(
+ branchLocation,
+ customerLocation,
+ );
+
+ // Tiempo total estimado en minutos
+ const totalEstimatedTime = timeToBranch + timeToCustomer;
+
+ // Formatear el tiempo estimado
+ const formattedEstimatedTime =
+ totalEstimatedTime < 60
+ ? `${totalEstimatedTime} minutos`
+ : `${Math.floor(totalEstimatedTime / 60)} horas ${
+ totalEstimatedTime % 60
+ } minutos`;
+
+ // Retornar la orden con el tiempo estimado formateado
+ return {
+ ...details,
+ estimatedTime: new Date(), // Mantener el tipo original como Date
+ formattedEstimatedTime, // Asignar el tiempo formateado
+ };
+ }),
+ );
+
setOrders(detailedOrders);
} catch (error) {
console.error('Error al obtener las órdenes asignadas:', error);
@@ -127,14 +228,6 @@ export default function DeliveryHomeScreen() {
setSelectedOrder(null);
};
- const formatTime = (isoDate: string | Date): string => {
- const date = typeof isoDate === 'string' ? new Date(isoDate) : isoDate;
- return date.toLocaleTimeString('es-VE', {
- hour: '2-digit',
- minute: '2-digit',
- });
- };
-
const calculateElapsedTime = (createdAt: string): string => {
const now = new Date();
const createdDate = new Date(createdAt);
@@ -186,8 +279,10 @@ export default function DeliveryHomeScreen() {
orderCode={order.orderId.split('-')[0]} // Mostrar solo los primeros 8 caracteres
orderType="pedido"
address={order.address?.adress || 'Dirección no disponible'}
- branch={branchNames[order.branchId] || 'Sucursal no disponible'}
- estimatedTime={formatTime(order.estimatedTime)} // Pasar directamente la cadena ISO
+ branch={
+ branchNames[order.branchId]?.name || 'Sucursal no disponible'
+ }
+ estimatedTime={order.formattedEstimatedTime} // Mostrar el tiempo estimado
elapsedTime={calculateElapsedTime(order.createdAt)}
deliveryStatus={
order.deliveryStatus as
From a2db7355a63d55246a580c23836f0184839366ca Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Wed, 21 May 2025 13:19:02 -0400
Subject: [PATCH 4/9] Updating to latest sdk version
---
package-lock.json | 8 ++++----
package.json | 6 +++---
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 95c5fb0..ac7f9d9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.2.0",
"dependencies": {
"@expo-google-fonts/poppins": "^0.2.3",
- "@pharmatech/sdk": "^0.4.11",
+ "@pharmatech/sdk": "^0.4.19",
"@ptomasroos/react-native-multi-slider": "^2.2.2",
"@react-native-community/slider": "^4.5.6",
"@reduxjs/toolkit": "^2.6.1",
@@ -3592,9 +3592,9 @@
}
},
"node_modules/@pharmatech/sdk": {
- "version": "0.4.11",
- "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.11.tgz",
- "integrity": "sha512-iJChZa3uTsQrz6kLZJfnwrBTZOe2o6azPlwk/ByiMxA5V7UlPfVp8sDz4EIppYbBl2+uM4sLMGnQmiWatKEECA==",
+ "version": "0.4.19",
+ "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.19.tgz",
+ "integrity": "sha512-ZiYsoiVRtDjxs5eDqtUAZrEsUWMEZ9+q5XpSOSHt+gTcZLBzntIOO21ZyAm6ONXyRO6EORsOy2vlnnOv87Cs0g==",
"license": "MIT",
"dependencies": {
"axios": "^1.8.1"
diff --git a/package.json b/package.json
index 62c765a..bd58b0d 100644
--- a/package.json
+++ b/package.json
@@ -19,13 +19,14 @@
},
"dependencies": {
"@expo-google-fonts/poppins": "^0.2.3",
- "@pharmatech/sdk": "^0.4.11",
+ "@pharmatech/sdk": "^0.4.19",
"@ptomasroos/react-native-multi-slider": "^2.2.2",
"@react-native-community/slider": "^4.5.6",
"@reduxjs/toolkit": "^2.6.1",
"date-fns": "^4.1.0",
"expo": "~52.0.37",
"expo-constants": "~17.0.7",
+ "expo-dev-client": "~5.0.20",
"expo-font": "~13.0.4",
"expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
@@ -51,8 +52,7 @@
"react-native-svg": "^15.11.2",
"react-redux": "^9.2.0",
"socket.io-client": "^4.8.1",
- "tailwindcss": "^3.4.17",
- "expo-dev-client": "~5.0.20"
+ "tailwindcss": "^3.4.17"
},
"devDependencies": {
"@babel/core": "^7.25.2",
From 82411e0d4621fd7790d2343262acac98cd4e16c4 Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Sat, 24 May 2025 16:44:57 -0400
Subject: [PATCH 5/9] Adding expanded view of maps in delivery flow
---
src/components/DeliveryMap.tsx | 26 +--
src/components/HistoryMap.tsx | 16 +-
src/components/HistoryOrderCard.tsx | 9 +-
src/screens/delivery/DeliveryDetailScreen.tsx | 176 +++++++++++++-----
.../delivery/DeliveryHistoryDetailScreen.tsx | 94 ++++++++--
.../delivery/DeliveryHistoryScreen.tsx | 13 +-
6 files changed, 235 insertions(+), 99 deletions(-)
diff --git a/src/components/DeliveryMap.tsx b/src/components/DeliveryMap.tsx
index fa3e7e4..3546f1c 100644
--- a/src/components/DeliveryMap.tsx
+++ b/src/components/DeliveryMap.tsx
@@ -1,6 +1,12 @@
import React, { useEffect, useState } from 'react';
import MapView, { Marker, Polyline } from 'react-native-maps';
-import { StyleSheet, View, Alert, ActivityIndicator } from 'react-native';
+import {
+ StyleSheet,
+ View,
+ Alert,
+ ActivityIndicator,
+ ViewStyle,
+} from 'react-native';
import * as Location from 'expo-location';
import { Config } from '../config';
import { Colors } from '../styles/theme';
@@ -9,12 +15,14 @@ interface DeliveryMapProps {
deliveryState: number;
branchLocation: { latitude: number; longitude: number };
customerLocation: { latitude: number; longitude: number };
+ style?: ViewStyle; // Permitir estilos personalizados
}
const DeliveryMap: React.FC = ({
deliveryState,
branchLocation,
customerLocation,
+ style, // Recibir el estilo como prop
}) => {
const [deliveryLocation, setDeliveryLocation] = useState<{
latitude: number;
@@ -182,16 +190,16 @@ const DeliveryMap: React.FC = ({
if (isLoading) {
// Mostrar un indicador de carga mientras se obtienen los datos
return (
-
+
);
}
return (
-
+
= ({
const styles = StyleSheet.create({
container: {
- height: 300,
- borderRadius: 16,
- overflow: 'hidden',
- marginBottom: 16,
- },
- map: {
- flex: 1,
+ flex: 1, // Permitir que el mapa ocupe todo el espacio disponible
},
loadingContainer: {
- height: 300,
+ flex: 1, // Asegura que el indicador de carga ocupe todo el espacio disponible
justifyContent: 'center',
alignItems: 'center',
},
diff --git a/src/components/HistoryMap.tsx b/src/components/HistoryMap.tsx
index 7a0b0d7..ba62402 100644
--- a/src/components/HistoryMap.tsx
+++ b/src/components/HistoryMap.tsx
@@ -1,18 +1,20 @@
import React, { useEffect, useState } from 'react';
import MapView, { Marker, Polyline } from 'react-native-maps';
-import { StyleSheet, View, Alert } from 'react-native';
+import { StyleSheet, View, Alert, ViewStyle } from 'react-native';
import { Config } from '../config';
interface HistoryMapProps {
deliveryLocation: { latitude: number; longitude: number };
branchLocation: { latitude: number; longitude: number };
customerLocation: { latitude: number; longitude: number };
+ style?: ViewStyle; // Permitir estilos personalizados
}
const HistoryMap: React.FC = ({
deliveryLocation,
branchLocation,
customerLocation,
+ style, // Recibir el estilo como prop
}) => {
const [routeToBranch, setRouteToBranch] = useState<
{ latitude: number; longitude: number }[]
@@ -118,9 +120,9 @@ const HistoryMap: React.FC = ({
};
return (
-
+
= ({
const styles = StyleSheet.create({
container: {
- height: 300,
- borderRadius: 16,
- overflow: 'hidden',
- marginBottom: 16,
- },
- map: {
- flex: 1,
+ flex: 1, // Permitir que el mapa ocupe todo el espacio disponible
},
});
diff --git a/src/components/HistoryOrderCard.tsx b/src/components/HistoryOrderCard.tsx
index e52e37e..e70d09c 100644
--- a/src/components/HistoryOrderCard.tsx
+++ b/src/components/HistoryOrderCard.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { View, StyleSheet } from 'react-native';
import {
MapPinIcon,
- ClockIcon,
+ CalendarDaysIcon,
BuildingStorefrontIcon,
} from 'react-native-heroicons/outline';
import { CheckCircleIcon } from 'react-native-heroicons/solid';
@@ -84,9 +84,9 @@ const HistoryOrderCard: React.FC = ({
-
+
- Tiempo de entrega: {estimatedTime}
+ Fecha de entrega: {estimatedTime}
@@ -151,6 +151,9 @@ const styles = StyleSheet.create({
fontSize: FontSizes.b3.size,
color: Colors.textMain,
marginLeft: 8,
+ flexWrap: 'wrap', // Permite que el texto se ajuste en varias líneas
+ maxWidth: '90%', // Limita el ancho del texto al 90% del contenedor
+ textAlign: 'left',
},
button: {
marginBottom: 8,
diff --git a/src/screens/delivery/DeliveryDetailScreen.tsx b/src/screens/delivery/DeliveryDetailScreen.tsx
index ac5d5f5..0e0b25e 100644
--- a/src/screens/delivery/DeliveryDetailScreen.tsx
+++ b/src/screens/delivery/DeliveryDetailScreen.tsx
@@ -5,6 +5,8 @@ import {
ScrollView,
ActivityIndicator,
Image,
+ Modal,
+ TouchableOpacity,
} from 'react-native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import {
@@ -12,6 +14,7 @@ import {
BuildingStorefrontIcon,
MapPinIcon,
PhoneIcon,
+ XMarkIcon,
} from 'react-native-heroicons/solid';
import Badge from '../../components/Badge';
import PoppinsText from '../../components/PoppinsText';
@@ -48,6 +51,43 @@ const DeliveryDetailScreen: React.FC = () => {
>({});
const [deliveryStateBadge, setDeliveryStateBadge] = useState(0);
const router = useRouter();
+ const [isMapModalVisible, setIsMapModalVisible] = useState(false);
+
+ const branchLocation = {
+ latitude: branchNames[orderDetails?.branchId ?? '']?.latitude || 0,
+ longitude: branchNames[orderDetails?.branchId ?? '']?.longitude || 0,
+ };
+
+ const customerLocation = {
+ latitude: orderDetails?.address?.latitude || 0,
+ longitude: orderDetails?.address?.longitude || 0,
+ };
+
+ const deliveryState = useSelector(
+ (state: RootState) => state.delivery.deliveryState[id as string] || 0,
+ );
+
+ // Estados para las alertas
+ const [showAlert, setShowAlert] = useState(false);
+ const [alertType, setAlertType] = useState<'success' | 'error' | 'info'>(
+ 'info',
+ );
+ const [alertMessage, setAlertMessage] = useState('');
+
+ const deliveryStates = [
+ 'Buscando pedido en sucursal de origen',
+ 'Haciendo entrega del pedido',
+ ];
+
+ const buttonStates = [
+ 'Comenzar entrega',
+ 'Llegué a la sucursal',
+ 'Ya tengo los productos del pedido',
+ 'Ir a destino de entrega',
+ 'Ya hice la entrega',
+ ];
+
+ const [showConfirmationPopup, setShowConfirmationPopup] = useState(false);
useEffect(() => {
const fetchOrderDetails = async () => {
@@ -143,41 +183,21 @@ const DeliveryDetailScreen: React.FC = () => {
fetchOrder();
}, [orderDetails]);
- const deliveryState = useSelector(
- (state: RootState) => state.delivery.deliveryState[id as string] || 0,
- );
-
- // Estados para las alertas
- const [showAlert, setShowAlert] = useState(false);
- const [alertType, setAlertType] = useState<'success' | 'error' | 'info'>(
- 'info',
- );
- const [alertMessage, setAlertMessage] = useState('');
-
- const deliveryStates = [
- 'Buscando pedido en sucursal de origen',
- 'Haciendo entrega del pedido',
- ];
-
- const buttonStates = [
- 'Comenzar entrega',
- 'Llegué a la sucursal',
- 'Ya tengo los productos del pedido',
- 'Ir a destino de entrega',
- 'Ya hice la entrega',
- ];
-
- const [showConfirmationPopup, setShowConfirmationPopup] = useState(false);
-
- const branchLocation = {
- latitude: branchNames[orderDetails?.branchId ?? '']?.latitude || 0,
- longitude: branchNames[orderDetails?.branchId ?? '']?.longitude || 0,
- };
+ // Mostrar un indicador de carga mientras se obtienen los detalles del pedido o la orden
+ if (isOrderDetailsLoading || isFetchingOrder) {
+ return ;
+ }
- const customerLocation = {
- latitude: orderDetails?.address?.latitude || 0,
- longitude: orderDetails?.address?.longitude || 0,
- };
+ if (!orderDetails) {
+ // Mostrar un mensaje de error si no se encuentran los detalles del pedido
+ return (
+
+
+ No se encontraron datos del pedido.
+
+
+ );
+ }
const handleNextState = async () => {
try {
@@ -267,22 +287,6 @@ const DeliveryDetailScreen: React.FC = () => {
}
};
- if (isOrderDetailsLoading || isFetchingOrder) {
- // Mostrar un indicador de carga mientras se obtienen los detalles del pedido o la orden
- return ;
- }
-
- if (!orderDetails) {
- // Mostrar un mensaje de error si no se encuentran los detalles del pedido
- return (
-
-
- No se encontraron datos del pedido.
-
-
- );
- }
-
return (
{/* Mostrar alertas */}
@@ -407,6 +411,15 @@ const DeliveryDetailScreen: React.FC = () => {
deliveryState={deliveryState}
branchLocation={branchLocation}
customerLocation={customerLocation}
+ style={{ height: 300, marginBottom: 16 }}
+ />
+
+
);
};
@@ -674,6 +721,35 @@ const styles = StyleSheet.create({
top: 20,
zIndex: 1000,
},
+ expandButton: {
+ marginBottom: 12,
+ },
+ modalContainer: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: 16,
+ right: 16,
+ zIndex: 10,
+ backgroundColor: Colors.textWhite,
+ padding: 8,
+ borderRadius: 16,
+ },
+ fullScreenMapContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ modalFloatingButton: {
+ position: 'absolute',
+ bottom: 16,
+ left: 16,
+ right: 16,
+ zIndex: 10,
+ },
});
export default DeliveryDetailScreen;
diff --git a/src/screens/delivery/DeliveryHistoryDetailScreen.tsx b/src/screens/delivery/DeliveryHistoryDetailScreen.tsx
index 1d1b4c3..8dca6d3 100644
--- a/src/screens/delivery/DeliveryHistoryDetailScreen.tsx
+++ b/src/screens/delivery/DeliveryHistoryDetailScreen.tsx
@@ -5,9 +5,15 @@ import {
ScrollView,
ActivityIndicator,
Image,
+ Modal,
+ TouchableOpacity,
} from 'react-native';
import { useLocalSearchParams } from 'expo-router';
-import { PhoneIcon, EnvelopeIcon } from 'react-native-heroicons/solid';
+import {
+ PhoneIcon,
+ EnvelopeIcon,
+ XMarkIcon,
+} from 'react-native-heroicons/solid';
import Badge from '../../components/Badge';
import PoppinsText from '../../components/PoppinsText';
import CustomerAvatar from '../../components/CustomerAvatar';
@@ -21,6 +27,7 @@ import {
OrderDetailedResponse,
} from '@pharmatech/sdk';
import { UserService } from '../../services/user';
+import Button from '../../components/Button';
const DeliveryHistoryDetailScreen: React.FC = () => {
const { id } = useLocalSearchParams();
@@ -31,6 +38,8 @@ const DeliveryHistoryDetailScreen: React.FC = () => {
);
const [loading, setLoading] = useState(true);
+ const [isMapModalVisible, setIsMapModalVisible] = useState(false);
+
const [order, setOrder] = useState(
undefined,
);
@@ -111,19 +120,6 @@ const DeliveryHistoryDetailScreen: React.FC = () => {
minute: '2-digit',
},
);
- const creationTime = new Date(orderDetails.createdAt).toLocaleTimeString(
- 'es-VE',
- {
- hour: '2-digit',
- minute: '2-digit',
- },
- );
- const creationTimePlusOneHour = new Date(
- new Date(orderDetails.createdAt).getTime() + 60 * 43 * 1000,
- ).toLocaleTimeString('es-VE', {
- hour: '2-digit',
- minute: '2-digit',
- });
return (
@@ -211,23 +207,22 @@ const DeliveryHistoryDetailScreen: React.FC = () => {
+ {/* Historial de la entrega
+
Historial de la entrega
- {/* Contenedor de la información de la sucursal */}
+
- {/* Línea vertical punteada */}
+
- {/* Línea vertical punteada */}
- {/* Círculo middle */}
- {/* Círculo inferior */}
@@ -261,7 +256,7 @@ const DeliveryHistoryDetailScreen: React.FC = () => {
-
+ */}
{/* Recorrido de entrega */}
@@ -280,6 +275,15 @@ const DeliveryHistoryDetailScreen: React.FC = () => {
latitude: orderDetails.address.latitude,
longitude: orderDetails.address.longitude,
}}
+ style={{ marginBottom: 16, height: 300 }}
+ />
+
+ setIsMapModalVisible(true)}
+ style={styles.expandButton}
/>
{/* Pedido */}
@@ -334,6 +338,40 @@ const DeliveryHistoryDetailScreen: React.FC = () => {
+
+ {/* Modal para el mapa ampliado */}
+ setIsMapModalVisible(false)}
+ >
+
+ {/* Botón de cierre */}
+ setIsMapModalVisible(false)}
+ >
+
+
+
+ {/* Mapa ampliado */}
+
+
+
);
};
@@ -610,6 +648,22 @@ const styles = StyleSheet.create({
scrollSpacer: {
height: 64,
},
+ expandButton: {
+ marginBottom: 12,
+ },
+ modalContainer: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: 16,
+ right: 16,
+ zIndex: 10,
+ backgroundColor: Colors.textWhite,
+ padding: 8,
+ borderRadius: 16,
+ },
});
export default DeliveryHistoryDetailScreen;
diff --git a/src/screens/delivery/DeliveryHistoryScreen.tsx b/src/screens/delivery/DeliveryHistoryScreen.tsx
index 95dea26..5112bc5 100644
--- a/src/screens/delivery/DeliveryHistoryScreen.tsx
+++ b/src/screens/delivery/DeliveryHistoryScreen.tsx
@@ -119,10 +119,15 @@ const DeliveryHistoryScreen: React.FC = () => {
orderType="pedido"
address={order.address?.adress || 'Dirección no disponible'}
branch={branchNames[order.branchId] || 'Sucursal no disponible'}
- estimatedTime={calculateElapsedTime(
- order.createdAt,
- order.updatedAt,
- )} // Tiempo transcurrido entre creación y última actualización
+ estimatedTime={new Date(order.updatedAt).toLocaleDateString(
+ 'es-VE',
+ {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ },
+ )} // Fecha de la última actualización
elapsedTime={calculateElapsedTime(order.createdAt)}
completionTime={new Date(order.updatedAt).toLocaleTimeString(
'es-VE',
From 44e2cc4e3a622b82491b1c48d3baaa8007da598e Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Sun, 25 May 2025 15:40:19 -0400
Subject: [PATCH 6/9] Integrating delivery status websocket
---
src/hooks/useNotifications.ts | 2 +-
src/lib/deliverySocket/deliverySocket.ts | 52 +++++++++
src/lib/socketUrl.ts | 4 +-
src/redux/slices/deliverySlice.ts | 22 +++-
src/screens/delivery/DeliveryDetailScreen.tsx | 103 +++++++++++++++---
src/screens/delivery/DeliveryHomeScreen.tsx | 42 ++++++-
6 files changed, 202 insertions(+), 23 deletions(-)
create mode 100644 src/lib/deliverySocket/deliverySocket.ts
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 127bb31..d3a0832 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -67,7 +67,7 @@ export function NotificationsProvider(props: { children: ReactNode }) {
useEffect(() => {
const interval = setInterval(() => {
void refreshNotifications();
- }, 30_000); // ajusta intervalo según necesidad
+ }, 1200_000); // ajusta intervalo según necesidad
return () => clearInterval(interval);
}, [refreshNotifications]);
diff --git a/src/lib/deliverySocket/deliverySocket.ts b/src/lib/deliverySocket/deliverySocket.ts
new file mode 100644
index 0000000..5e5e3ec
--- /dev/null
+++ b/src/lib/deliverySocket/deliverySocket.ts
@@ -0,0 +1,52 @@
+import io, { Socket } from 'socket.io-client';
+import { SOCKET_URL } from '../socketUrl';
+import * as SecureStore from 'expo-secure-store';
+
+let socket: Socket | null = null;
+
+export const initializeSocket = async (): Promise => {
+ if (socket) return socket;
+
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ console.error('Token de autenticación no encontrado');
+ throw new Error('Token de autenticación no encontrado');
+ }
+
+ console.log('Inicializando WebSocket con URL:', SOCKET_URL);
+ console.log('Enviando token JWT:', token);
+
+ socket = io(SOCKET_URL, {
+ autoConnect: false,
+ transports: ['polling'], // Asegurarnos de que ambos transportes estén habilitados
+ transportOptions: {
+ polling: {
+ extraHeaders: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ },
+ });
+
+ socket.on('connect', () => {
+ console.log('WebSocket conectado exitosamente');
+ });
+
+ socket.on('connect_error', (error) => {
+ console.error('Error de conexión al WebSocket:', error);
+ });
+
+ socket.on('disconnect', (reason) => {
+ console.warn('WebSocket desconectado. Razón:', reason);
+ });
+
+ return socket;
+};
+
+export const disconnectSocket = () => {
+ if (socket) {
+ console.log('Desconectando WebSocket');
+ socket.disconnect();
+ socket = null;
+ }
+};
diff --git a/src/lib/socketUrl.ts b/src/lib/socketUrl.ts
index 65016c6..7788574 100644
--- a/src/lib/socketUrl.ts
+++ b/src/lib/socketUrl.ts
@@ -1,5 +1,5 @@
const devModeFlag = process.env.PHARMATECH_DEV_MODE === 'true';
-const devUrl = 'ws://api-dev-8jfx.onrender.com';
-const prodUrl = 'ws://api-d8h5.onrender.com';
+const devUrl = 'https://api-dev-8jfx.onrender.com';
+const prodUrl = 'https://api-d8h5.onrender.com';
export const SOCKET_URL = devModeFlag ? devUrl : prodUrl;
diff --git a/src/redux/slices/deliverySlice.ts b/src/redux/slices/deliverySlice.ts
index e2f7d42..4de3264 100644
--- a/src/redux/slices/deliverySlice.ts
+++ b/src/redux/slices/deliverySlice.ts
@@ -1,5 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { OrderDeliveryDetailedResponse } from '@pharmatech/sdk';
+import {
+ OrderDeliveryDetailedResponse,
+ OrderDeliveryStatus,
+} from '@pharmatech/sdk';
interface DeliveryState {
orders: Record; // Almacenar múltiples órdenes por ID
@@ -30,6 +33,15 @@ const deliverySlice = createSlice({
) {
state.deliveryState[action.payload.id] = action.payload.state;
},
+ updateDeliveryStatus(
+ state,
+ action: PayloadAction<{ id: string; status: OrderDeliveryStatus }>,
+ ) {
+ if (state.orders[action.payload.id]) {
+ state.orders[action.payload.id].deliveryStatus = action.payload
+ .status as OrderDeliveryStatus;
+ }
+ },
resetOrderState(state, action: PayloadAction) {
delete state.orders[action.payload];
delete state.deliveryState[action.payload];
@@ -37,6 +49,10 @@ const deliverySlice = createSlice({
},
});
-export const { setOrderDetails, setDeliveryState, resetOrderState } =
- deliverySlice.actions;
+export const {
+ setOrderDetails,
+ setDeliveryState,
+ updateDeliveryStatus,
+ resetOrderState,
+} = deliverySlice.actions;
export default deliverySlice.reducer;
diff --git a/src/screens/delivery/DeliveryDetailScreen.tsx b/src/screens/delivery/DeliveryDetailScreen.tsx
index 0e0b25e..be72c39 100644
--- a/src/screens/delivery/DeliveryDetailScreen.tsx
+++ b/src/screens/delivery/DeliveryDetailScreen.tsx
@@ -32,8 +32,14 @@ import { RootState } from '../../redux/store';
import {
setOrderDetails,
setDeliveryState,
+ updateDeliveryStatus,
} from '../../redux/slices/deliverySlice';
import Popup from '../../components/Popup';
+import {
+ initializeSocket,
+ disconnectSocket,
+} from '../../lib/deliverySocket/deliverySocket';
+import { Socket } from 'socket.io-client';
const DeliveryDetailScreen: React.FC = () => {
const { id } = useLocalSearchParams();
@@ -160,12 +166,10 @@ const DeliveryDetailScreen: React.FC = () => {
}
const orderId = orderDetails.orderId;
- console.log('ID de la orden:', orderId);
const order = await UserService.getOrder(orderId);
if (order.success) {
- console.log('Datos del pedido:', order.data);
setOrder(order.data);
} else {
console.error('Error al obtener el pedido:', order.error);
@@ -183,6 +187,66 @@ const DeliveryDetailScreen: React.FC = () => {
fetchOrder();
}, [orderDetails]);
+ // Configuración del WebSocket para recibir actualizaciones en tiempo real
+ useEffect(() => {
+ let socket: Socket;
+
+ const setupSocket = async () => {
+ try {
+ socket = await initializeSocket();
+ console.log('Conectando al WebSocket...');
+ socket.connect();
+
+ socket.on('connect', () => {
+ console.log('WebSocket conectado en DeliveryDetailScreen');
+ });
+
+ socket.on('connect_error', (error) => {
+ console.error(
+ 'Error de conexión al WebSocket en DeliveryDetailScreen:',
+ error,
+ );
+ });
+
+ socket.on('deliveryUpdated', (data: { id: string; status: string }) => {
+ console.log('Evento recibido: deliveryUpdated', data);
+ if (data.id === id) {
+ dispatch(
+ updateDeliveryStatus({
+ id: data.id,
+ status: data.status as OrderDeliveryStatus,
+ }),
+ );
+ }
+ });
+
+ socket.on('disconnect', (reason) => {
+ console.warn(
+ 'WebSocket desconectado en DeliveryDetailScreen. Razón:',
+ reason,
+ );
+ });
+ } catch (error) {
+ console.error(
+ 'Error configurando el WebSocket en DeliveryDetailScreen:',
+ error,
+ );
+ }
+ };
+
+ setupSocket();
+
+ return () => {
+ if (socket) {
+ console.log(
+ 'Limpiando listeners y desconectando WebSocket en DeliveryDetailScreen',
+ );
+ socket.off('deliveryUpdated');
+ disconnectSocket();
+ }
+ };
+ }, [id, dispatch]);
+
// Mostrar un indicador de carga mientras se obtienen los detalles del pedido o la orden
if (isOrderDetailsLoading || isFetchingOrder) {
return ;
@@ -207,29 +271,35 @@ const DeliveryDetailScreen: React.FC = () => {
return;
}
+ let nextStatus: OrderDeliveryStatus | null = null;
+
// Lógica para otros estados
switch (buttonStates[deliveryState]) {
case 'Comenzar entrega':
- await DeliveryService.updateOrderStatus(
- orderDetails!.id,
- OrderDeliveryStatus.WAITING_CONFIRMATION,
- );
+ nextStatus = OrderDeliveryStatus.WAITING_CONFIRMATION;
+ await DeliveryService.updateOrderStatus(orderDetails!.id, nextStatus);
break;
case 'Ya tengo los productos del pedido':
- await DeliveryService.updateOrderStatus(
- orderDetails!.id,
- OrderDeliveryStatus.PICKED_UP,
- );
+ nextStatus = OrderDeliveryStatus.PICKED_UP;
+ await DeliveryService.updateOrderStatus(orderDetails!.id, nextStatus);
break;
case 'Ir a destino de entrega':
- await DeliveryService.updateOrderStatus(
- orderDetails!.id,
- OrderDeliveryStatus.IN_ROUTE,
- );
+ nextStatus = OrderDeliveryStatus.IN_ROUTE;
+ await DeliveryService.updateOrderStatus(orderDetails!.id, nextStatus);
setDeliveryStateBadge(1); // Cambiar a "Haciendo entrega del pedido"
break;
}
+ if (nextStatus) {
+ // Emitir el evento al WebSocket
+ const socket = await initializeSocket();
+ socket.emit('deliveryUpdated', {
+ id: orderDetails!.id,
+ status: nextStatus,
+ });
+ }
+
+ // Mostrar alerta de éxito
setAlertType('info');
setAlertMessage('Se actualizó el estado del pedido.');
setShowAlert(true);
@@ -238,12 +308,14 @@ const DeliveryDetailScreen: React.FC = () => {
setShowAlert(false);
}, 2500);
+ // Actualizar el estado local
dispatch(
setDeliveryState({ id: id as string, state: deliveryState + 1 }),
);
} catch (error) {
console.error('Error al actualizar el estado del delivery:', error);
+ // Mostrar alerta de error
setAlertType('error');
setAlertMessage('Hubo un problema al actualizar el estado del pedido.');
setShowAlert(true);
@@ -436,8 +508,7 @@ const DeliveryDetailScreen: React.FC = () => {
productos
- {order?.details?.map((detail, index) => {
- console.log(`Producto ${index + 1}:`, detail); // Log para depuración
+ {order?.details?.map((detail) => {
return (
{
+ let socket: Socket;
+
+ const setupSocket = async () => {
+ try {
+ socket = await initializeSocket();
+ socket.connect();
+
+ socket.on('deliveryUpdated', (data: { id: string; status: string }) => {
+ setOrders((prevOrders) =>
+ prevOrders.map((order) =>
+ order.id === data.id
+ ? {
+ ...order,
+ deliveryStatus: data.status as OrderDeliveryStatus,
+ }
+ : order,
+ ),
+ );
+ });
+ } catch (error) {
+ console.error('Error configurando el WebSocket:', error);
+ }
+ };
+
+ setupSocket();
+
+ return () => {
+ if (socket) {
+ socket.off('deliveryUpdated');
+ disconnectSocket();
+ }
+ };
+ }, []);
+
const handleTakeOrder = (order: OrderDeliveryDetailedResponse) => {
const query = encodeURIComponent(JSON.stringify(order));
router.push(`/deliveryDetail/${order.id}?data=${query}`);
From b130695fb3fa0599c375510aa9b37030dd46e216 Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Sun, 25 May 2025 22:20:48 -0400
Subject: [PATCH 7/9] Integrating delivery real time location websocket
---
src/components/DeliveryMap.tsx | 53 ++--------------
src/screens/delivery/DeliveryDetailScreen.tsx | 63 +++++++++++++++++++
2 files changed, 68 insertions(+), 48 deletions(-)
diff --git a/src/components/DeliveryMap.tsx b/src/components/DeliveryMap.tsx
index 3546f1c..5ba7b4d 100644
--- a/src/components/DeliveryMap.tsx
+++ b/src/components/DeliveryMap.tsx
@@ -1,13 +1,6 @@
import React, { useEffect, useState } from 'react';
import MapView, { Marker, Polyline } from 'react-native-maps';
-import {
- StyleSheet,
- View,
- Alert,
- ActivityIndicator,
- ViewStyle,
-} from 'react-native';
-import * as Location from 'expo-location';
+import { StyleSheet, View, ActivityIndicator, ViewStyle } from 'react-native';
import { Config } from '../config';
import { Colors } from '../styles/theme';
@@ -15,19 +8,17 @@ interface DeliveryMapProps {
deliveryState: number;
branchLocation: { latitude: number; longitude: number };
customerLocation: { latitude: number; longitude: number };
- style?: ViewStyle; // Permitir estilos personalizados
+ deliveryLocation: { latitude: number; longitude: number } | null; // NUEVO
+ style?: ViewStyle;
}
const DeliveryMap: React.FC = ({
deliveryState,
branchLocation,
customerLocation,
- style, // Recibir el estilo como prop
+ deliveryLocation,
+ style,
}) => {
- const [deliveryLocation, setDeliveryLocation] = useState<{
- latitude: number;
- longitude: number;
- } | null>(null);
const [routeCoordinates, setRouteCoordinates] = useState<
{ latitude: number; longitude: number }[]
>([]);
@@ -36,40 +27,6 @@ const DeliveryMap: React.FC = ({
>([]);
const [isLoading, setIsLoading] = useState(true); // Estado de carga
- // Solicitar permisos y obtener la ubicación del delivery
- useEffect(() => {
- const getDeliveryLocation = async () => {
- try {
- const { status } = await Location.getForegroundPermissionsAsync();
- if (status !== 'granted') {
- const { status: newStatus } =
- await Location.requestForegroundPermissionsAsync();
- if (newStatus !== 'granted') {
- Alert.alert(
- 'Permiso denegado',
- 'No se pudo obtener la ubicación del delivery.',
- );
- return;
- }
- }
-
- const location = await Location.getCurrentPositionAsync({});
- setDeliveryLocation({
- latitude: location.coords.latitude,
- longitude: location.coords.longitude,
- });
- } catch (error) {
- console.error('Error al obtener la ubicación del delivery:', error);
- Alert.alert(
- 'Error',
- 'Hubo un problema al obtener la ubicación del delivery.',
- );
- }
- };
-
- getDeliveryLocation();
- }, []);
-
// Obtener la ruta desde Google Maps Directions API
const fetchRoute = async (
originLat: number,
diff --git a/src/screens/delivery/DeliveryDetailScreen.tsx b/src/screens/delivery/DeliveryDetailScreen.tsx
index be72c39..48c84c0 100644
--- a/src/screens/delivery/DeliveryDetailScreen.tsx
+++ b/src/screens/delivery/DeliveryDetailScreen.tsx
@@ -40,6 +40,7 @@ import {
disconnectSocket,
} from '../../lib/deliverySocket/deliverySocket';
import { Socket } from 'socket.io-client';
+import * as Location from 'expo-location';
const DeliveryDetailScreen: React.FC = () => {
const { id } = useLocalSearchParams();
@@ -94,6 +95,10 @@ const DeliveryDetailScreen: React.FC = () => {
];
const [showConfirmationPopup, setShowConfirmationPopup] = useState(false);
+ const [deliveryLocation, setDeliveryLocation] = useState<{
+ latitude: number;
+ longitude: number;
+ } | null>(null);
useEffect(() => {
const fetchOrderDetails = async () => {
@@ -247,6 +252,62 @@ const DeliveryDetailScreen: React.FC = () => {
};
}, [id, dispatch]);
+ // ...existing code...
+
+ useEffect(() => {
+ let socket: Socket;
+ let locationSubscription: Location.LocationSubscription | null = null;
+
+ const startTracking = async () => {
+ try {
+ socket = await initializeSocket();
+ socket.connect();
+
+ // Permisos de ubicación
+ const { status } = await Location.requestForegroundPermissionsAsync();
+ if (status !== 'granted') {
+ console.warn('Permiso de ubicación denegado');
+ return;
+ }
+
+ // Suscribirse a cambios de ubicación
+ locationSubscription = await Location.watchPositionAsync(
+ {
+ accuracy: Location.Accuracy.High,
+ timeInterval: 5000, // cada 5 segundos
+ distanceInterval: 10, // o cada 10 metros
+ },
+ (location) => {
+ const { latitude, longitude } = location.coords;
+ setDeliveryLocation({ latitude, longitude });
+
+ // Emitir coordenadas por WebSocket
+ if (orderDetails?.id) {
+ socket.emit('updateCoordinates', {
+ orderId: orderDetails.id,
+ latitude,
+ longitude,
+ });
+ }
+ },
+ );
+ } catch (error) {
+ console.error('Error en el tracking de ubicación:', error);
+ }
+ };
+
+ startTracking();
+
+ return () => {
+ if (locationSubscription) {
+ locationSubscription.remove();
+ }
+ if (socket) {
+ disconnectSocket();
+ }
+ };
+ }, [orderDetails?.id]);
+
// Mostrar un indicador de carga mientras se obtienen los detalles del pedido o la orden
if (isOrderDetailsLoading || isFetchingOrder) {
return ;
@@ -483,6 +544,7 @@ const DeliveryDetailScreen: React.FC = () => {
deliveryState={deliveryState}
branchLocation={branchLocation}
customerLocation={customerLocation}
+ deliveryLocation={deliveryLocation}
style={{ height: 300, marginBottom: 16 }}
/>
@@ -591,6 +653,7 @@ const DeliveryDetailScreen: React.FC = () => {
deliveryState={deliveryState}
branchLocation={branchLocation}
customerLocation={customerLocation}
+ deliveryLocation={deliveryLocation}
/>
{/* Botón flotante sobre el modal */}
From 3dfc79b5e8cc61d2ed663f1aa1ede1ad8e07d36f Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Thu, 29 May 2025 17:22:54 -0400
Subject: [PATCH 8/9] Merge branch 'dev' into fix/General-fixes
---
package-lock.json | 8 +-
package.json | 2 +-
src/app/_layout.tsx | 21 +
src/app/in-progress-order.tsx | 3 +
src/components/Card.tsx | 35 +-
src/components/CardButton.tsx | 7 +-
src/components/OrderSummary.tsx | 51 ++-
src/components/PaymentInfoForm.tsx | 46 +-
src/components/PaymentStatusMessage.tsx | 94 ++++-
src/components/SearchInput.tsx | 3 +-
src/hooks/useCart.ts | 20 +-
src/redux/slices/cartSlice.ts | 25 +-
src/screens/CartListScreen.tsx | 75 ++--
src/screens/CheckoutScreen.tsx | 535 ++++++++----------------
src/screens/InProgressOrderScreen.tsx | 463 ++++++++++++++++++++
src/screens/OrdersScreen.tsx | 3 +-
src/screens/ProductDetailScreen.tsx | 80 +++-
src/screens/tab/HomeScreen.tsx | 135 +++---
src/screens/tab/OffersScreen.tsx | 155 ++++++-
src/services/order.ts | 14 +
src/services/paymentConfirmation.ts | 1 +
src/services/paymentInformation.ts | 6 +-
src/types/Product.ts | 4 +-
src/types/api.d.ts | 1 +
src/utils/formatPrice.ts | 8 +
25 files changed, 1225 insertions(+), 570 deletions(-)
create mode 100644 src/app/in-progress-order.tsx
create mode 100644 src/screens/InProgressOrderScreen.tsx
create mode 100644 src/utils/formatPrice.ts
diff --git a/package-lock.json b/package-lock.json
index ac7f9d9..aa1a182 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.2.0",
"dependencies": {
"@expo-google-fonts/poppins": "^0.2.3",
- "@pharmatech/sdk": "^0.4.19",
+ "@pharmatech/sdk": "^0.4.21",
"@ptomasroos/react-native-multi-slider": "^2.2.2",
"@react-native-community/slider": "^4.5.6",
"@reduxjs/toolkit": "^2.6.1",
@@ -3592,9 +3592,9 @@
}
},
"node_modules/@pharmatech/sdk": {
- "version": "0.4.19",
- "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.19.tgz",
- "integrity": "sha512-ZiYsoiVRtDjxs5eDqtUAZrEsUWMEZ9+q5XpSOSHt+gTcZLBzntIOO21ZyAm6ONXyRO6EORsOy2vlnnOv87Cs0g==",
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/@pharmatech/sdk/-/sdk-0.4.21.tgz",
+ "integrity": "sha512-fxyXlgKN3qLxuGVg6bmRS5DhqfLOLsCYazffax5x0iYFFBIQHQBVRLltm5h84jvOEM0oA7ipD08VVXWMyBj/vQ==",
"license": "MIT",
"dependencies": {
"axios": "^1.8.1"
diff --git a/package.json b/package.json
index bd58b0d..1cd1ff6 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
},
"dependencies": {
"@expo-google-fonts/poppins": "^0.2.3",
- "@pharmatech/sdk": "^0.4.19",
+ "@pharmatech/sdk": "^0.4.21",
"@ptomasroos/react-native-multi-slider": "^2.2.2",
"@react-native-community/slider": "^4.5.6",
"@reduxjs/toolkit": "^2.6.1",
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 4aea9df..0440b97 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -157,6 +157,27 @@ export default function RootLayout() {
/>
({
+ headerTitle: '',
+ headerTransparent: true,
+ headerShown: true,
+ headerLeft: () => (
+ navigation.goBack()}
+ style={{
+ padding: 12,
+ marginLeft: -12,
+ flexDirection: 'row',
+ alignItems: 'center',
+ }}
+ >
+
+
+ ),
+ })}
+ />
+ = ({
id,
@@ -15,13 +16,17 @@ const ProductCard: React.FC = ({
imageUrl,
name,
category,
- //originalPrice,
- //discount,
+ originalPrice,
+ discount,
finalPrice,
getQuantity,
}) => {
- const { getItemQuantity, updateCartQuantity } = useCart();
+ const { getItemQuantity, addToCart } = useCart();
const router = useRouter();
+ const computedFinalPrice = discount
+ ? (finalPrice * (100 - discount)) / 100
+ : finalPrice;
+
return (
@@ -57,7 +62,15 @@ const ProductCard: React.FC = ({
{
if (getQuantity) getQuantity(quantity);
- updateCartQuantity(id, quantity);
+ // Asegura que price siempre sea number
+ addToCart({
+ id,
+ name,
+ price: originalPrice ?? 0,
+ quantity,
+ image: imageUrl,
+ discount: discount ?? 0,
+ });
}}
initialValue={getItemQuantity(id)}
/>
@@ -66,22 +79,18 @@ const ProductCard: React.FC = ({
{truncateString(name)}
- {/**
{discount && (
- ${originalPrice}
+ ${formatPrice(originalPrice ?? 0)}
- {discount}% // Comentado
+ {discount}%
- )}*/}
+ )}
- ${finalPrice}
+ ${formatPrice(computedFinalPrice)}
diff --git a/src/components/CardButton.tsx b/src/components/CardButton.tsx
index db632b2..ea62541 100644
--- a/src/components/CardButton.tsx
+++ b/src/components/CardButton.tsx
@@ -19,15 +19,16 @@ const CardButton: React.FC = ({
}) => {
const [count, setCount] = useState(initialValue);
const [showCounter, setShowCounter] = useState(false);
+ const [hasInteracted, setHasInteracted] = useState(false);
const toggleCounter = () => setShowCounter(count > 0);
useEffect(toggleCounter, [count]);
useEffect(() => {
- if (getValue) {
+ if (hasInteracted && getValue) {
getValue(count);
}
- if (syncQuantity) {
+ if (hasInteracted && syncQuantity) {
syncQuantity(count);
}
}, [count]);
@@ -37,11 +38,13 @@ const CardButton: React.FC = ({
}, [initialValue]);
const incrementCount = () => {
+ setHasInteracted(true);
setCount((prev) => prev + 1);
};
const decrementCount = () => {
if (count > 0) {
+ setHasInteracted(true);
setCount((prev) => prev - 1);
}
};
diff --git a/src/components/OrderSummary.tsx b/src/components/OrderSummary.tsx
index 978f236..5b441a0 100644
--- a/src/components/OrderSummary.tsx
+++ b/src/components/OrderSummary.tsx
@@ -5,6 +5,7 @@ import PoppinsText from './PoppinsText';
import { Colors, FontSizes } from '../styles/theme';
import type { CartItem } from '../redux/slices/cartSlice';
import { ChevronDownIcon, ChevronUpIcon } from 'react-native-heroicons/outline';
+import { formatPrice } from '../utils/formatPrice';
const OrderSummary = () => {
const { cartItems } = useCart();
@@ -14,26 +15,29 @@ const OrderSummary = () => {
(sum, item) => sum + item.price * item.quantity,
0,
);
- // const totalDiscount = cartItems.reduce(
- // (sum, item) => sum + item.price * (item.quantity * 0.1), // Comentado
- // 0,
- // );
+ const totalDiscount = cartItems.reduce(
+ (sum, item) =>
+ sum + item.price * item.quantity * ((item.discount ?? 0) / 100),
+ 0,
+ );
+ const total = subtotal - totalDiscount;
const renderItem = ({ item }: { item: CartItem }) => {
- // const discount = 10; // Comentado
- // const discountedPrice = item.price * (1 - discount / 100); // Comentado
- // const totalDiscountedPrice = discountedPrice * item.quantity; // Comentado
+ const discount = item.discount ?? 0;
+ const discountedPrice = item.price * (1 - discount / 100);
+ const totalDiscountedPrice = discountedPrice * item.quantity;
const totalOriginalPrice = item.price * item.quantity;
return (
- {/**
-
-
- -10%
-
- */}
+ {discount > 0 && (
+
+
+ -{discount}%
+
+
+ )}
{
{item.name}
- ${totalOriginalPrice.toFixed(2)}
+ ${formatPrice(totalDiscountedPrice)}
- {/**
- ${totalOriginalPrice.toFixed(2)}
- */}
+ ${formatPrice(totalOriginalPrice)}
+
- (${item.price} c/u)
+ (${formatPrice(discountedPrice)} c/u)
Cantidad: {item.quantity}
@@ -102,12 +105,20 @@ const OrderSummary = () => {
Subtotal
- ${subtotal.toFixed(2)}
+ ${formatPrice(subtotal)}
Descuentos
- -$0.00
+
+ -${formatPrice(totalDiscount)}
+
+
+
+ Total
+
+ ${formatPrice(total)}
+
>
diff --git a/src/components/PaymentInfoForm.tsx b/src/components/PaymentInfoForm.tsx
index a0d9d53..e8b9b73 100644
--- a/src/components/PaymentInfoForm.tsx
+++ b/src/components/PaymentInfoForm.tsx
@@ -7,12 +7,7 @@ import Dropdown from './Dropdown';
import { PharmaTech } from '@pharmatech/sdk';
interface Props {
- paymentMethod:
- | 'punto_de_venta'
- | 'efectivo'
- | 'transferencia'
- | 'pago_movil'
- | null;
+ paymentMethod: 'CARD' | 'CASH' | 'BANK_TRANSFER' | 'MOBILE_PAYMENT' | null;
total: string;
onValidationChange: (isValid: boolean) => void;
onBankChange: (value: string) => void;
@@ -90,11 +85,14 @@ const PaymentInfoForm: React.FC = ({
useEffect(() => {
const isValid =
paymentMethod !== null &&
- (paymentMethod === 'pago_movil' || paymentMethod === 'transferencia')
+ (paymentMethod === 'MOBILE_PAYMENT' || paymentMethod === 'BANK_TRANSFER')
? bank.trim() !== '' &&
- /^\d+$/.test(reference) &&
+ /^\d{4,}$/.test(reference) &&
reference.trim() !== '' &&
- /^\d{1,8}$/.test(documentNumber) &&
+ !/^0+$/.test(reference) &&
+ /^\d{7,8}$/.test(documentNumber) &&
+ !/^0+$/.test(documentNumber) &&
+ !/^0/.test(documentNumber) &&
/^\d{11}$/.test(phone)
: true;
@@ -129,8 +127,8 @@ const PaymentInfoForm: React.FC = ({
return (
- {(paymentMethod === 'pago_movil' ||
- paymentMethod === 'transferencia') && (
+ {(paymentMethod === 'MOBILE_PAYMENT' ||
+ paymentMethod === 'BANK_TRANSFER') && (
<>
Realiza el pago en la siguiente cuenta de Pharmatech
@@ -155,7 +153,7 @@ const PaymentInfoForm: React.FC = ({
- {paymentMethod === 'pago_movil' ? (
+ {paymentMethod === 'MOBILE_PAYMENT' ? (
= ({
placeholder="Ingrese la referencia"
getValue={handleReferenceChange}
fieldType="number"
- errorText="Debe ser un número válido"
- validation={(val) => /^\d+$/.test(val) && val.trim() !== ''}
- showIcon
+ errorText="Debe ser un número valido"
+ validation={(val) =>
+ /^\d{4,}$/.test(val) && val.trim() !== '' && !/^0+$/.test(val)
+ }
+ showIcon={reference.length > 0}
useDefaultValidation={false}
{...editableInputProps}
/>
@@ -220,9 +220,14 @@ const PaymentInfoForm: React.FC = ({
value={documentNumber}
getValue={handleDocumentNumberChange}
fieldType="number"
- errorText="El campo no debe estar vacío"
- validation={(val) => /^\d+$/.test(val) && val.trim() !== ''}
- showIcon
+ errorText="Debe ser un número de documento valido"
+ validation={(val) =>
+ /^\d{7,8}$/.test(val) &&
+ val.trim() !== '' &&
+ !/^0+$/.test(val) &&
+ !/^0/.test(val)
+ }
+ showIcon={documentNumber.length > 0}
useDefaultValidation={false}
{...editableInputProps}
/>
@@ -233,13 +238,14 @@ const PaymentInfoForm: React.FC = ({
getValue={handlePhoneChange}
fieldType="number"
errorText="Debe tener exactamente 11 dígitos"
- validation={(val) => /^\d{11}$/.test(val)}
- showIcon
+ validation={(val) => /^\d{11}$/.test(val) && !/^0+$/.test(val)}
+ showIcon={phone.length > 0}
useDefaultValidation={false}
{...editableInputProps}
/>
>
)}
+ {/* Si se requiere mostrar algo para CASH o CARD, agregar aquí */}
);
};
diff --git a/src/components/PaymentStatusMessage.tsx b/src/components/PaymentStatusMessage.tsx
index 0446869..51748ba 100644
--- a/src/components/PaymentStatusMessage.tsx
+++ b/src/components/PaymentStatusMessage.tsx
@@ -1,44 +1,98 @@
import React from 'react';
-import { CheckCircleIcon, XCircleIcon } from 'react-native-heroicons/outline';
+import {
+ CheckCircleIcon,
+ XCircleIcon,
+ ClockIcon,
+} from 'react-native-heroicons/outline';
import theme from '../styles/theme';
import { Text, View, StyleSheet } from 'react-native';
+type OrderStatus =
+ | 'requested'
+ | 'approved'
+ | 'ready_for_pickup'
+ | 'in_progress'
+ | 'canceled'
+ | 'completed';
+
interface PaymentStatusMessageProps {
- status: 'approved' | 'rejected';
+ orderStatus: OrderStatus;
orderNumber: string;
userName: string;
}
+const statusConfig: Record<
+ OrderStatus,
+ {
+ icon: React.ElementType;
+ color: string;
+ title: string;
+ message: (orderNumber: string, userName: string) => string;
+ }
+> = {
+ requested: {
+ icon: ClockIcon,
+ color: theme.Colors.semanticWarning,
+ title: 'Orden en espera',
+ message: () =>
+ `Estamos procesando tu orden. En un momento actualizaremos el estado de tu orden. Si tienes alguna duda, por favor contacta a nuestro equipo de soporte.`,
+ },
+ approved: {
+ icon: CheckCircleIcon,
+ color: theme.Colors.semanticSuccess,
+ title: 'Orden aprobada',
+ message: (_orderNumber, userName) =>
+ `¡Gracias por tu compra, ${userName}! Tu orden ha sido aprobada.`,
+ },
+ ready_for_pickup: {
+ icon: CheckCircleIcon,
+ color: theme.Colors.semanticSuccess,
+ title: 'Lista para recoger',
+ message: () =>
+ 'Tu pedido está listo para ser recogido en la sucursal seleccionada.',
+ },
+ in_progress: {
+ icon: ClockIcon,
+ color: theme.Colors.semanticWarning,
+ title: 'Orden en progreso',
+ message: () =>
+ 'Tu pedido está en progreso. Pronto comenzaremos a procesarlo.',
+ },
+ canceled: {
+ icon: XCircleIcon,
+ color: theme.Colors.semanticDanger,
+ title: 'Orden rechazada',
+ message: () =>
+ 'No pudimos procesar tu orden. Lamentamos informarte que hubo un problema al generar tu pedido.',
+ },
+ completed: {
+ icon: CheckCircleIcon,
+ color: theme.Colors.semanticSuccess,
+ title: 'Orden completada',
+ message: (_orderNumber, userName) =>
+ `¡Gracias por tu compra, ${userName}! Tu orden ha sido completada exitosamente.`,
+ },
+};
+
const PaymentStatusMessage: React.FC = ({
- status,
+ orderStatus,
orderNumber,
userName,
}) => {
+ const config = statusConfig[orderStatus] || statusConfig['requested'];
+ const Icon = config.icon;
+
return (
- {status === 'approved' ? (
-
- ) : (
-
- )}
+
- Orden #{orderNumber}
+ {config.title} #{orderNumber}
- {status === 'approved'
- ? `¡Gracias por tu compra, ${userName}!`
- : 'No pudimos procesar tu orden. Lamentamos informarte que hubo un problema al generar tu pedido.'}
+ {config.message(orderNumber, userName)}
diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx
index 63ffa96..20fc96c 100644
--- a/src/components/SearchInput.tsx
+++ b/src/components/SearchInput.tsx
@@ -17,6 +17,7 @@ import PoppinsText from './PoppinsText';
import { router } from 'expo-router';
import { ProductService } from '../services/products';
import { ProductPresentation } from '@pharmatech/sdk';
+import { formatPrice } from '../utils/formatPrice';
interface SearchInputProps {
placeholder?: string;
@@ -195,7 +196,7 @@ const SearchInput: React.FC = ({
color: Colors.primary,
}}
>
- $ {p?.price}
+ $ {formatPrice(p?.price)}
diff --git a/src/hooks/useCart.ts b/src/hooks/useCart.ts
index 05eae89..6fd2a91 100644
--- a/src/hooks/useCart.ts
+++ b/src/hooks/useCart.ts
@@ -15,6 +15,8 @@ export const useCart = () => {
const userId = useSelector((state: RootState) => state.cart.userId);
const addToCart = (item: CartItem) => {
+ if (item.quantity <= 0) return; // <-- NO agregar si cantidad es 0
+ console.log('[addToCart] Item añadido al carrito:', item); // <-- LOG
dispatch(addItem(item));
};
@@ -22,8 +24,22 @@ export const useCart = () => {
dispatch(removeItem(id));
};
- const updateCartQuantity = (id: string, quantity: number) => {
- dispatch(updateQuantity({ id, quantity }));
+ const updateCartQuantity = (
+ id: string,
+ quantity: number,
+ discount?: number,
+ price?: number,
+ ) => {
+ // Busca el item actual en el carrito
+ const item = cartItems.find((item) => item.id === id);
+ dispatch(
+ updateQuantity({
+ id,
+ quantity,
+ discount: typeof discount === 'number' ? discount : item?.discount,
+ price: typeof price === 'number' ? price : item?.price,
+ }),
+ );
};
const getItemQuantity = (id: string) => {
diff --git a/src/redux/slices/cartSlice.ts b/src/redux/slices/cartSlice.ts
index 26183bc..017b1d9 100644
--- a/src/redux/slices/cartSlice.ts
+++ b/src/redux/slices/cartSlice.ts
@@ -6,6 +6,7 @@ export type CartItem = {
price: number;
quantity: number;
image: string;
+ discount?: number;
};
type CartState = {
@@ -35,13 +36,16 @@ const cartSlice = createSlice({
}
},
addItem: (state, action: PayloadAction) => {
- const existingItem = state.items.find(
+ const existing = state.items.find(
(item) => item.id === action.payload.id,
);
- if (existingItem) {
- existingItem.quantity = action.payload.quantity;
+ if (existing) {
+ // Si ya existe, actualiza cantidad y descuento correctamente
+ existing.quantity = action.payload.quantity;
+ existing.price = action.payload.price;
+ existing.discount = action.payload.discount; // <-- ¡Asegúrate de actualizar el descuento!
} else {
- state.items.push(action.payload);
+ state.items.push({ ...action.payload });
}
state.total = calculateTotal(state.items);
},
@@ -51,11 +55,22 @@ const cartSlice = createSlice({
},
updateQuantity: (
state,
- action: PayloadAction<{ id: string; quantity: number }>,
+ action: PayloadAction<{
+ id: string;
+ quantity: number;
+ discount?: number;
+ price?: number;
+ }>,
) => {
const item = state.items.find((item) => item.id === action.payload.id);
if (item) {
item.quantity = action.payload.quantity;
+ if (typeof action.payload.discount === 'number') {
+ item.discount = action.payload.discount;
+ }
+ if (typeof action.payload.price === 'number') {
+ item.price = action.payload.price;
+ }
}
state.total = calculateTotal(state.items);
},
diff --git a/src/screens/CartListScreen.tsx b/src/screens/CartListScreen.tsx
index b5a6f51..35762bc 100644
--- a/src/screens/CartListScreen.tsx
+++ b/src/screens/CartListScreen.tsx
@@ -14,6 +14,7 @@ import type { CartItem } from '../redux/slices/cartSlice';
import Button from '../components/Button';
import { TrashIcon } from 'react-native-heroicons/outline';
import { useRouter } from 'expo-router';
+import { formatPrice } from '../utils/formatPrice';
const CartListScreen = () => {
const router = useRouter();
@@ -22,28 +23,34 @@ const CartListScreen = () => {
const subtotal = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
- ); // Total price sum
- // const totalDiscount = cartItems.reduce(
- // (sum, item) => sum + item.price * (item.quantity * 0.1), // Comentado
- // 0,
- // ); // Discount sum
- const total = subtotal; // Subtotal without discount
+ );
+ // Total price sum
+ const totalDiscount = cartItems.reduce(
+ (sum, item) =>
+ sum + item.price * item.quantity * ((item.discount ?? 0) / 100),
+ 0,
+ ); // Discount sum
+ const total = subtotal - totalDiscount; // Subtotal with discount
const renderItem = ({ item }: { item: CartItem }) => {
- // const discount = 10; // Comentado
- // const discountedPrice = item.price * (1 - discount / 100); // Comentado
- // const totalDiscountedPrice = discountedPrice * item.quantity; // Comentado
- const totalOriginalPrice = item.price * item.quantity; // Original total price
+ console.log('[CartListScreen] Renderizando item:', item); // <-- LOG
+ // Usar el descuento del item, si existe, si no 0
+ const discount = item.discount ?? 0;
+ const discountedPrice = item.price * (1 - discount / 100);
+ const totalDiscountedPrice = discountedPrice * item.quantity;
+ const totalOriginalPrice = item.price * item.quantity;
return (
{/* Discount badge */}
- {/*
-
- -{discount}%
-
- */}
+ {discount > 0 && (
+
+
+ -{discount}%
+
+
+ )}
{
{item.name}
-
- ${totalOriginalPrice.toFixed(2)}
- {/* Original total price */}
-
+
+
+ ${formatPrice(totalDiscountedPrice)}
+
+ {discount > 0 && (
+
+ ${formatPrice(totalOriginalPrice)}
+
+ )}
+
- (${item.price} c/u)
+ (${formatPrice(discountedPrice)} c/u)
updateCartQuantity(item.id, quantity)}
+ getValue={(quantity) =>
+ updateCartQuantity(item.id, quantity, item.discount ?? 0)
+ }
initialValue={item.quantity > 0 ? item.quantity : 0}
- syncQuantity={(quantity) => updateCartQuantity(item.id, quantity)}
+ syncQuantity={(quantity) =>
+ updateCartQuantity(item.id, quantity, item.discount ?? 0)
+ }
/>
removeFromCart(item.id)}
@@ -118,19 +135,19 @@ const CartListScreen = () => {
Subtotal
- ${subtotal.toFixed(2)}
+ ${formatPrice(subtotal)}
- {/*
+
Descuentos
- -${totalDiscount.toFixed(2)}
+ -${formatPrice(totalDiscount)}
- */}
+
Total
- ${total.toFixed(2)}
+ ${formatPrice(total)}
{
>
)}
+
);
};
@@ -264,6 +282,9 @@ const styles = StyleSheet.create({
color: Colors.textMain,
marginBottom: 20,
},
+ height: {
+ height: 64,
+ },
row: {
flexDirection: 'row',
justifyContent: 'space-between',
diff --git a/src/screens/CheckoutScreen.tsx b/src/screens/CheckoutScreen.tsx
index fc2f671..90ad041 100644
--- a/src/screens/CheckoutScreen.tsx
+++ b/src/screens/CheckoutScreen.tsx
@@ -1,18 +1,13 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import {
View,
StyleSheet,
ScrollView,
- TouchableOpacity,
Animated,
BackHandler,
+ RefreshControl,
} from 'react-native';
-import {
- ShoppingBagIcon,
- TruckIcon,
- MapPinIcon,
- ChevronLeftIcon,
-} from 'react-native-heroicons/outline';
+import { ShoppingBagIcon, TruckIcon } from 'react-native-heroicons/outline';
import { Colors, FontSizes } from '../styles/theme';
import RadioCard from '../components/RadioCard';
import OrderSummary from '../components/OrderSummary';
@@ -22,80 +17,55 @@ import Steps from '../components/Steps';
import PaymentMethods from '../components/PaymentMethods';
import PoppinsText from '../components/PoppinsText';
import LocationSelector from '../components/LocationSelector';
-import PaymentInfoForm from '../components/PaymentInfoForm';
import Coupon from '../components/Coupon';
-import PaymentStatusMessage from '../components/PaymentStatusMessage';
import { useRouter } from 'expo-router';
import { OrderService } from '../services/order';
-import { UserService } from '../services/user';
-import { OrderType, CreateOrder, CreateOrderDetail } from '../types/api.d';
import BranchMapModal from '../components/BranchMapModal';
import { useDispatch, useSelector } from 'react-redux';
import { clearCart } from '../redux/slices/cartSlice';
import { useFocusEffect } from '@react-navigation/native';
-import Popup from '../components/Popup'; // Import the Popup component
+import Popup from '../components/Popup';
import EmailVerificationModal from './tab/EmailVerificationModal';
import { RootState, AppDispatch } from '../redux/store';
import {
- setStep,
setOption,
setPayment,
setLocationId,
- setPaymentInfoValid,
- resetCheckout,
setCouponDiscount,
setCouponApplied,
} from '../redux/slices/checkoutSlice';
+import {
+ OrderType,
+ CreateOrder,
+ CreateOrderDetail,
+ PaymentMethod,
+} from '@pharmatech/sdk';
+import { formatPrice } from '../utils/formatPrice';
+
const CheckoutScreen = () => {
const dispatch = useDispatch();
- const {
- step,
- option,
- payment,
- locationId,
- paymentInfoValid,
- couponDiscount,
- couponApplied,
- } = useSelector((state: RootState) => state.checkout);
+ const { step, option, payment, locationId, couponDiscount, couponApplied } =
+ useSelector((state: RootState) => state.checkout);
const router = useRouter();
const { cartItems } = useCart();
- const [status, setStatus] = useState<'approved' | 'rejected'>('approved');
const [errorMessage, setErrorMessage] = useState(null);
- const [userName, setUserName] = useState('Usuario');
+ //const [userName, setUserName] = useState('Usuario');
const [modalVisible, setModalVisible] = useState(false);
const [selectedBranch, setSelectedBranch] = useState<{
name: string;
latitude: number;
longitude: number;
} | null>(null);
- const [orderNumber, setOrderNumber] = useState(null);
+ // const [orderNumber, setOrderNumber] = useState(null); // commented: unused
const [popupVisible, setPopupVisible] = useState(false);
const [popupMessages, setPopupMessages] = useState([]);
const [validationPopupVisible, setValidationPopupVisible] = useState(false);
const [emailVerificationModalVisible, setEmailVerificationModalVisible] =
useState(false); // Track modal visibility
-
- useEffect(() => {
- const fetchUserName = async () => {
- const response = await UserService.getProfile();
- if (response.success && response.data) {
- const { firstName, isValidated } = response.data;
- setUserName(firstName);
- if (!isValidated) {
- setValidationPopupVisible(true);
- }
- } else if (!response.success) {
- console.error(
- 'Error al obtener el nombre del usuario:',
- response.error,
- );
- }
- };
-
- fetchUserName();
- }, []);
+ // const [orderStatus, setOrderStatus] = useState(null); // commented: unused
+ // Suscribe al socket usando el hook; solo se conecta cuando orderNumber no es null
useFocusEffect(
React.useCallback(() => {
@@ -114,28 +84,29 @@ const CheckoutScreen = () => {
}, [step]),
);
- const isSimplifiedSteps =
- (option === 'pickup' && payment === 'punto_de_venta') ||
- (option === 'delivery' && payment === 'efectivo');
-
- const stepsLabels = isSimplifiedSteps
- ? ['Opciones de Compra', 'Confirmación de orden']
- : ['Opciones de Compra', 'Visualización de datos', 'Confirmación de orden'];
+ // Solo mostrar el paso 1
+ const stepsLabels = ['Opciones de Compra'];
const subtotal = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
- {
- /** const totalDiscount = cartItems.reduce(
- (sum, item) => sum + item.price * (item.quantity * 0.1),
+
+ // Calcula el descuento total de los productos (igual que OrderSummary)
+ const totalDiscount = cartItems.reduce(
+ (sum, item) =>
+ sum + item.price * item.quantity * ((item.discount ?? 0) / 100),
0,
- );*/
- }
- const subtotalAfterDiscount = subtotal; // - totalDiscount;
+ );
+
+ // Subtotal después de descuentos de productos
+ const subtotalAfterDiscount = subtotal - totalDiscount;
+
+ // Aplica el cupón sobre el subtotal ya descontado
const subtotalAfterCoupon = couponApplied
? subtotalAfterDiscount * (1 - couponDiscount / 100)
: subtotalAfterDiscount;
+
const total = subtotalAfterCoupon;
const renderFooterMessage = () => {
@@ -148,25 +119,21 @@ const CheckoutScreen = () => {
return null;
};
- const isValidUUID = (value: string | null): boolean => {
- const uuidRegex =
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
- return value !== null && uuidRegex.test(value);
- };
+ // const isValidUUID = (value: string | null): boolean => {
+ // const uuidRegex =
+ // /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ // return value !== null && uuidRegex.test(value);
+ // };
const handleContinue = async () => {
const missingFields: string[] = [];
- if (step < stepsLabels.length - 1) {
- // Validation for steps before the confirmation step
- if (step === 1) {
- if (!option) missingFields.push('Seleccionar una opción de compra.');
- if (!locationId)
- missingFields.push('Seleccionar una opción de locación.');
- if (!payment) missingFields.push('Seleccionar un método de pago.');
- } else if (step === 2 && !isSimplifiedSteps && !paymentInfoValid) {
- missingFields.push('Completar la información de pago.');
- }
+ // Validación solo para el paso 1
+ if (step === 1) {
+ if (!option) missingFields.push('Seleccionar una opción de compra.');
+ if (!locationId)
+ missingFields.push('Seleccionar una opción de locación.');
+ if (!payment) missingFields.push('Seleccionar un método de pago.');
if (missingFields.length > 0) {
setPopupMessages(missingFields);
@@ -174,197 +141,109 @@ const CheckoutScreen = () => {
return;
}
- dispatch(setStep(step + 1));
- } else if (step === stepsLabels.length - 1) {
- // Ensure payment form is valid before creating the order
- if (!isSimplifiedSteps && !paymentInfoValid) {
- setPopupMessages(['Completar la información de pago correctamente.']);
- setPopupVisible(true);
- return;
+ // Validar los productos del carrito
+ const products: CreateOrderDetail[] = cartItems
+ .filter((item) => item.quantity > 0)
+ .map((item) => ({
+ productPresentationId: item.id,
+ quantity: item.quantity,
+ }));
+
+ setErrorMessage(null);
+
+ let sdkPaymentMethod: PaymentMethod;
+ switch (payment) {
+ case 'efectivo':
+ sdkPaymentMethod = PaymentMethod.CASH;
+ break;
+ case 'punto_de_venta':
+ sdkPaymentMethod = PaymentMethod.CARD;
+ break;
+ case 'transferencia':
+ sdkPaymentMethod = PaymentMethod.BANK_TRANSFER;
+ break;
+ case 'pago_movil':
+ sdkPaymentMethod = PaymentMethod.MOBILE_PAYMENT;
+ break;
+ default:
+ sdkPaymentMethod = PaymentMethod.CASH;
}
- // Create the order on the penultimate step
- try {
- if (option === 'pickup' && !isValidUUID(locationId)) {
- setErrorMessage('La sucursal seleccionada no es válida.');
- setStatus('rejected');
- return;
- }
-
- if (option === 'delivery' && !isValidUUID(locationId)) {
- setErrorMessage('La dirección seleccionada no es válida.');
- setStatus('rejected');
- return;
- }
-
- if (!option) {
- setErrorMessage('Debe seleccionar una opción de compra.');
- setStatus('rejected');
- return;
- }
-
- setErrorMessage(null);
-
- // Validar los productos del carrito
- const products: CreateOrderDetail[] = cartItems
- .filter((item) => item.quantity > 0)
- .map((item) => ({
- productPresentationId: item.id,
- quantity: item.quantity, // Solo incluir los campos esperados
- }));
-
- if (products.length === 0) {
- setErrorMessage('No hay productos válidos en el carrito.');
- setStatus('rejected');
- return;
- }
-
- // Log del carrito y los productos seleccionados
- console.log('Productos en el carrito:', cartItems);
- console.log('Productos seleccionados para la orden:', products);
-
- // Construir el payload de la orden
- const orderPayload: CreateOrder = {
- type: option === 'pickup' ? OrderType.PICKUP : OrderType.DELIVERY,
- branchId: option === 'pickup' ? locationId || undefined : undefined,
- userAddressId:
- option === 'delivery' ? locationId || undefined : undefined,
- products,
- };
-
- // Log del payload que se enviará al backend
- console.log('Payload enviado al backend:', orderPayload);
+ // Construir el payload de la orden usando el método seleccionado
+ const orderPayload: CreateOrder = {
+ type: option === 'pickup' ? OrderType.PICKUP : OrderType.DELIVERY,
+ branchId: option === 'pickup' ? locationId || undefined : undefined,
+ userAddressId:
+ option === 'delivery' ? locationId || undefined : undefined,
+ products,
+ paymentMethod: sdkPaymentMethod,
+ ...(couponApplied && {
+ couponCode:
+ typeof couponDiscount === 'number' && couponDiscount > 0
+ ? 'COUPON'
+ : undefined,
+ }),
+ };
+ try {
// Enviar la orden al backend
const orderResponse = await OrderService.create(orderPayload);
- // Log de la respuesta del backend
- console.log('Respuesta del backend:', orderResponse);
-
- // Validar la respuesta
if (!orderResponse?.id) {
setErrorMessage(
'No pudimos procesar tu orden. Inténtalo nuevamente.',
);
- setStatus('rejected');
- return;
}
- // Guardar el número de orden generado
- setOrderNumber(orderResponse.id);
-
- console.log('Orden creada exitosamente:', orderResponse);
-
- setStatus('approved');
- dispatch(setStep(step + 1)); // Move to the confirmation step
+ // Redirigir a la pantalla de orden en progreso (step 2 y 3)
+ router.push({
+ pathname: '/in-progress-order',
+ params: {
+ orderNumber: orderResponse.id,
+ },
+ });
} catch (error) {
console.error('Error al procesar la orden:', error);
setErrorMessage('Ocurrió un error inesperado. Inténtalo nuevamente.');
- setStatus('rejected');
}
- } else if (step === stepsLabels.length) {
- // Limpiar el carrito al salir del flujo de checkout
- dispatch(clearCart());
- dispatch(resetCheckout());
- router.dismissAll();
- router.replace({
- pathname: '/(tabs)',
- });
- }
- };
-
- const handleGoBack = () => {
- if (step === stepsLabels.length) {
- // Si estamos en el último paso, no permitir retroceder
return;
}
-
- if (step === 1) {
- router.back();
- } else if (step > 1) {
- dispatch(setStep(step - 1));
- }
- };
-
- const handleOpenMapModal = () => {
- if (selectedBranch) {
- setModalVisible(true); // Open the modal only if a branch is selected
- } else {
- console.error('No branch selected to display on the map.');
- }
};
- const renderConfirmationContent = (status: 'approved' | 'rejected') => {
- if (option === 'pickup' && status === 'approved') {
- return (
- <>
-
- Tu pedido ya está listo para que pases por él en la sucursal
- indicada. En el mapa adjunto podrás ver la ubicación exacta para que
- llegues sin problemas.
-
-
- Sucursal de retiro:{' '}
- {selectedBranch?.name || '[Nombre de la sucursal]'}
-
- }
- onPress={handleOpenMapModal} // Open the modal
- />
- >
- );
- }
-
- if (option === 'delivery' && status === 'approved') {
- return (
- <>
-
- Estamos preparando tu pedido para enviarlo a la dirección indicada.
-
-
- Recibirás notificaciones cuando tu pedido esté en camino. En breve,
- un motorizado tomará tu orden.
-
- console.log('Información del repartidor')}
- />
- >
- );
- }
-
- if (status === 'rejected') {
- return (
- <>
-
- Lamentamos informarte que hubo un problema al generar tu pedido.
-
- console.log('Volver a Intentar')}
- />
- >
- );
- }
-
- return null;
- };
+ //const handleOpenMapModal = () => {
+ //if (selectedBranch) {
+ // setModalVisible(true); // Open the modal only if a branch is selected
+ // } else {
+ // console.error('No branch selected to display on the map.');
+ //}
+ // };
+
+ // const renderConfirmationContent = (status: OrderStatus | null) => {
+ // if (status === 'approved' && option === 'pickup') {
+ // return Tu pedido está listo para recoger.;
+ // }
+ // if (status === 'approved' && option === 'delivery') {
+ // return Tu pedido está en camino.;
+ // }
+ // return null;
+ // };
const handleApplyCoupon = (discountAmount: number) => {
dispatch(setCouponDiscount(discountAmount));
dispatch(setCouponApplied(true));
};
+ const [refreshing, setRefreshing] = useState(false); // <-- Agregado
+
+ const onRefresh = React.useCallback(() => {
+ setRefreshing(true);
+ // Aquí puedes recargar datos del carrito, usuario, etc.
+ // Simulación de recarga:
+ setTimeout(() => {
+ setRefreshing(false);
+ }, 1000);
+ }, []);
+
return (
<>
{
visible={emailVerificationModalVisible}
onClose={() => setEmailVerificationModalVisible(false)} // Close the modal
/>
-
+
+ }
+ >
- {step < stepsLabels.length && (
-
-
-
- )}
-
+ {/* Solo mostrar el step 1 */}
{
/>
-
{errorMessage && (
{errorMessage}
)}
-
- {step === 1 && (
- <>
-
- Opciones de Compra
-
-
-
- }
- selected={option === 'pickup'}
- onPress={() => {
- dispatch(setOption('pickup'));
- dispatch(setLocationId(null));
- }}
- />
-
-
- }
- selected={option === 'delivery'}
- onPress={() => {
- dispatch(setOption('delivery'));
- dispatch(setLocationId(null));
- }}
- />
-
-
- dispatch(setLocationId(val))}
- setSelectedBranch={setSelectedBranch}
+ {/* Solo el paso 1 */}
+
+ Opciones de Compra
+
+
+
+ }
+ selected={option === 'pickup'}
+ onPress={() => {
+ dispatch(setOption('pickup'));
+ dispatch(setLocationId(null));
+ }}
/>
-
- dispatch(setPayment(val))}
- />
-
- {renderFooterMessage() && (
-
- {renderFooterMessage()}
-
- )}
- >
- )}
-
- {step === 2 && !isSimplifiedSteps && (
- <>
-
- Visualización de datos
-
-
-
- dispatch(setPaymentInfoValid(isValid))
- }
- onBankChange={(value) => console.log('Bank changed:', value)}
- onReferenceChange={(value) =>
- console.log('Reference changed:', value)
- }
- onDocumentNumberChange={(value) =>
- console.log('Document number changed:', value)
- }
- onPhoneChange={(value) =>
- console.log('Phone changed:', value)
- }
- />
-
- >
- )}
-
- {step === stepsLabels.length && (
- <>
-
- Confirmación de Orden
-
-
+
+ }
+ selected={option === 'delivery'}
+ onPress={() => {
+ dispatch(setOption('delivery'));
+ dispatch(setLocationId(null));
+ }}
/>
-
- {renderConfirmationContent(status)}
-
- >
+
+
+ dispatch(setLocationId(val))}
+ setSelectedBranch={setSelectedBranch}
+ />
+
+ dispatch(setPayment(val))}
+ />
+
+ {renderFooterMessage() && (
+
+ {renderFooterMessage()}
+
)}
-
- {step !== 2 && step < stepsLabels.length && (
- dispatch(setCouponApplied(true))}
- />
- )}
+ dispatch(setCouponApplied(true))}
+ />
{couponApplied && (
@@ -529,25 +355,16 @@ const CheckoutScreen = () => {
>
)}
-
- {/* IVA:
-+${iva.toFixed(2)} */}
-
+
Total:
- ${total.toFixed(2)}
+ ${formatPrice(total)}
{
+ const { orderNumber } = useLocalSearchParams();
+
+ // Siempre inicia en step 3
+ const [step, setStep] = useState(3);
+ const [userName, setUserName] = useState('Usuario');
+ const [order, setOrder] = useState(null);
+ const [showValidationPopup, setShowValidationPopup] = useState(false);
+ const [paymentFormValid, setPaymentFormValid] = useState(false);
+ const [bank, setBank] = useState('');
+ const [reference, setReference] = useState('');
+ const [documentNumber, setDocumentNumber] = useState('');
+ const [phoneNumber, setPhoneNumber] = useState('');
+ const [backendResponse, setBackendResponse] = useState(null);
+ const [refreshing, setRefreshing] = useState(false);
+ const [isSubmittingPayment, setIsSubmittingPayment] = useState(false);
+ const router = useRouter();
+ const dispatch = useDispatch();
+
+ // Extrae la lógica de obtención de la orden
+ const fetchOrder = async () => {
+ if (!orderNumber) return;
+ try {
+ const sdk = PharmaTech.getInstance();
+ const jwt = await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ console.error('No JWT found in SecureStore');
+ return;
+ }
+ const orderData = await sdk.order.getById(orderNumber as string, jwt);
+ setOrder(orderData);
+
+ // Solo cambia a step 2 si status es 'approved' y paymentMethod es BANK_TRANSFER o MOBILE_PAYMENT
+ if (
+ orderData &&
+ orderData.status &&
+ orderData.status.toLowerCase() === 'approved' &&
+ orderData.paymentMethod &&
+ ['BANK_TRANSFER', 'MOBILE_PAYMENT'].includes(
+ orderData.paymentMethod.toUpperCase(),
+ )
+ ) {
+ setStep(2);
+ return;
+ }
+ setStep(3);
+ } catch (error) {
+ console.error('Error fetching order:', error);
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ const fetchUserName = async () => {
+ const response = await UserService.getProfile();
+ if (response.success && response.data) {
+ setUserName(response.data.firstName);
+ }
+ };
+ fetchUserName();
+ }, []);
+
+ useEffect(() => {
+ fetchOrder();
+ }, [orderNumber]);
+
+ // 1) socket-only: actualiza order.status
+ useEffect(() => {
+ let socket: Socket;
+ const setupSocket = async () => {
+ socket = await initializeSocket();
+ socket.connect();
+ socket.on('orderUpdated', (data: { orderId: string; status: string }) => {
+ console.log('[Socket] orderUpdated received →', data);
+ if (orderNumber && data.orderId === orderNumber) {
+ setOrder((prev) =>
+ prev ? { ...prev, status: data.status as OrderStatus } : prev,
+ );
+ setIsSubmittingPayment(false); // <-- Oculta el loader cuando llega el update
+ }
+ });
+ };
+ setupSocket();
+ return () => {
+ socket?.off('orderUpdated');
+ disconnectSocket();
+ };
+ }, []); // solo al montar
+
+ // 2) efecto separado: cada vez que 'order' cambia, recalcula el step
+ useEffect(() => {
+ if (!order) return;
+ const normalized = order.status.toLowerCase();
+ const pm = order.paymentMethod?.toLowerCase();
+ if (
+ normalized === 'approved' &&
+ (pm === 'bank_transfer' || pm === 'mobile_payment')
+ ) {
+ setStep(2);
+ } else {
+ setStep(3);
+ }
+ }, [order]);
+
+ const handleContinue = () => {
+ // Limpiar el carrito al salir del flujo de checkout
+ dispatch(clearCart());
+ dispatch(resetCheckout());
+ router.dismissAll();
+ router.replace({
+ pathname: '/(tabs)',
+ });
+ };
+
+ // Handler para pull-to-refresh
+ const onRefresh = async () => {
+ setRefreshing(true);
+ await fetchOrder();
+ };
+
+ return (
+ <>
+ {/* Alerta para mostrar la respuesta del backend */}
+ {backendResponse && (
+ setBackendResponse(null)}
+ />
+ )}
+
+ }
+ >
+
+
+
+
+ {step === 1 && (
+ <>
+
+ Opciones de Compra
+
+ >
+ )}
+ {/* Ocultar step 2 si el método de pago es CARD o CASH */}
+ {step === 2 &&
+ order &&
+ order.paymentMethod &&
+ !['CARD', 'CASH'].includes(order.paymentMethod.toUpperCase()) && (
+ <>
+
+ Visualización de datos
+
+
+ {isSubmittingPayment ? (
+
+
+
+ Procesando pago...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Total:
+
+
+ ${order.totalPrice ? order.totalPrice : ''}
+
+
+
+
+ {
+ if (!paymentFormValid) {
+ setShowValidationPopup(true);
+ return;
+ }
+ setIsSubmittingPayment(true); // <-- Muestra loader
+ try {
+ const sdk = PharmaTech.getInstance();
+ const jwt =
+ await SecureStore.getItemAsync('auth_token');
+ if (!jwt) {
+ setBackendResponse('No JWT found en SecureStore');
+ setIsSubmittingPayment(false);
+ return;
+ }
+ const paymentConfirmation: PaymentConfirmation = {
+ bank,
+ reference,
+ documentId: documentNumber,
+ phoneNumber,
+ orderId: order.id,
+ };
+ const response = await sdk.paymentConfirmation.create(
+ paymentConfirmation,
+ jwt,
+ );
+ setBackendResponse(
+ response && response.id
+ ? '¡Pago enviado correctamente!'
+ : 'Respuesta recibida del servidor.',
+ );
+ // NO cambies el step aquí, espera al socket
+ } catch (error: unknown) {
+ let errorMessage =
+ 'Error enviando confirmación de pago. Intenta nuevamente.';
+ function hasMessage(
+ e: unknown,
+ ): e is { message: string } {
+ return (
+ typeof e === 'object' &&
+ e !== null &&
+ 'message' in e &&
+ typeof (e as { message?: unknown }).message ===
+ 'string'
+ );
+ }
+ if (hasMessage(error)) {
+ errorMessage = error.message;
+ }
+ setBackendResponse(errorMessage);
+ setIsSubmittingPayment(false); // Oculta loader si hay error
+ }
+ }}
+ />
+
+
+ >
+ )}
+ {step === 3 && order && (
+ <>
+
+ Confirmacion de la orden
+
+
+ {/* Botón Volver al Home en un View con padding 20 */}
+
+
+
+
+
+
+
+
+
+ Total:
+
+
+ ${order.totalPrice ? order.totalPrice : ''}
+
+
+
+
+
+ >
+ )}
+
+
+ setShowValidationPopup(false),
+ }}
+ onClose={() => setShowValidationPopup(false)}
+ />
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ scrollContainer: {
+ flexGrow: 1,
+ justifyContent: 'flex-start', // Fuerza el contenido arriba SIEMPRE
+ },
+ container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ },
+ steps: {
+ marginTop: 30,
+ padding: 20,
+ // No uses flex ni justifyContent aquí
+ },
+ stepLabel: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ alignSelf: 'flex-start',
+ paddingVertical: 8,
+ },
+ purchaseOptionsTitle: {
+ fontSize: FontSizes.h5.size,
+ color: Colors.textMain,
+ alignSelf: 'flex-start',
+ padding: 20,
+ },
+ confirmationContainer: {
+ marginTop: 20,
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ },
+ buttonContainer: {
+ marginTop: 20,
+ alignItems: 'flex-end',
+ },
+ nextButton: {
+ color: Colors.primary,
+ fontSize: FontSizes.h5.size,
+ fontWeight: 'bold',
+ padding: 10,
+ width: '100%',
+ height: 50,
+ marginTop: 15,
+ marginBottom: 16,
+ },
+ whiteBackgroundContainer: {
+ backgroundColor: '#FFFFFF',
+ padding: 20,
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ marginTop: 20,
+ },
+ paymentInfoFormContainer: {
+ paddingHorizontal: 20,
+ paddingTop: 0,
+ },
+ totalContainer: {
+ width: '100%',
+ marginBottom: 10,
+ },
+ totalRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+ },
+ totalLabel: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.textMain,
+ },
+ totalAmount: {
+ fontSize: FontSizes.h5.size,
+ lineHeight: FontSizes.h5.lineHeight,
+ color: Colors.primary,
+ },
+ checkoutButton: {
+ marginTop: 24,
+ width: '100%',
+ },
+ loaderContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 40,
+ },
+});
+
+export default InProgressOrderScreen;
diff --git a/src/screens/OrdersScreen.tsx b/src/screens/OrdersScreen.tsx
index 73a5600..5433cbf 100644
--- a/src/screens/OrdersScreen.tsx
+++ b/src/screens/OrdersScreen.tsx
@@ -16,6 +16,7 @@ import { UserService } from '../services/user';
import { truncateString } from '../utils/commons';
import OrderBadge from '../components/OrderBadge';
import { useCart } from '../hooks/useCart';
+import { formatPrice } from '../utils/formatPrice';
const OrdersScreen = () => {
const [ordersList, setOrdersList] = useState(
@@ -159,7 +160,7 @@ const OrdersScreen = () => {
{/* Formatear fecha */}
- ${order.totalPrice.toFixed(2)}
+ ${formatPrice(order.totalPrice)}
{
const { id, productId } = useLocalSearchParams<{
@@ -291,7 +292,21 @@ const ProductDetailScreen: React.FC = () => {
);
};
- console.log('Current inventory state:', inventory); // Log para inspeccionar el estado de inventory
+ const getProductDiscount = () => {
+ // Busca promo en product.promo
+ if (product?.promo && typeof product.promo.discount === 'number') {
+ return product.promo.discount;
+ }
+ return 0;
+ };
+
+ const getOriginalPrice = () => product?.price ?? 0;
+ const getDiscount = () => getProductDiscount();
+ const getFinalPrice = () => {
+ const original = getOriginalPrice();
+ const discount = getDiscount();
+ return discount > 0 ? (original * (100 - discount)) / 100 : original;
+ };
return (
@@ -383,10 +398,24 @@ const ProductDetailScreen: React.FC = () => {
{/* Información del producto */}
- $ {product?.price}
- {/* {discount && ( // Comentado */}
- {/* -{discount}% */}
- {/* )} */}
+ {product ? (
+ getDiscount() > 0 ? (
+ <>
+
+ ${formatPrice(getFinalPrice())}
+
+
+ -{getDiscount()}%
+
+ >
+ ) : (
+
+ ${formatPrice(getOriginalPrice())}
+
+ )
+ ) : (
+
+ )}
{
{
- if (product?.id) updateCartQuantity(product.id, quantity);
+ if (product?.id && quantity > 0) {
+ const promo = product.promo;
+ const discount =
+ typeof promo?.discount === 'number' ? promo.discount : 0;
+ addToCart({
+ id: product.id,
+ name:
+ product.product.name +
+ ' ' +
+ product.presentation.name +
+ ' ' +
+ product.presentation.quantity +
+ ' ' +
+ product.presentation.measurementUnit,
+ price: product.price,
+ quantity,
+ image: images?.[0]?.url || 'https://via.placeholder.com/150',
+ discount,
+ });
+ }
}}
initialValue={getQuantity()}
/>
@@ -636,6 +684,20 @@ const styles = StyleSheet.create({
color: Colors.primary,
marginRight: 10,
},
+ originalPrice: {
+ fontSize: FontSizes.b1.size,
+ color: Colors.disableText,
+ textDecorationLine: 'line-through',
+ marginRight: 14,
+ },
+ discountBadge: {
+ fontSize: FontSizes.c1.size,
+ backgroundColor: Colors.semanticInfo,
+ borderRadius: 5,
+ padding: 4,
+ color: Colors.textWhite,
+ marginRight: 10,
+ },
productName: {
textAlign: 'center',
fontSize: FontSizes.h5.size,
@@ -698,12 +760,6 @@ const styles = StyleSheet.create({
alignItems: 'flex-end',
zIndex: 999,
},
- discount: {
- fontSize: FontSizes.b4.size,
- backgroundColor: Colors.semanticInfo,
- borderRadius: 5,
- padding: 4,
- },
});
export default ProductDetailScreen;
diff --git a/src/screens/tab/HomeScreen.tsx b/src/screens/tab/HomeScreen.tsx
index 8d1e2de..c919820 100644
--- a/src/screens/tab/HomeScreen.tsx
+++ b/src/screens/tab/HomeScreen.tsx
@@ -8,6 +8,7 @@ import { Colors, FontSizes } from '../../styles/theme';
import Carousel from '../../components/Carousel';
import { ProductService } from '../../services/products';
import { Product } from '../../types/Product';
+import type { Promo } from '@pharmatech/sdk';
import EmailVerificationModal from './EmailVerificationModal';
import { decodeJWT } from '../../helper/jwtHelper';
@@ -20,7 +21,7 @@ export default function HomeScreen() {
const router = useRouter();
const { showEmailVerification: showEmailVerificationParam } =
useLocalSearchParams();
- const { cartItems, addToCart, updateCartQuantity, setCartUserId } = useCart();
+ const { cartItems, updateCartQuantity, setCartUserId } = useCart();
const getItemQuantity = (productId: string) => {
const cartItem = cartItems.find((item) => item.id === productId.toString());
@@ -63,7 +64,60 @@ export default function HomeScreen() {
const productsData = await ProductService.getProducts(1, 20);
if (productsData.success) {
const pd = productsData.data.results;
- const carouselProducts = pd.map((p) => ({
+ const carouselProducts = pd.map((p) => {
+ // Usa el descuento real si hay promo, si no, no lo agregues
+ // @ts-expect-error: promo puede estar en p o en p.presentation
+ const promo: Promo | undefined = p.promo ?? p.presentation.promo;
+ const discount: Promo['discount'] | undefined = promo?.discount
+ ? Math.round(promo.discount * 100) / 100
+ : undefined;
+ return {
+ id: p.id,
+ presentationId: p.presentation.id,
+ productId: p.product.id,
+ imageUrl: p.product.images[0].url,
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ category: p.product.categories[0].name,
+ originalPrice: p.price,
+ ...(discount !== undefined ? { discount } : {}),
+ finalPrice: p.price,
+ quantity: getItemQuantity(p.id),
+ getQuantity: (quantity: number) => {
+ updateCartQuantity(p.id, quantity);
+ },
+ };
+ });
+
+ setProducts(carouselProducts);
+ } else {
+ console.log(productsData.error);
+ }
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const obtainRecommendedProducts = async () => {
+ setLoadingRecommendations(true);
+ try {
+ const recommendations = await ProductService.getRecommendations();
+ const carouselRecommendations = recommendations.results.map((p) => {
+ // Usa el descuento real si hay promo, si no, no lo agregues
+ // @ts-expect-error: promo puede estar en p o en p.presentation
+ const promo: Promo | undefined = p.promo ?? p.presentation.promo;
+ const discount: Promo['discount'] | undefined = promo?.discount
+ ? Math.round(promo.discount * 100) / 100
+ : undefined;
+ return {
id: p.id,
presentationId: p.presentation.id,
productId: p.product.id,
@@ -76,83 +130,16 @@ export default function HomeScreen() {
p.presentation.quantity +
' ' +
p.presentation.measurementUnit,
- category: p.product.categories[0].name,
- //originalPrice: p.price,
- //discount: 10,
+ category: p.product.categories[0]?.name || 'Sin categoría',
+ originalPrice: p.price,
+ ...(discount !== undefined ? { discount } : {}),
finalPrice: p.price,
quantity: getItemQuantity(p.id),
getQuantity: (quantity: number) => {
- addToCart({
- id: p.id,
- name:
- p.product.name +
- ' ' +
- p.presentation.name +
- ' ' +
- p.presentation.quantity +
- ' ' +
- p.presentation.measurementUnit,
- price: p.price,
- quantity,
- image:
- p.product.images?.[0]?.url || 'https://via.placeholder.com/150',
- });
updateCartQuantity(p.id, quantity);
},
- }));
-
- setProducts(carouselProducts);
- } else {
- console.log(productsData.error);
- }
- } catch (error) {
- console.error('Error fetching products:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const obtainRecommendedProducts = async () => {
- setLoadingRecommendations(true);
- try {
- const recommendations = await ProductService.getRecommendations();
- const carouselRecommendations = recommendations.results.map((p) => ({
- id: p.id,
- presentationId: p.presentation.id,
- productId: p.product.id,
- imageUrl: p.product.images[0].url,
- name:
- p.product.name +
- ' ' +
- p.presentation.name +
- ' ' +
- p.presentation.quantity +
- ' ' +
- p.presentation.measurementUnit,
- category: p.product.categories[0]?.name || 'Sin categoría',
- //originalPrice: p.price,
- //discount: 10,
- finalPrice: p.price,
- quantity: getItemQuantity(p.id),
- getQuantity: (quantity: number) => {
- addToCart({
- id: p.id,
- name:
- p.product.name +
- ' ' +
- p.presentation.name +
- ' ' +
- p.presentation.quantity +
- ' ' +
- p.presentation.measurementUnit,
- price: p.price,
- quantity,
- image:
- p.product.images?.[0]?.url || 'https://via.placeholder.com/150',
- });
- updateCartQuantity(p.id, quantity);
- },
- }));
+ };
+ });
setRecommendedProducts(carouselRecommendations);
} catch (error) {
diff --git a/src/screens/tab/OffersScreen.tsx b/src/screens/tab/OffersScreen.tsx
index 7a74c8c..adf3708 100644
--- a/src/screens/tab/OffersScreen.tsx
+++ b/src/screens/tab/OffersScreen.tsx
@@ -1,12 +1,141 @@
-import React from 'react';
-import { View, StyleSheet } from 'react-native';
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ FlatList,
+ StyleSheet,
+ ActivityIndicator,
+ ListRenderItemInfo,
+} from 'react-native';
import PoppinsText from '../../components/PoppinsText';
-import { Colors } from '../../styles/theme';
+import { Colors, FontSizes } from '../../styles/theme';
+import Card from '../../components/Card';
+import { ProductService } from '../../services/products';
+import type { Promo } from '@pharmatech/sdk';
export default function OffersScreen() {
+ const [offers, setOffers] = useState<
+ Array<{
+ id: string;
+ presentationId: string;
+ productId: string;
+ imageUrl: string;
+ name: string;
+ category: string;
+ originalPrice: number;
+ discount: number;
+ finalPrice: number;
+ quantity: number;
+ getQuantity: (quantity: number) => void;
+ }>
+ >([]);
+ const [loading, setLoading] = useState(false);
+
+ function hasPromo(item: {
+ [key: string]: unknown;
+ }): item is { promo?: Promo } {
+ return 'promo' in item && typeof item.promo !== 'undefined';
+ }
+
+ const loadOffers = async () => {
+ setLoading(true);
+ const res = await ProductService.getProducts(1, 50);
+ if (res.success) {
+ const promoItems = res.data.results.filter(
+ (item) => hasPromo(item) && Boolean(item.promo?.discount),
+ );
+
+ const mappedPromoItems = promoItems.map((p) => {
+ const promo: Promo | undefined = hasPromo(p) ? p.promo : undefined;
+ const discount: Promo['discount'] = promo?.discount ?? 0;
+
+ return {
+ id: p.id,
+ presentationId: p.presentation.id,
+ productId: p.product.id,
+ imageUrl:
+ p.product.images?.[0]?.url || 'https://via.placeholder.com/150',
+ name:
+ p.product.name +
+ ' ' +
+ p.presentation.name +
+ ' ' +
+ p.presentation.quantity +
+ ' ' +
+ p.presentation.measurementUnit,
+ category: p.product.categories?.[0]?.name || 'Sin categoría',
+ originalPrice: p.price,
+ discount,
+ finalPrice: p.price,
+ quantity: 0,
+ getQuantity: () => 0,
+ };
+ });
+ setOffers(mappedPromoItems);
+ } else {
+ console.error('Error al cargar productos:', res.error);
+ }
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ loadOffers();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ const renderOffer = ({
+ item,
+ }: ListRenderItemInfo<{
+ id: string;
+ presentationId: string;
+ productId: string;
+ imageUrl: string;
+ name: string;
+ category: string;
+ originalPrice: number;
+ discount: number;
+ finalPrice: number;
+ quantity: number;
+ getQuantity: (quantity: number) => void;
+ }>) => {
+ return (
+
+ );
+ };
+
return (
- Pantalla Ofertas
+
+ Ofertas especiales
+
+ item.id}
+ numColumns={2}
+ contentContainerStyle={styles.list}
+ renderItem={renderOffer}
+ columnWrapperStyle={styles.columnWrapper}
+ onRefresh={loadOffers}
+ refreshing={loading}
+ />
);
@@ -14,6 +143,24 @@ export default function OffersScreen() {
const styles = StyleSheet.create({
container: {
+ flex: 1,
+ backgroundColor: Colors.bgColor,
+ paddingHorizontal: 5,
+ },
+ title: {
+ fontSize: FontSizes.s1.size,
+ color: Colors.textMain,
+ paddingHorizontal: 15,
+ },
+ list: {
+ paddingBottom: 20,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ columnWrapper: {
+ justifyContent: 'space-between',
+ },
+ loaderContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
diff --git a/src/services/order.ts b/src/services/order.ts
index 81b1f2d..da3c1ae 100644
--- a/src/services/order.ts
+++ b/src/services/order.ts
@@ -26,4 +26,18 @@ export const OrderService = {
throw new Error(extractErrorMessage(error));
}
},
+
+ getById: async (orderId: string) => {
+ try {
+ const token = await SecureStore.getItemAsync('auth_token');
+ if (!token) {
+ throw new Error('Token de autenticación no encontrado');
+ }
+ const orderDetail = await api.order.getById(orderId, token);
+ return orderDetail;
+ } catch (error) {
+ console.error('Error en OrderService.getById:', error);
+ throw new Error(extractErrorMessage(error));
+ }
+ },
};
diff --git a/src/services/paymentConfirmation.ts b/src/services/paymentConfirmation.ts
index 7a8c33e..de25636 100644
--- a/src/services/paymentConfirmation.ts
+++ b/src/services/paymentConfirmation.ts
@@ -24,6 +24,7 @@ export const PaymentConfirmationService = {
'phoneNumber',
'bank',
'reference',
+ 'orderId',
];
for (const field of requiredFields) {
if (!paymentConfirmation[field]) {
diff --git a/src/services/paymentInformation.ts b/src/services/paymentInformation.ts
index bc627ae..6016004 100644
--- a/src/services/paymentInformation.ts
+++ b/src/services/paymentInformation.ts
@@ -13,11 +13,11 @@ export const PaymentInformationService = {
throw new Error('Token de autenticación no encontrado');
}
- // Llamar al método findAll con el argumento directamente
+ // Call the API method
const response = await api.paymentInformation.findAll(paymentMethod);
- // La respuesta ahora es directamente un array de PaymentInfoResponse
- return [response];
+ // Devolver la respuesta directamente (se asume que ya es un array)
+ return response;
} catch (error) {
console.error('Error en PaymentInformationService.findAll:', error);
throw new Error(extractErrorMessage(error));
diff --git a/src/types/Product.ts b/src/types/Product.ts
index b037903..a759249 100644
--- a/src/types/Product.ts
+++ b/src/types/Product.ts
@@ -5,8 +5,8 @@ export type Product = {
imageUrl: string;
name: string;
category: string;
- //originalPrice: number;
- //discount: number;
+ originalPrice?: number;
+ discount?: number;
finalPrice: number;
quantity: number;
getQuantity: (quantity: number) => void;
diff --git a/src/types/api.d.ts b/src/types/api.d.ts
index 1cfae9b..2cf6ed0 100644
--- a/src/types/api.d.ts
+++ b/src/types/api.d.ts
@@ -201,6 +201,7 @@ export interface PaymentConfirmation {
reference: string;
documentId: string;
phoneNumber: string;
+ orderId: string;
}
export interface PaymentConfirmationResponse extends BaseModel {
diff --git a/src/utils/formatPrice.ts b/src/utils/formatPrice.ts
new file mode 100644
index 0000000..8e0c1e4
--- /dev/null
+++ b/src/utils/formatPrice.ts
@@ -0,0 +1,8 @@
+export function formatPrice(value: number, options?: Intl.NumberFormatOptions) {
+ // Divide entre 100 y muestra con dos decimales
+ return (value / 100).toLocaleString('es-VE', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ ...options,
+ });
+}
From 27f9f698d665bcc2c264f10414f66b07f2bf09c7 Mon Sep 17 00:00:00 2001
From: miglesiasDev <1001.28391672.ucla@gmail.com>
Date: Thu, 29 May 2025 19:51:03 -0400
Subject: [PATCH 9/9] Fixes for products with no stock
---
src/components/CardButton.tsx | 42 +++++++++++++++++++++++++----
src/screens/ProductDetailScreen.tsx | 41 +++++++++++++++++++++++++++-
src/screens/tab/HomeScreen.tsx | 4 ++-
3 files changed, 80 insertions(+), 7 deletions(-)
diff --git a/src/components/CardButton.tsx b/src/components/CardButton.tsx
index ea62541..1ab08d0 100644
--- a/src/components/CardButton.tsx
+++ b/src/components/CardButton.tsx
@@ -9,6 +9,8 @@ interface CardButtonProps {
initialValue: number;
size?: number;
syncQuantity?: (count: number) => void;
+ disabled?: boolean;
+ showNoStockAlert?: () => void; // <-- nuevo prop opcional
}
const CardButton: React.FC = ({
@@ -16,6 +18,8 @@ const CardButton: React.FC = ({
initialValue = 0,
size = 4,
syncQuantity,
+ disabled = false, // <-- Valor por defecto
+ showNoStockAlert, // <-- Add this line
}) => {
const [count, setCount] = useState(initialValue);
const [showCounter, setShowCounter] = useState(false);
@@ -38,11 +42,19 @@ const CardButton: React.FC = ({
}, [initialValue]);
const incrementCount = () => {
+ if (disabled) {
+ if (showNoStockAlert) showNoStockAlert();
+ return;
+ }
setHasInteracted(true);
setCount((prev) => prev + 1);
};
const decrementCount = () => {
+ if (disabled) {
+ if (showNoStockAlert) showNoStockAlert();
+ return;
+ }
if (count > 0) {
setHasInteracted(true);
setCount((prev) => prev - 1);
@@ -50,6 +62,7 @@ const CardButton: React.FC = ({
};
const showCounterIncrement = () => {
+ if (disabled) return; // <-- No permitir si está deshabilitado
setShowCounter(true);
incrementCount();
};
@@ -58,18 +71,34 @@ const CardButton: React.FC = ({
{!showCounter ? (
{
+ if (disabled && showNoStockAlert) showNoStockAlert();
+ else showCounterIncrement();
+ }}
>
) : (
-
+
{
+ if (disabled && showNoStockAlert) showNoStockAlert();
+ else decrementCount();
+ }}
>
@@ -80,7 +109,10 @@ const CardButton: React.FC = ({
{
+ if (disabled && showNoStockAlert) showNoStockAlert();
+ else incrementCount();
+ }}
>
diff --git a/src/screens/ProductDetailScreen.tsx b/src/screens/ProductDetailScreen.tsx
index d0728a4..816fe4b 100644
--- a/src/screens/ProductDetailScreen.tsx
+++ b/src/screens/ProductDetailScreen.tsx
@@ -41,6 +41,7 @@ import {
import Button from '../components/Button';
import { BranchService } from '../services/branches';
import { formatPrice } from '../utils/formatPrice';
+import Alert from '../components/Alerts';
const ProductDetailScreen: React.FC = () => {
const { id, productId } = useLocalSearchParams<{
@@ -62,6 +63,7 @@ const ProductDetailScreen: React.FC = () => {
const [hoverRating, setHoverRating] = useState(0);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [isLoadingInventory, setIsLoadingInventory] = useState(false);
+ const [showNoStockAlert, setShowNoStockAlert] = useState(false);
const imagesScrollRef = useRef(null);
//const discount = 10;
const router = useRouter();
@@ -308,6 +310,8 @@ const ProductDetailScreen: React.FC = () => {
return discount > 0 ? (original * (100 - discount)) / 100 : original;
};
+ console.log('Stock actual:', product?.stock);
+
return (
@@ -339,6 +343,18 @@ const ProductDetailScreen: React.FC = () => {
+ {/* ALERTA EN LA PARTE SUPERIOR */}
+ {showNoStockAlert && (
+
+ setShowNoStockAlert(false)}
+ />
+
+ )}
+
{/* Carrusel de imágenes */}
{
setShowNoStockAlert(true)}
getValue={(quantity) => {
+ const stock = product?.stock ?? 0;
+ // Si no hay stock o la cantidad supera el stock, muestra alerta y no actualices
+ if (stock === 0 || quantity > stock) {
+ setShowNoStockAlert(true);
+ return;
+ }
+ // Si la cantidad es 0, elimina del carrito
+ if (product?.id && quantity === 0) {
+ updateCartQuantity(product.id, 0);
+ return;
+ }
+ // Si hay stock y cantidad válida, agrega o actualiza en el carrito
if (product?.id && quantity > 0) {
const promo = product.promo;
const discount =
typeof promo?.discount === 'number' ? promo.discount : 0;
+ updateCartQuantity(product.id, quantity);
addToCart({
id: product.id,
name:
@@ -633,7 +665,6 @@ const ProductDetailScreen: React.FC = () => {
});
}
}}
- initialValue={getQuantity()}
/>
@@ -653,6 +684,14 @@ const styles = StyleSheet.create({
width: width,
height: 200,
},
+ alertContainer: {
+ position: 'absolute',
+ width: 326,
+ left: '50%',
+ marginLeft: -163,
+ top: 20,
+ zIndex: 1000,
+ },
imageIndicators: {
flexDirection: 'row',
justifyContent: 'center',
diff --git a/src/screens/tab/HomeScreen.tsx b/src/screens/tab/HomeScreen.tsx
index c919820..92a711f 100644
--- a/src/screens/tab/HomeScreen.tsx
+++ b/src/screens/tab/HomeScreen.tsx
@@ -64,7 +64,9 @@ export default function HomeScreen() {
const productsData = await ProductService.getProducts(1, 20);
if (productsData.success) {
const pd = productsData.data.results;
- const carouselProducts = pd.map((p) => {
+ // Filtra solo los que tengan stock > 0
+ const availableProducts = pd.filter((p) => p.stock > 0);
+ const carouselProducts = availableProducts.map((p) => {
// Usa el descuento real si hay promo, si no, no lo agregues
// @ts-expect-error: promo puede estar en p o en p.presentation
const promo: Promo | undefined = p.promo ?? p.presentation.promo;