freshyo/apps/user-ui/app/(drawer)/(tabs)/me/my-orders/index.tsx
2026-01-24 00:13:15 +05:30

669 lines
No EOL
25 KiB
TypeScript
Executable file

import React, { useState, useCallback, useEffect } from 'react';
import { View, FlatList, Alert, ActivityIndicator } from 'react-native';
import { Image } from 'expo-image';
import { MaterialIcons, Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { tw, useManualRefresh, MyText, MyFlatList, useMarkDataFetchers, REFUND_STATUS, MyTouchableOpacity, theme } from 'common-ui';
import { trpc } from '@/src/trpc-client';
// import RazorpayCheckout from 'react-native-razorpay';
import OrderMenu from '@/components/OrderMenu';
import dayjs from 'dayjs';
// Type definitions
interface OrderItem {
productName: string;
quantity: number;
price: number;
amount: number;
image: string | null;
}
interface Order {
id: number;
orderId: string;
orderDate: string;
deliveryStatus: string;
deliveryDate?: string;
orderStatus: string;
cancelReason: string | null;
totalAmount: number;
deliveryCharge: number;
paymentMode: string;
paymentStatus: string;
refundStatus: string;
refundAmount: number | null;
userNotes: string | null;
items: OrderItem[];
discountAmount?: number;
isFlashDelivery: boolean;
createdAt: string;
}
interface OrderFooterProps {
isLoadingMore: boolean;
loadMoreError: string | null;
hasNextPage: boolean;
allOrders: Order[];
onRetryLoadMore: () => void;
}
const OrderFooter: React.FC<OrderFooterProps> = ({
isLoadingMore,
loadMoreError,
hasNextPage,
allOrders,
onRetryLoadMore
}) => {
if (isLoadingMore) {
return (
<View style={tw`py-6 items-center`}>
<ActivityIndicator size="small" color="#3B82F6" />
<MyText style={tw`text-gray-500 mt-2 text-sm`}>Loading more orders...</MyText>
</View>
);
}
if (loadMoreError) {
return (
<View style={tw`py-6 items-center`}>
<MyText style={tw`text-red-500 text-sm mb-2`}>{loadMoreError}</MyText>
<MyTouchableOpacity
onPress={onRetryLoadMore}
style={tw`bg-blue-500 px-4 py-2 rounded-lg`}
>
<MyText style={tw`text-white font-medium text-sm`}>Retry</MyText>
</MyTouchableOpacity>
</View>
);
}
if (!hasNextPage && allOrders.length > 0) {
return (
<View style={tw`py-6 items-center`}>
<MyText style={tw`text-gray-400 text-xs`}>End of list</MyText>
</View>
);
}
return null;
};
interface OrderItemProps {
item: Order;
index: number;
getStatusColor: (status: string) => any;
getRefundStatusColor: (status: string) => any;
onPress: (orderId: number) => void;
onRetryPayment: (orderId: number) => void;
onViewMoreItems: (orderId: number) => void;
isPaymentPending: boolean;
}
const OrderItem: React.FC<OrderItemProps> = ({
item,
index,
getStatusColor,
getRefundStatusColor,
onPress,
onRetryPayment,
onViewMoreItems,
isPaymentPending
}) => {
const mainStatus = getStatusColor(item.orderStatus);
const deliveryStatus = getStatusColor(item.deliveryStatus);
const totalAmount = item.totalAmount;
return (
<MyTouchableOpacity
style={tw`bg-white rounded-[28px] mb-6 border border-slate-100 shadow-sm overflow-hidden`}
onPress={() => onPress(item.id)}
activeOpacity={0.95}
>
{/* Top Header: Order ID and Status */}
<View style={tw`flex-row justify-between items-center px-5 py-4 bg-slate-50/50 border-b border-slate-100`}>
<View>
<View style={tw`flex-row items-center`}>
<View style={tw`w-2 h-2 rounded-full bg-brand500 mr-2`} />
<MyText style={tw`text-[10px] font-bold text-slate-400 uppercase tracking-tighter`}>Order Reference</MyText>
</View>
<MyText style={tw`text-base font-extrabold text-slate-900 mt-0.5`}>#{item.orderId}</MyText>
</View>
<View style={tw`flex-row items-center gap-2`}>
{item.orderStatus.toLowerCase() === 'cancelled' && (
<View style={[tw`flex-row items-center px-3 py-1.5 rounded-full border ${mainStatus.bg} ${mainStatus.border}`]}>
<MaterialIcons name={mainStatus.icon as any} size={14} color={mainStatus.color} />
<MyText style={[tw`text-[11px] font-bold ml-1.5 uppercase tracking-wide`, { color: mainStatus.color }]}>
{mainStatus.label}
</MyText>
</View>
)}
{item.isFlashDelivery && (
<View style={tw`px-2 py-1 bg-amber-100 rounded-full border border-amber-200 flex-row items-center`}>
<MyText style={tw`text-[10px] font-black text-amber-700 uppercase mr-1`}></MyText>
<MyText style={tw`text-[10px] font-black text-amber-700 uppercase`}>FLASH</MyText>
</View>
)}
</View>
</View>
<View style={tw`p-5`}>
{/* Order Date & Quick Stats */}
<View style={tw`flex-row justify-between items-start mb-5`}>
<View style={tw`flex-col gap-3 flex-1`}>
{(item.deliveryDate || item.isFlashDelivery) && (
<View style={tw`flex-row items-center`}>
<View style={[tw`w-9 h-9 rounded-xl items-center justify-center mr-3`, item.isFlashDelivery ? tw`bg-amber-50` : tw`bg-brand50`]}>
{item.isFlashDelivery ? (
<MaterialIcons name="bolt" size={18} color="#D97706" />
) : (
<Ionicons name="time" size={18} color={theme.colors.brand600} />
)}
</View>
<View>
<MyText style={[tw`text-[10px] font-bold uppercase`, item.isFlashDelivery ? tw`text-amber-700` : tw`text-brand700`]}>
{item.isFlashDelivery ? "Flash Delivery" : "Delivery Time"}
</MyText>
<MyText style={[tw`text-sm font-extrabold`, item.isFlashDelivery ? tw`text-amber-900` : tw`text-brand900`]}>
{item.isFlashDelivery
? dayjs(item.createdAt || item.orderDate).add(30, 'minutes').format("DD MMM, hh:mm A")
: dayjs(item.deliveryDate).format("DD MMM, hh:mm A")
}
</MyText>
{item.isFlashDelivery && (
<View style={tw`flex-row items-center mt-1`}>
<MyText style={tw`text-[9px] font-bold text-amber-600 uppercase`}> 30-Min Delivery</MyText>
</View>
)}
</View>
</View>
)}
<View style={tw`flex-row items-center`}>
<View style={tw`w-9 h-9 rounded-xl bg-slate-100 items-center justify-center mr-3`}>
<MaterialIcons name="calendar-today" size={18} color="#64748B" />
</View>
<View>
<MyText style={tw`text-[10px] font-bold text-slate-400 uppercase`}>Placed On</MyText>
<MyText style={tw`text-sm font-semibold text-slate-800`}>
{dayjs(item.orderDate).format("DD MMM YYYY, hh:mm A")}
</MyText>
</View>
</View>
</View>
<OrderMenu
orderId={item.id.toString()}
postActionHandler={() => {
// refetch will be handled by parent
}}
/>
</View>
{/* Items Section */}
<View style={tw`bg-slate-50/50 rounded-2xl p-3 mb-5 border border-slate-100`}>
<View style={tw`flex-row justify-between items-center mb-3 px-1`}>
<MyText style={tw`text-[11px] font-bold text-slate-500 uppercase tracking-wider`}>Items Summary</MyText>
<MyText style={tw`text-[11px] font-bold text-brand600`}>{item.items.length} {item.items.length === 1 ? 'Item' : 'Items'}</MyText>
</View>
{item.items.slice(0, 2).map((product, index) => (
<View key={index} style={tw`flex-row items-center mb-3 last:mb-0`}>
<View style={tw`relative`}>
<Image
source={{ uri: product.image || undefined }}
style={tw`w-12 h-12 rounded-xl bg-white border border-slate-200`}
defaultSource={require('@/assets/logo.png')}
/>
<View style={tw`absolute -top-1.5 -right-1.5 bg-brand500 w-5 h-5 rounded-full items-center justify-center border-2 border-white`}>
<MyText style={tw`text-[10px] font-bold text-white`}>{product.quantity}</MyText>
</View>
</View>
<View style={tw`flex-1 ml-4`}>
<MyText style={tw`text-sm font-bold text-slate-800`} numberOfLines={1}>
{product.productName}
</MyText>
<MyText style={tw`text-xs text-slate-500 mt-0.5 font-medium`}>
Unit Price: {product.price}
</MyText>
</View>
<MyText style={tw`text-sm font-extrabold text-slate-900`}>
{product.amount}
</MyText>
</View>
))}
{item.items.length > 2 && (
<MyTouchableOpacity
style={tw`mt-2 pt-2 border-t border-slate-100 items-center`}
onPress={() => onViewMoreItems(item.id)}
>
<MyText style={tw`text-xs text-brand600 font-bold`}>
+ {item.items.length - 2} more {item.items.length - 2 === 1 ? 'item' : 'items'} in this order
</MyText>
</MyTouchableOpacity>
)}
</View>
{/* Delivery Status - Single Line */}
<View style={tw`flex-row items-center justify-between mb-5`}>
<View style={tw`flex-row items-center flex-1`}>
<View style={[tw`p-1.5 rounded-lg mr-2`, { backgroundColor: deliveryStatus.color + '15' }]}>
{item.isFlashDelivery ? (
<MaterialIcons name="bolt" size={14} color="#D97706" />
) : (
<MaterialIcons name="local-shipping" size={14} color={deliveryStatus.color} />
)}
</View>
<MyText style={tw`text-[10px] font-bold text-slate-400 uppercase mr-2`}>
{item.isFlashDelivery ? "Flash Delivery:" : "Shipping Status:"}
</MyText>
<MyText style={[tw`text-xs font-bold`, item.isFlashDelivery ? tw`text-amber-700` : { color: deliveryStatus.color }]} numberOfLines={1}>
{item.deliveryStatus}
</MyText>
</View>
{item.isFlashDelivery && (
<View style={tw`px-2 py-0.5 bg-amber-100 rounded-full border border-amber-200`}>
<MyText style={tw`text-[8px] font-black text-amber-700 uppercase`}> FAST</MyText>
</View>
)}
</View>
{/* User Notes or Delivery Time if present */}
{item.userNotes && (
<View style={tw`flex-col gap-3 mb-5`}>
<View style={tw`flex-row items-start bg-amber-50/50 p-3 rounded-2xl border border-amber-100`}>
<MaterialIcons name="sticky-note-2" size={16} color="#D97706" style={tw`mt-0.5`} />
<View style={tw`ml-3 flex-1`}>
<MyText style={tw`text-[10px] font-bold text-amber-700 uppercase`}>Your Instructions</MyText>
<MyText style={tw`text-xs font-medium text-amber-900 mt-0.5`} numberOfLines={2}>{item.userNotes}</MyText>
</View>
</View>
</View>
)}
{/* Cancellation Info */}
{item.cancelReason && (
<View style={tw`bg-rose-50 p-4 rounded-2xl mb-5 border border-rose-100`}>
<View style={tw`flex-row items-center mb-2`}>
<Ionicons name="alert-circle" size={16} color="#E11D48" />
<MyText style={tw`text-xs text-rose-800 font-bold ml-2`}>Cancellation Details</MyText>
</View>
<MyText style={tw`text-xs text-rose-600 font-medium leading-relaxed`}>{item.cancelReason}</MyText>
{item.refundStatus && item.refundStatus !== REFUND_STATUS.NOT_APPLICABLE && (
<View style={tw`mt-3 pt-3 border-t border-rose-100 flex-row justify-between items-center`}>
<MyText style={tw`text-[10px] font-bold text-rose-700 uppercase`}>Refund Status</MyText>
<View style={tw`flex-row items-center px-2 py-1 bg-white rounded-lg border border-rose-200`}>
<MaterialIcons
name={getRefundStatusColor(item.refundStatus).icon as any}
size={12}
color={getRefundStatusColor(item.refundStatus).color}
/>
<MyText style={[tw`text-[10px] font-bold ml-1.5 uppercase`, { color: getRefundStatusColor(item.refundStatus).color }]}>
{item.refundStatus}
</MyText>
</View>
</View>
)}
</View>
)}
{/* Footer: Price and CTA */}
<View style={tw`flex-row justify-between items-center pt-5 border-t border-slate-100`}>
<View>
<MyText style={tw`text-[10px] font-bold text-slate-400 uppercase tracking-wider`}>Amount to Pay</MyText>
<MyText style={tw`text-2xl font-black text-slate-900`}>{totalAmount}</MyText>
{item.discountAmount ? (
<View style={tw`flex-row items-center mt-1 bg-emerald-50 self-start px-2 py-0.5 rounded-full`}>
<MaterialIcons name="local-offer" size={10} color="#059669" />
<MyText style={tw`text-[10px] font-bold text-emerald-700 ml-1`}>Saved {item.discountAmount}</MyText>
</View>
) : null}
{item.deliveryCharge > 0 && (
<View style={tw`flex-row items-center mt-1`}>
<MaterialIcons name="local-shipping" size={12} color="#6B7280" style={tw`mr-1`} />
<MyText style={tw`text-[10px] font-medium text-slate-600`}>Delivery: {item.deliveryCharge}</MyText>
</View>
)}
</View>
{(item.paymentMode === 'Online' && (item.paymentStatus === 'pending' || item.paymentStatus === 'failed')) ? (
<MyTouchableOpacity
onPress={(e) => {
e.stopPropagation();
onRetryPayment(item.id);
}}
disabled={isPaymentPending}
style={tw`bg-rose-600 px-6 py-3 rounded-2xl shadow-lg shadow-rose-200 flex-row items-center`}
>
{isPaymentPending ? (
<ActivityIndicator size="small" color="white" />
) : (
<>
<MaterialIcons name="payment" size={18} color="white" />
<MyText style={tw`text-white font-bold text-sm ml-2`}>Retry Payment</MyText>
</>
)}
</MyTouchableOpacity>
) : (
<View style={tw`bg-brand600 px-5 py-3 rounded-2xl shadow-lg shadow-brand100 flex-row items-center`}>
<MyText style={tw`text-white font-bold text-sm mr-1`}>View Details</MyText>
<MaterialIcons name="arrow-forward" size={16} color="white" />
</View>
)}
</View>
</View>
</MyTouchableOpacity>
);
};
export default function MyOrders() {
const router = useRouter();
// Infinite scroll state
const [allOrders, setAllOrders] = useState<Order[]>([]);
const [currentPage, setCurrentPage] = useState<number>(1);
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
const [hasNextPage, setHasNextPage] = useState<boolean>(true);
const [loadMoreError, setLoadMoreError] = useState<string | null>(null);
const pageSize = 10;
const { data: ordersData, isLoading, error, refetch } = trpc.user.order.getOrders.useQuery({
page: currentPage,
pageSize: pageSize,
});
const createRazorpayOrderMutation = trpc.user.payment.createRazorpayOrder.useMutation({
onSuccess: (paymentData) => {
const order = allOrders.find(o => o.id === retryOrderId);
if (order) {
const totalAmount = order.items.reduce((sum, p) => sum + p.amount, 0);
// initiateRazorpayPayment(paymentData.razorpayOrderId, paymentData.key, totalAmount);
}
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to create payment order');
},
});
const verifyPaymentMutation = trpc.user.payment.verifyPayment.useMutation({
onSuccess: () => {
refetch();
Alert.alert('Success', 'Payment completed successfully');
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Payment verification failed');
refetch();
},
});
// Handle data accumulation for infinite scroll
useEffect(() => {
if (ordersData?.data) {
if (currentPage === 1) {
// First page - replace all orders
setAllOrders(ordersData.data);
} else {
// Subsequent pages - append to existing orders
setAllOrders(prev => [...prev, ...ordersData.data]);
}
// Check if there are more pages
const totalPages = ordersData.pagination?.totalPages || 1;
setHasNextPage(currentPage < totalPages);
setIsLoadingMore(false);
setLoadMoreError(null); // Clear any previous errors
}
}, [ordersData, currentPage]);
// Handle errors during infinite scroll loading
useEffect(() => {
if (error && currentPage > 1) {
// If there's an error loading more pages, show error state
setIsLoadingMore(false);
setLoadMoreError('Failed to load more orders. Please try again.');
}
}, [error, currentPage]);
// Reset to first page on manual refresh
useManualRefresh(() => {
setCurrentPage(1);
setAllOrders([]);
setHasNextPage(true);
setIsLoadingMore(false);
setLoadMoreError(null);
refetch();
});
useMarkDataFetchers(() => {
setCurrentPage(1);
setAllOrders([]);
setHasNextPage(true);
setIsLoadingMore(false);
setLoadMoreError(null);
refetch();
});
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [dialogItems, setDialogItems] = useState<OrderItem[]>([]);
const [retryOrderId, setRetryOrderId] = useState<number>(0);
const openDialog = useCallback((items: OrderItem[]) => {
setDialogItems(items);
setDialogOpen(true);
}, []);
// Infinite scroll functions
const loadMoreOrders = useCallback(() => {
if (!isLoadingMore && hasNextPage && !isLoading) {
setIsLoadingMore(true);
setCurrentPage(prev => prev + 1);
}
}, [isLoadingMore, hasNextPage, isLoading]);
const getStatusColor = (status: string) => {
const s = status.toLowerCase();
switch (s) {
case 'delivered':
case 'success':
case 'completed':
return {
bg: 'bg-emerald-50',
text: 'text-emerald-700',
icon: 'check-circle',
color: '#059669',
border: 'border-emerald-100',
label: 'Delivered'
};
case 'cancelled':
case 'failed':
return {
bg: 'bg-rose-50',
text: 'text-rose-700',
icon: 'cancel',
color: '#E11D48',
border: 'border-rose-100',
label: 'Cancelled'
};
case 'pending':
case 'payment_pending':
return {
bg: 'bg-amber-50',
text: 'text-amber-700',
icon: 'schedule',
color: '#D97706',
border: 'border-amber-100',
label: 'Pending'
};
case 'packaged':
return {
bg: 'bg-brand50',
text: 'text-brand700',
icon: 'inventory',
color: '#1570EF',
border: 'border-brand100',
label: 'Packaged'
};
case 'processing':
case 'confirmed':
case 'shipped':
return {
bg: 'bg-brand50',
text: 'text-brand700',
icon: 'local-shipping',
color: '#1570EF',
border: 'border-brand100',
label: status.charAt(0).toUpperCase() + status.slice(1)
};
default:
return {
bg: 'bg-slate-50',
text: 'text-slate-700',
icon: 'info',
color: '#64748B',
border: 'border-slate-100',
label: status
};
}
};
const getRefundStatusColor = (status: string) => {
switch (status) {
case REFUND_STATUS.SUCCESS:
return { bg: 'bg-emerald-50', text: 'text-emerald-700', icon: 'check-circle', color: '#059669', border: 'border-emerald-100' };
case REFUND_STATUS.PROCESSING:
return { bg: 'bg-brand50', text: 'text-brand700', icon: 'refresh', color: '#1570EF', border: 'border-brand100' };
case REFUND_STATUS.PENDING:
return { bg: 'bg-amber-50', text: 'text-amber-700', icon: 'schedule', color: '#D97706', border: 'border-amber-100' };
case REFUND_STATUS.NOT_APPLICABLE:
return { bg: 'bg-slate-50', text: 'text-slate-700', icon: 'info', color: '#64748B', border: 'border-slate-100' };
default:
return { bg: 'bg-slate-50', text: 'text-slate-700', icon: 'info', color: '#64748B', border: 'border-slate-100' };
}
};
const handleRetryPayment = (orderId: number) => {
setRetryOrderId(orderId);
createRazorpayOrderMutation.mutate({ orderId: orderId.toString() });
};
// const initiateRazorpayPayment = (razorpayOrderId: string, key: string, amount: number) => {
// const options = {
// key,
// amount: amount * 100, // in paisa
// currency: 'INR',
// order_id: razorpayOrderId,
// name: 'Meat Farmer',
// description: 'Order Payment Retry',
// prefill: {
// // Add user details if available
// },
// };
// RazorpayCheckout.open(options)
// .then((data: any) => {
// // Payment success
// verifyPaymentMutation.mutate({
// razorpay_payment_id: data.razorpay_payment_id,
// razorpay_order_id: data.razorpay_order_id,
// razorpay_signature: data.razorpay_signature,
// });
// })
// .catch((error: any) => {
// Alert.alert('Payment Failed', 'Payment failed. Please try again.');
// refetch();
// });
// };
if (isLoading && currentPage === 1) {
return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<ActivityIndicator size="large" color="#3B82F6" />
<MyText style={tw`text-gray-500 mt-4 font-medium`}>Loading your orders...</MyText>
</View>
);
}
if (error) {
return (
<View style={tw`flex-1 justify-center items-center bg-gray-50 p-6`}>
<View style={tw`bg-white p-6 rounded-full shadow-sm mb-4`}>
<MaterialIcons name="error-outline" size={48} color="#EF4444" />
</View>
<MyText style={tw`text-gray-900 text-xl font-bold mt-2`}>Unable to load orders</MyText>
<MyText style={tw`text-gray-500 text-center mt-2 mb-6`}>Please check your connection and try again</MyText>
<MyTouchableOpacity
onPress={() => refetch()}
style={tw`bg-blue-600 px-8 py-3 rounded-full shadow-md`}
>
<MyText style={tw`text-white font-bold`}>Retry</MyText>
</MyTouchableOpacity>
</View>
);
}
return (
<View style={tw`flex-1 bg-gray-50`}>
<MyFlatList
style={tw`flex-1`}
contentContainerStyle={tw`px-4 py-6`}
data={allOrders}
renderItem={({ item, index }) => (
<OrderItem
item={item}
index={index}
getStatusColor={getStatusColor}
getRefundStatusColor={getRefundStatusColor}
onPress={(orderId) => router.push(`/(drawer)/(tabs)/me/my-orders/${orderId}`)}
onRetryPayment={handleRetryPayment}
onViewMoreItems={(orderId) => router.push(`/(drawer)/(tabs)/me/my-orders/${orderId}`)}
isPaymentPending={createRazorpayOrderMutation.isPending}
/>
)}
keyExtractor={(item) => item.orderId}
showsVerticalScrollIndicator={false}
onEndReached={loadMoreOrders}
onEndReachedThreshold={0.5}
ListFooterComponent={
<OrderFooter
isLoadingMore={isLoadingMore}
loadMoreError={loadMoreError}
hasNextPage={hasNextPage}
allOrders={allOrders}
onRetryLoadMore={() => {
setLoadMoreError(null);
loadMoreOrders();
}}
/>
}
ListEmptyComponent={
<View style={tw`flex-1 justify-center items-center py-20`}>
<View style={tw`bg-white p-6 rounded-full shadow-sm mb-4`}>
<MaterialIcons name="shopping-bag" size={64} color="#E5E7EB" />
</View>
<MyText style={tw`text-gray-900 text-lg font-bold mt-2`}>No orders yet</MyText>
<MyText style={tw`text-gray-500 text-center mt-2`}>Your order history will appear here</MyText>
<MyTouchableOpacity
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
onPress={() => router.push('/(drawer)/(tabs)/home')}
>
<MyText style={tw`text-white font-bold`}>Start Shopping</MyText>
</MyTouchableOpacity>
</View>
}
/>
</View>
);
}