Files
project_it207_client/app/(tabs)/orders.tsx
2025-11-18 16:18:43 +07:00

375 lines
8.9 KiB
TypeScript

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}>
{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 đơ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',
},
});