order
This commit is contained in:
375
app/(tabs)/orders.tsx
Normal file
375
app/(tabs)/orders.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import orderApi, { Order } from '../../services/orderApi';
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
PENDING: '#FFA726',
|
||||
CONFIRMED: '#42A5F5',
|
||||
SHIPPING: '#AB47BC',
|
||||
DELIVERED: '#66BB6A',
|
||||
CANCELLED: '#EF5350',
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
PENDING: 'Chờ xác nhận',
|
||||
CONFIRMED: 'Đã xác nhận',
|
||||
SHIPPING: 'Đang giao',
|
||||
DELIVERED: 'Đã giao',
|
||||
CANCELLED: 'Đã hủy',
|
||||
};
|
||||
|
||||
const FILTER_TABS = [
|
||||
{ key: 'ALL', label: 'Tất cả' },
|
||||
{ key: 'PENDING', label: 'Chờ xác nhận' },
|
||||
{ key: 'SHIPPING', label: 'Đang giao' },
|
||||
{ key: 'DELIVERED', label: 'Đã giao' },
|
||||
{ key: 'CANCELLED', label: 'Đã hủy' },
|
||||
];
|
||||
|
||||
export default function OrdersListScreen() {
|
||||
const router = useRouter();
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('ALL');
|
||||
|
||||
useEffect(() => {
|
||||
loadOrders();
|
||||
}, [activeTab]);
|
||||
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
const response = activeTab === 'ALL'
|
||||
? await orderApi.getAllOrders()
|
||||
: await orderApi.getOrdersByStatus(activeTab);
|
||||
|
||||
if (response.success && response.data) {
|
||||
setOrders(response.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Load orders error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
loadOrders();
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('vi-VN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatPrice = (price: number) => {
|
||||
return new Intl.NumberFormat('vi-VN', {
|
||||
style: 'currency',
|
||||
currency: 'VND',
|
||||
}).format(price);
|
||||
};
|
||||
|
||||
const renderOrderItem = ({ item }: { item: Order }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.orderCard}
|
||||
onPress={() => router.push(`/order-detail?id=${item.orderId}`)}
|
||||
>
|
||||
<View style={styles.orderHeader}>
|
||||
<Text style={styles.orderId}>Đơn hàng #{item.orderId}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
{ backgroundColor: STATUS_COLORS[item.orderStatus] },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.statusText}>
|
||||
{STATUS_LABELS[item.orderStatus]}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.orderDate}>{formatDate(item.createdAt)}</Text>
|
||||
|
||||
<View style={styles.orderItems}>
|
||||
{item.orderItems.slice(0, 2).map((orderItem) => (
|
||||
<Text key={orderItem.orderItemId} style={styles.productName}>
|
||||
• {orderItem.productName} x{orderItem.quantity}
|
||||
</Text>
|
||||
))}
|
||||
{item.orderItems.length > 2 && (
|
||||
<Text style={styles.moreItems}>
|
||||
và {item.orderItems.length - 2} sản phẩm khác
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.orderFooter}>
|
||||
<Text style={styles.totalLabel}>Tổng tiền:</Text>
|
||||
<Text style={styles.totalPrice}>
|
||||
{formatPrice(item.totalPrice)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.viewDetailButton}>
|
||||
<Text style={styles.viewDetailText}>Xem chi tiết</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color="#ff6b6b" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderEmptyList = () => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="receipt-outline" size={80} color="#ccc" />
|
||||
<Text style={styles.emptyText}>Chưa có đơn hàng nào</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.shopButton}
|
||||
onPress={() => router.push('/')}
|
||||
>
|
||||
<Text style={styles.shopButtonText}>Mua sắm ngay</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#ff6b6b" />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={24} color="#333" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Đơn hàng của tôi</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<View style={styles.tabContainer}>
|
||||
<FlatList
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={FILTER_TABS}
|
||||
keyExtractor={(item) => item.key}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
activeTab === item.key && styles.activeTab,
|
||||
]}
|
||||
onPress={() => setActiveTab(item.key)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabText,
|
||||
activeTab === item.key && styles.activeTabText,
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Orders List */}
|
||||
<FlatList
|
||||
data={orders}
|
||||
renderItem={renderOrderItem}
|
||||
keyExtractor={(item) => item.orderId.toString()}
|
||||
contentContainerStyle={orders.length > 0 ? styles.listContainer : styles.emptyListContainer}
|
||||
ListEmptyComponent={renderEmptyList}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={['#ff6b6b']}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e0e0e0',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#333',
|
||||
},
|
||||
tabContainer: {
|
||||
backgroundColor: '#fff',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e0e0e0',
|
||||
},
|
||||
tab: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginHorizontal: 4,
|
||||
borderRadius: 20,
|
||||
},
|
||||
activeTab: {
|
||||
backgroundColor: '#ff6b6b',
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
},
|
||||
activeTabText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700',
|
||||
},
|
||||
listContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
emptyListContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
orderCard: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
orderHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
orderId: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#333',
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 12,
|
||||
},
|
||||
statusText: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
orderDate: {
|
||||
fontSize: 13,
|
||||
color: '#999',
|
||||
marginBottom: 12,
|
||||
},
|
||||
orderItems: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
productName: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
marginBottom: 4,
|
||||
},
|
||||
moreItems: {
|
||||
fontSize: 13,
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
orderFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#f0f0f0',
|
||||
},
|
||||
totalLabel: {
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
},
|
||||
totalPrice: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#ff6b6b',
|
||||
},
|
||||
viewDetailButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 12,
|
||||
},
|
||||
viewDetailText: {
|
||||
color: '#ff6b6b',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
marginRight: 4,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
color: '#999',
|
||||
marginTop: 16,
|
||||
marginBottom: 24,
|
||||
},
|
||||
shopButton: {
|
||||
backgroundColor: '#ff6b6b',
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
},
|
||||
shopButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user