This commit is contained in:
2025-11-18 16:18:43 +07:00
parent f150a84cf0
commit 08b352f686
10 changed files with 2140 additions and 27 deletions

530
app/order-success.tsx Normal file
View File

@@ -0,0 +1,530 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
ActivityIndicator,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import orderApi, { Order, OrderProgress } 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 hàng',
DELIVERED: 'Đã giao hàng',
CANCELLED: 'Đã hủy',
};
export default function OrderDetailScreen() {
const router = useRouter();
const params = useLocalSearchParams();
const orderId = params.orderId as string;
const [order, setOrder] = useState<Order | null>(null);
const [timeline, setTimeline] = useState<OrderProgress['timeline']>([]);
const [loading, setLoading] = useState(true);
const [cancelling, setCancelling] = useState(false);
useEffect(() => {
if (orderId) {
console.log("Order Success Params:", params);
console.log("Order ID:", orderId);
loadOrderDetail();
loadOrderProgress();
}
}, [orderId]);
const loadOrderDetail = async () => {
try {
const response = await orderApi.getOrderDetail(Number(orderId));
if (response.success && response.data) {
setOrder(response.data);
}
} catch (error: any) {
console.error('Load order detail error:', error);
Alert.alert('Lỗi', 'Không thể tải thông tin đơn hàng');
} finally {
setLoading(false);
}
};
const loadOrderProgress = async () => {
try {
const response = await orderApi.getOrderProgress(Number(orderId));
if (response.success && response.data) {
setTimeline(response.data.timeline);
}
} catch (error: any) {
console.error('Load order progress error:', error);
}
};
const handleCancelOrder = () => {
Alert.alert(
'Hủy đơn hàng',
'Bạn có chắc chắn muốn hủy đơn hàng này?',
[
{ text: 'Không', style: 'cancel' },
{ text: 'Có', onPress: cancelOrder, style: 'destructive' },
]
);
};
const cancelOrder = async () => {
setCancelling(true);
try {
const response = await orderApi.cancelOrder(Number(orderId));
if (response.success) {
Alert.alert('Thành công', 'Đơn hàng đã được hủy');
loadOrderDetail();
loadOrderProgress();
} else {
Alert.alert('Lỗi', response.message || 'Không thể hủy đơn hàng');
}
} catch (error: any) {
Alert.alert('Lỗi', error.message || 'Không thể hủy đơn hàng');
} finally {
setCancelling(false);
}
};
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);
};
if (loading) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#ff6b6b" />
</View>
</SafeAreaView>
);
}
if (!order) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.errorContainer}>
<Ionicons name="alert-circle-outline" size={80} color="#ccc" />
<Text style={styles.errorText}>Không tìm thấy đơn hàng</Text>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<Text style={styles.backButtonText}>Quay lại</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.push("/orders")}>
<Ionicons name="arrow-back" size={24} color="#333" />
</TouchableOpacity>
<Text style={styles.headerTitle}>Chi tiết đơn hàng</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView style={styles.scrollView}>
{/* Order Status */}
<View style={styles.statusSection}>
<View
style={[
styles.statusBadge,
{ backgroundColor: STATUS_COLORS[order.orderStatus] },
]}
>
<Text style={styles.statusText}>
{STATUS_LABELS[order.orderStatus]}
</Text>
</View>
<Text style={styles.orderId}>Đơn hàng #{order.orderId}</Text>
<Text style={styles.orderDate}>
Đt hàng: {formatDate(order.createdAt)}
</Text>
</View>
{/* Timeline */}
{order.orderStatus !== 'CANCELLED' && timeline.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Tiến đ đơn hàng</Text>
{timeline.map((step, index) => (
<View key={index} style={styles.timelineItem}>
<View style={styles.timelineLeft}>
<View
style={[
styles.timelineDot,
step.completed && styles.timelineDotActive,
]}
>
{step.completed && (
<Ionicons name="checkmark" size={16} color="#fff" />
)}
</View>
{index < timeline.length - 1 && (
<View
style={[
styles.timelineLine,
step.completed && styles.timelineLineActive,
]}
/>
)}
</View>
<View style={styles.timelineRight}>
<Text
style={[
styles.timelineStatus,
step.completed && styles.timelineStatusActive,
]}
>
{STATUS_LABELS[step.status]}
</Text>
{step.timestamp && (
<Text style={styles.timelineDate}>
{formatDate(step.timestamp)}
</Text>
)}
</View>
</View>
))}
</View>
)}
{/* Shipping Address */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="location" size={18} color="#ff6b6b" />
<Text style={styles.sectionTitle}>Đa chỉ giao hàng</Text>
</View>
<Text style={styles.addressText}>{order.shippingAddress}</Text>
</View>
{/* Order Items */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="cart" size={18} color="#ff6b6b" />
<Text style={styles.sectionTitle}>Sản phẩm</Text>
</View>
{order.orderItems.map((item) => (
<View key={item.orderItemId} style={styles.productItem}>
<View style={styles.productInfo}>
<Text style={styles.productName}>{item.productName}</Text>
<Text style={styles.productQuantity}>x{item.quantity}</Text>
</View>
<View style={styles.productPrices}>
<Text style={styles.productPrice}>
{formatPrice(item.price)}
</Text>
<Text style={styles.productSubtotal}>
{formatPrice(item.subtotal)}
</Text>
</View>
</View>
))}
</View>
{/* Payment Summary */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Thanh toán</Text>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Tạm tính</Text>
<Text style={styles.summaryValue}>
{formatPrice(order.totalPrice)}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Phí vận chuyển</Text>
<Text style={styles.summaryValue}>Miễn phí</Text>
</View>
<View style={[styles.summaryRow, styles.totalRow]}>
<Text style={styles.totalLabel}>Tổng cộng</Text>
<Text style={styles.totalValue}>
{formatPrice(order.totalPrice)}
</Text>
</View>
</View>
<View style={{ height: 100 }} />
</ScrollView>
{/* Cancel Button */}
{(order.orderStatus === 'PENDING' ||
order.orderStatus === 'CONFIRMED') && (
<View style={styles.footer}>
<TouchableOpacity
style={[styles.cancelButton, cancelling && styles.buttonDisabled]}
onPress={handleCancelOrder}
disabled={cancelling}
>
{cancelling ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.cancelButtonText}>Hủy đơn hàng</Text>
)}
</TouchableOpacity>
</View>
)}
</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',
},
scrollView: {
flex: 1,
},
statusSection: {
backgroundColor: '#fff',
padding: 20,
alignItems: 'center',
marginTop: 12,
},
statusBadge: {
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 20,
marginBottom: 12,
},
statusText: {
color: '#fff',
fontSize: 14,
fontWeight: '700',
},
orderId: {
fontSize: 18,
fontWeight: '700',
marginBottom: 4,
color: '#333',
},
orderDate: {
fontSize: 14,
color: '#666',
},
section: {
backgroundColor: '#fff',
marginTop: 12,
padding: 16,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
marginLeft: 8,
color: '#333',
},
timelineItem: {
flexDirection: 'row',
marginBottom: 8,
},
timelineLeft: {
alignItems: 'center',
marginRight: 12,
},
timelineDot: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#e0e0e0',
alignItems: 'center',
justifyContent: 'center',
},
timelineDotActive: {
backgroundColor: '#4CAF50',
},
timelineLine: {
width: 2,
flex: 1,
backgroundColor: '#e0e0e0',
marginTop: 4,
},
timelineLineActive: {
backgroundColor: '#4CAF50',
},
timelineRight: {
flex: 1,
paddingBottom: 16,
},
timelineStatus: {
fontSize: 14,
color: '#999',
marginBottom: 4,
},
timelineStatusActive: {
color: '#333',
fontWeight: '700',
},
timelineDate: {
fontSize: 12,
color: '#999',
},
addressText: {
fontSize: 14,
color: '#333',
lineHeight: 20,
},
productItem: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
productInfo: {
flex: 1,
marginRight: 12,
},
productName: {
fontSize: 15,
marginBottom: 4,
color: '#333',
},
productQuantity: {
fontSize: 13,
color: '#666',
},
productPrices: {
alignItems: 'flex-end',
},
productPrice: {
fontSize: 13,
color: '#666',
marginBottom: 4,
},
productSubtotal: {
fontSize: 15,
fontWeight: '700',
color: '#ff6b6b',
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 10,
},
summaryLabel: {
fontSize: 15,
color: '#666',
},
summaryValue: {
fontSize: 15,
color: '#333',
},
totalRow: {
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
marginTop: 8,
paddingTop: 16,
},
totalLabel: {
fontSize: 17,
fontWeight: '700',
color: '#333',
},
totalValue: {
fontSize: 20,
fontWeight: '700',
color: '#ff6b6b',
},
footer: {
backgroundColor: '#fff',
padding: 16,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
cancelButton: {
backgroundColor: '#EF5350',
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
buttonDisabled: {
opacity: 0.6,
},
cancelButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 32,
},
errorText: {
fontSize: 16,
color: '#999',
marginTop: 16,
marginBottom: 24,
},
backButton: {
backgroundColor: '#ff6b6b',
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
},
backButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
});