From 8bfeee75af3296609ceac0df45a9acf51b99b9d1 Mon Sep 17 00:00:00 2001 From: Mvjtb <1001.25854794.ucla@gmail.com> Date: Wed, 14 May 2025 15:57:15 -0400 Subject: [PATCH 01/28] Fixes del sprint 5 para el checkout --- src/components/PaymentInfoForm.tsx | 30 ++++++--- src/components/PaymentStatusMessage.tsx | 87 +++++++++++++++++++------ src/screens/CheckoutScreen.tsx | 58 ++++++++++++----- src/services/order.ts | 14 ++++ 4 files changed, 141 insertions(+), 48 deletions(-) diff --git a/src/components/PaymentInfoForm.tsx b/src/components/PaymentInfoForm.tsx index a0d9d53..2b20240 100644 --- a/src/components/PaymentInfoForm.tsx +++ b/src/components/PaymentInfoForm.tsx @@ -92,9 +92,12 @@ const PaymentInfoForm: React.FC = ({ paymentMethod !== null && (paymentMethod === 'pago_movil' || paymentMethod === 'transferencia') ? bank.trim() !== '' && - /^\d+$/.test(reference) && + /^\d{4,}$/.test(reference) && reference.trim() !== '' && - /^\d{1,8}$/.test(documentNumber) && + !/^0+$/.test(reference) && // no solo ceros + /^\d{7,8}$/.test(documentNumber) && + !/^0+$/.test(documentNumber) && // no solo ceros + !/^0/.test(documentNumber) && // no inicia en 0 /^\d{11}$/.test(phone) : true; @@ -208,9 +211,11 @@ const PaymentInfoForm: React.FC = ({ 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 +225,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,8 +243,8 @@ 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} /> diff --git a/src/components/PaymentStatusMessage.tsx b/src/components/PaymentStatusMessage.tsx index 0446869..9cb0f57 100644 --- a/src/components/PaymentStatusMessage.tsx +++ b/src/components/PaymentStatusMessage.tsx @@ -1,44 +1,91 @@ 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' + | 'rejected'; + 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 Espera', + message: () => + 'Tu pedido está en espera. Pronto comenzaremos a procesarlo.', + }, + rejected: { + 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.', + }, +}; + 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/screens/CheckoutScreen.tsx b/src/screens/CheckoutScreen.tsx index fc2f671..fee4794 100644 --- a/src/screens/CheckoutScreen.tsx +++ b/src/screens/CheckoutScreen.tsx @@ -61,7 +61,6 @@ const CheckoutScreen = () => { const router = useRouter(); const { cartItems } = useCart(); - const [status, setStatus] = useState<'approved' | 'rejected'>('approved'); const [errorMessage, setErrorMessage] = useState(null); const [userName, setUserName] = useState('Usuario'); const [modalVisible, setModalVisible] = useState(false); @@ -76,6 +75,7 @@ const CheckoutScreen = () => { const [validationPopupVisible, setValidationPopupVisible] = useState(false); const [emailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false); // Track modal visibility + const [orderStatus, setOrderStatus] = useState(null); useEffect(() => { const fetchUserName = async () => { @@ -187,19 +187,19 @@ const CheckoutScreen = () => { try { if (option === 'pickup' && !isValidUUID(locationId)) { setErrorMessage('La sucursal seleccionada no es válida.'); - setStatus('rejected'); + setOrderStatus(null); return; } if (option === 'delivery' && !isValidUUID(locationId)) { setErrorMessage('La dirección seleccionada no es válida.'); - setStatus('rejected'); + setOrderStatus(null); return; } if (!option) { setErrorMessage('Debe seleccionar una opción de compra.'); - setStatus('rejected'); + setOrderStatus(null); return; } @@ -215,7 +215,7 @@ const CheckoutScreen = () => { if (products.length === 0) { setErrorMessage('No hay productos válidos en el carrito.'); - setStatus('rejected'); + setOrderStatus(null); return; } @@ -246,21 +246,34 @@ const CheckoutScreen = () => { setErrorMessage( 'No pudimos procesar tu orden. Inténtalo nuevamente.', ); - setStatus('rejected'); + + setOrderStatus(null); return; } // Guardar el número de orden generado setOrderNumber(orderResponse.id); + // Obtener el status real de la orden desde el backend + try { + const orderDetail = await OrderService.getById(orderResponse.id); + setOrderStatus(orderDetail.status); // <-- status real del backend + } catch (err) { + console.error( + 'No se pud git checkout -b fix/sprint-5-checkout-fixeso obtener el status de la orden:', + err, + ); + setOrderStatus(orderResponse.status ?? null); + } + console.log('Orden creada exitosamente:', orderResponse); - setStatus('approved'); dispatch(setStep(step + 1)); // Move to the confirmation step } catch (error) { console.error('Error al procesar la orden:', error); setErrorMessage('Ocurrió un error inesperado. Inténtalo nuevamente.'); - setStatus('rejected'); + + setOrderStatus(null); } } else if (step === stepsLabels.length) { // Limpiar el carrito al salir del flujo de checkout @@ -294,8 +307,8 @@ const CheckoutScreen = () => { } }; - const renderConfirmationContent = (status: 'approved' | 'rejected') => { - if (option === 'pickup' && status === 'approved') { + const renderConfirmationContent = (orderStatus: string | null) => { + if (option === 'pickup' && orderStatus === 'approved') { return ( <> @@ -319,7 +332,7 @@ const CheckoutScreen = () => { ); } - if (option === 'delivery' && status === 'approved') { + if (option === 'delivery' && orderStatus === 'approved') { return ( <> @@ -340,7 +353,7 @@ const CheckoutScreen = () => { ); } - if (status === 'rejected') { + if (orderStatus === 'rejected') { return ( <> @@ -497,13 +510,22 @@ const CheckoutScreen = () => { Confirmación de Orden - + {orderStatus ? ( + + ) : null} - {renderConfirmationContent(status)} + {renderConfirmationContent(orderStatus)} )} 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)); + } + }, }; From 443d8a15df3357c47ca3c47496812d7dff40709e Mon Sep 17 00:00:00 2001 From: Mvjtb <1001.25854794.ucla@gmail.com> Date: Fri, 16 May 2025 19:09:53 -0400 Subject: [PATCH 02/28] Add order status handling and websocket integration for real-time updates --- src/components/PaymentStatusMessage.tsx | 21 ++-- src/hooks/useOrderSocket.ts | 59 ++++++++++++ src/screens/CheckoutScreen.tsx | 122 +++++++----------------- src/utils/orderStatus.ts | 20 ++++ 4 files changed, 128 insertions(+), 94 deletions(-) create mode 100644 src/hooks/useOrderSocket.ts create mode 100644 src/utils/orderStatus.ts diff --git a/src/components/PaymentStatusMessage.tsx b/src/components/PaymentStatusMessage.tsx index 9cb0f57..51748ba 100644 --- a/src/components/PaymentStatusMessage.tsx +++ b/src/components/PaymentStatusMessage.tsx @@ -12,7 +12,8 @@ type OrderStatus = | 'approved' | 'ready_for_pickup' | 'in_progress' - | 'rejected'; + | 'canceled' + | 'completed'; interface PaymentStatusMessageProps { orderStatus: OrderStatus; @@ -32,11 +33,10 @@ const statusConfig: Record< requested: { icon: ClockIcon, color: theme.Colors.semanticWarning, - title: 'Orden En Espera', + 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.`, + `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, @@ -54,17 +54,24 @@ const statusConfig: Record< in_progress: { icon: ClockIcon, color: theme.Colors.semanticWarning, - title: 'Orden En Espera', + title: 'Orden en progreso', message: () => - 'Tu pedido está en espera. Pronto comenzaremos a procesarlo.', + 'Tu pedido está en progreso. Pronto comenzaremos a procesarlo.', }, - rejected: { + 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 = ({ diff --git a/src/hooks/useOrderSocket.ts b/src/hooks/useOrderSocket.ts new file mode 100644 index 0000000..ff7dd91 --- /dev/null +++ b/src/hooks/useOrderSocket.ts @@ -0,0 +1,59 @@ +// src/hooks/useOrderSocket.ts +import { useEffect, useRef } from 'react'; +import io, { Socket } from 'socket.io-client'; +import * as SecureStore from 'expo-secure-store'; +import { SOCKET_URL } from '../lib/socketUrl'; + +export type OrderStatus = + | 'requested' + | 'approved' + | 'ready_for_pickup' + | 'in_progress' + | 'canceled' + | 'completed'; + +export interface Order { + id: string; + status: OrderStatus; + // …otros campos de la orden… +} + +export function useOrderSocket( + orderId: string | null, + onOrderUpdate: (order: Order) => void, +) { + const socketRef = useRef(null); + + useEffect(() => { + if (!orderId) return; + let isMounted = true; + + SecureStore.getItemAsync('auth_token').then((token) => { + if (!isMounted || !token) return; + const sock = io(SOCKET_URL, { + transports: ['websocket'], + auth: { token }, + }); + socketRef.current = sock; + + sock.on('connect', () => console.log('[WS] Conectado con ID:', sock.id)); + sock.on('disconnect', (reason) => + console.log('[WS] Desconectado:', reason), + ); + + sock.on('order', (updatedOrder: Order) => { + if (updatedOrder.id === orderId) { + console.log('[WS] Estado actualizado:', updatedOrder.status); + onOrderUpdate(updatedOrder); + } + }); + }); + + return () => { + isMounted = false; + socketRef.current?.disconnect(); + }; + }, [orderId, onOrderUpdate]); + + return socketRef.current; +} diff --git a/src/screens/CheckoutScreen.tsx b/src/screens/CheckoutScreen.tsx index fee4794..f77862f 100644 --- a/src/screens/CheckoutScreen.tsx +++ b/src/screens/CheckoutScreen.tsx @@ -10,7 +10,6 @@ import { import { ShoppingBagIcon, TruckIcon, - MapPinIcon, ChevronLeftIcon, } from 'react-native-heroicons/outline'; import { Colors, FontSizes } from '../styles/theme'; @@ -46,6 +45,11 @@ import { setCouponDiscount, setCouponApplied, } from '../redux/slices/checkoutSlice'; +import { + useOrderSocket, + OrderStatus, + Order as OrderSocketType, +} from '../hooks/useOrderSocket'; const CheckoutScreen = () => { const dispatch = useDispatch(); @@ -76,6 +80,10 @@ const CheckoutScreen = () => { const [emailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false); // Track modal visibility const [orderStatus, setOrderStatus] = useState(null); + // Suscribe al socket usando el hook; solo se conecta cuando orderNumber no es null + useOrderSocket(orderNumber, (updatedOrder: OrderSocketType) => { + setOrderStatus(updatedOrder.status); + }); useEffect(() => { const fetchUserName = async () => { @@ -259,10 +267,7 @@ const CheckoutScreen = () => { const orderDetail = await OrderService.getById(orderResponse.id); setOrderStatus(orderDetail.status); // <-- status real del backend } catch (err) { - console.error( - 'No se pud git checkout -b fix/sprint-5-checkout-fixeso obtener el status de la orden:', - err, - ); + console.error('No se pudo obtener el status de la orden:', err); setOrderStatus(orderResponse.status ?? null); } @@ -299,77 +304,21 @@ const CheckoutScreen = () => { } }; - 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 = (orderStatus: string | null) => { - if (option === 'pickup' && orderStatus === '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]'} - -