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 7198700..0440b97 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/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/components/DeliveryMap.tsx b/src/components/DeliveryMap.tsx index c987c23..5ba7b4d 100644 --- a/src/components/DeliveryMap.tsx +++ b/src/components/DeliveryMap.tsx @@ -1,73 +1,31 @@ import React, { useEffect, useState } from 'react'; import MapView, { Marker, Polyline } from 'react-native-maps'; -import { StyleSheet, View, Alert } 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'; interface DeliveryMapProps { deliveryState: number; branchLocation: { latitude: number; longitude: number }; customerLocation: { latitude: number; longitude: number }; + deliveryLocation: { latitude: number; longitude: number } | null; // NUEVO + style?: ViewStyle; } const DeliveryMap: React.FC = ({ deliveryState, branchLocation, customerLocation, + deliveryLocation, + style, }) => { - const [deliveryLocation, setDeliveryLocation] = useState<{ - latitude: number; - longitude: number; - } | null>(null); const [routeCoordinates, setRouteCoordinates] = useState< { latitude: number; longitude: number }[] >([]); const [customerRouteCoordinates, setCustomerRouteCoordinates] = useState< { latitude: number; longitude: number }[] >([]); - - // 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, - }); - - // 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( - 'Error', - 'Hubo un problema al obtener la ubicación del delivery.', - ); - } - }; - - getDeliveryLocation(); - }, []); + const [isLoading, setIsLoading] = useState(true); // Estado de carga // Obtener la ruta desde Google Maps Directions API const fetchRoute = async ( @@ -96,11 +54,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 +90,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 +144,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 +201,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 styles = StyleSheet.create({ container: { - height: 300, - borderRadius: 16, - overflow: 'hidden', - marginBottom: 16, + flex: 1, // Permitir que el mapa ocupe todo el espacio disponible }, - map: { - flex: 1, + loadingContainer: { + 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/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/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/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/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/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 89beb9f..35762bc 100644 --- a/src/screens/CartListScreen.tsx +++ b/src/screens/CartListScreen.tsx @@ -161,6 +161,7 @@ const CartListScreen = () => { )} + ); }; @@ -170,6 +171,7 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: Colors.bgColor, padding: 16, + paddingTop: -16, }, header: { fontSize: FontSizes.h5.size, @@ -280,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/DirectionChangeScreen.tsx b/src/screens/DirectionChangeScreen.tsx index 261a9a6..500ef20 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 62485cd..3e6f478 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 983b8d2..8649dc8 100644 --- a/src/screens/DirectionScreen.tsx +++ b/src/screens/DirectionScreen.tsx @@ -198,6 +198,7 @@ const styles = StyleSheet.create({ flexGrow: 1, backgroundColor: Colors.bgColor, padding: 20, + paddingTop: -20, }, alertContainer: { position: 'absolute', @@ -210,7 +211,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 8b18a07..78ba921 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 aa48501..5433cbf 100644 --- a/src/screens/OrdersScreen.tsx +++ b/src/screens/OrdersScreen.tsx @@ -198,6 +198,7 @@ const styles = StyleSheet.create({ flexGrow: 1, backgroundColor: Colors.bgColor, padding: 20, + paddingTop: -20, }, alertContainer: { position: 'absolute', @@ -210,7 +211,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/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/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 9930ef0..637f2ac 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 { useLocalSearchParams, useRouter } from 'expo-router'; @@ -8,11 +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 { fromCheckout } = useLocalSearchParams(); + const navigation = useNavigation(); const [selectedLocation, setSelectedLocation] = useState<{ latitude: number; @@ -131,6 +140,15 @@ const SelectLocationScreen = () => { {/* Header con input para mostrar la dirección */} + + + { const { id } = useLocalSearchParams(); @@ -41,48 +51,29 @@ const DeliveryDetailScreen: React.FC = () => { const [order, setOrder] = useState( undefined, ); - useEffect(() => { - const fetchOrder = async () => { - try { - if (!orderDetails) { - console.error( - 'No se encontraron detalles de la orden de tipo delivery.', - ); - return; - } - - const orderId = orderDetails.orderId; // Extraer el ID de la orden - console.log('ID de la orden:', orderId); - - const order = await UserService.getOrder(orderId); // Usar el ID de la orden + 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(); + const [isMapModalVisible, setIsMapModalVisible] = useState(false); - if (order.success) { - console.log('Datos del pedido:', order.data); // Log para verificar los datos - setOrder(order.data); - } else { - console.error('Error al obtener el pedido:', order.error); - } - } catch (error) { - console.error('Error en fetchOrder:', error); - } finally { - setLoading(false); - } - }; + const branchLocation = { + latitude: branchNames[orderDetails?.branchId ?? '']?.latitude || 0, + longitude: branchNames[orderDetails?.branchId ?? '']?.longitude || 0, + }; - fetchOrder(); - }, [orderDetails]); + const customerLocation = { + latitude: orderDetails?.address?.latitude || 0, + longitude: orderDetails?.address?.longitude || 0, + }; const deliveryState = useSelector( (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'>( @@ -104,26 +95,31 @@ const DeliveryDetailScreen: React.FC = () => { ]; const [showConfirmationPopup, setShowConfirmationPopup] = 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 [deliveryLocation, setDeliveryLocation] = useState<{ + latitude: number; + longitude: number; + } | null>(null); useEffect(() => { const fetchOrderDetails = async () => { - if (!orderDetails) { - const details = await DeliveryService.getOrderDetails(id as string); - dispatch(setOrderDetails({ id: id as string, details })); + 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 { - // Obtener todas las sucursales y mapear sus nombres y coordenadas const branches = await BranchService.findAll({ page: 1, limit: 100 }); const branchMap = branches.results.reduce( ( @@ -145,14 +141,188 @@ const DeliveryDetailScreen: React.FC = () => { setBranchNames(branchMap); } catch (error) { - console.error('Error al obtener los detalles del pedido:', error); + console.error('Error al obtener las sucursales:', error); } finally { - setLoading(false); + setIsFetchingOrder(false); } }; - fetchOrderDetails(); - }, [id, orderDetails, dispatch]); + 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) { + // 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; + + const order = await UserService.getOrder(orderId); + + if (order.success) { + setOrder(order.data); + } else { + console.error('Error al obtener el pedido:', order.error); + } + } catch (error) { + console.error('Error en fetchOrder:', error); + } finally { + setIsFetchingOrder(false); // Finalizar el indicador de carga + if (timerId) { + clearTimeout(timerId); // Cancelar el temporizador si los datos están disponibles + } + } + }; + + 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]); + + // ...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 ; + } + + 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 { @@ -162,29 +332,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); @@ -193,12 +369,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); @@ -242,20 +420,6 @@ const DeliveryDetailScreen: React.FC = () => { } }; - if (loading) { - return ; - } - - if (!orderDetails) { - return ( - - - No se encontraron datos del pedido. - - - ); - } - return ( {/* Mostrar alertas */} @@ -380,6 +544,16 @@ const DeliveryDetailScreen: React.FC = () => { deliveryState={deliveryState} branchLocation={branchLocation} customerLocation={customerLocation} + deliveryLocation={deliveryLocation} + style={{ height: 300, marginBottom: 16 }} + /> + +