order
This commit is contained in:
530
app/order-success.tsx
Normal file
530
app/order-success.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user