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

434
app/(tabs)/checkout.tsx Normal file
View File

@@ -0,0 +1,434 @@
import React, { useState } from 'react';
import { getUser } from '@/services/auth';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useCart } from '../../hooks/useCart';
import orderApi from '../../services/orderApi';
interface PaymentMethod {
id: string;
name: string;
icon: string;
}
const PAYMENT_METHODS: PaymentMethod[] = [
{ id: 'COD', name: 'Thanh toán khi nhận hàng', icon: 'cash-outline' },
{ id: 'BANK', name: 'Chuyển khoản ngân hàng', icon: 'card-outline' },
{ id: 'MOMO', name: 'Ví MoMo', icon: 'wallet-outline' },
{ id: 'VNPAY', name: 'VNPay', icon: 'card-outline' },
];
export default function CheckoutScreen() {
const router = useRouter();
const { cart, refreshCart, clearCart } = useCart();
const [loading, setLoading] = useState(false);
const [shippingAddress, setShippingAddress] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [note, setNote] = useState('');
const [selectedPayment, setSelectedPayment] = useState('COD');
const formatPrice = (price: number) => {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
}).format(price);
};
const validateForm = () => {
if (!shippingAddress.trim()) {
Alert.alert('Lỗi', 'Vui lòng nhập địa chỉ giao hàng');
return false;
}
if (!phoneNumber.trim()) {
Alert.alert('Lỗi', 'Vui lòng nhập số điện thoại');
return false;
}
if (!/^[0-9]{10}$/.test(phoneNumber)) {
Alert.alert('Lỗi', 'Số điện thoại không hợp lệ (10 chữ số)');
return false;
}
return true;
};
const handleCreateOrder = async () => {
if (!validateForm() || !cart) return;
setLoading(true);
try {
// LẤY userId từ AsyncStorage
const user = await getUser();
if (!user) {
Alert.alert("Lỗi", "Bạn chưa đăng nhập!");
return;
}
const userId = user.userId;
const fullAddress = `${shippingAddress}${note ? ` - Ghi chú: ${note}` : ''} - SĐT: ${phoneNumber}`;
const orderData = {
shippingAddress: fullAddress,
items: cart.items.map(item => ({
productId: item.product.productId,
quantity: item.quantity,
})),
};
// Gọi API đúng với 2 tham số
const response = await orderApi.createOrder(userId, orderData);
if (response.success && response.data) {
await clearCart(false);
await refreshCart();
router.replace({
pathname: '/order-success',
params: { orderId: response.data.orderId.toString() }
});
} else {
Alert.alert("Lỗi", response.message || "Đặt hàng thất bại");
}
} catch (error: any) {
console.error("Create order error:", error);
Alert.alert("Lỗi", error.message || "Không thể kết nối đến server");
} finally {
setLoading(false);
}
};
if (!cart || cart.items.length === 0) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#ff6b6b" />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<Ionicons name="arrow-back" size={24} color="#333" />
</TouchableOpacity>
<Text style={styles.headerTitle}>Thanh toán</Text>
<View style={{ width: 24 }} />
</View>
<ScrollView style={styles.scrollView}>
{/* Shipping Address */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="location" size={20} color="#ff6b6b" />
<Text style={styles.sectionTitle}>Đa chỉ giao hàng</Text>
</View>
<TextInput
style={styles.input}
placeholder="Số nhà, tên đường, phường/xã"
value={shippingAddress}
onChangeText={setShippingAddress}
multiline
numberOfLines={2}
/>
<TextInput
style={styles.input}
placeholder="Số điện thoại (10 chữ số)"
value={phoneNumber}
onChangeText={setPhoneNumber}
keyboardType="phone-pad"
maxLength={10}
/>
<TextInput
style={styles.input}
placeholder="Ghi chú cho người giao hàng (không bắt buộc)"
value={note}
onChangeText={setNote}
multiline
numberOfLines={2}
/>
</View>
{/* Order Items */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="cart" size={20} color="#ff6b6b" />
<Text style={styles.sectionTitle}>
Sản phẩm ({cart.items.length})
</Text>
</View>
{cart.items.map((item) => (
<View key={item.cartItemId} style={styles.cartItem}>
<View style={styles.itemInfo}>
<Text style={styles.itemName} numberOfLines={2}>
{item.product.productName}
</Text>
<Text style={styles.itemPrice}>
{formatPrice(item.product.price)} x {item.quantity}
</Text>
</View>
<Text style={styles.itemTotal}>
{formatPrice(item.product.price * item.quantity)}
</Text>
</View>
))}
</View>
{/* Payment Method */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="card" size={20} color="#ff6b6b" />
<Text style={styles.sectionTitle}>Phương thức thanh toán</Text>
</View>
{PAYMENT_METHODS.map((method) => (
<TouchableOpacity
key={method.id}
style={[
styles.paymentMethod,
selectedPayment === method.id && styles.paymentMethodActive,
]}
onPress={() => setSelectedPayment(method.id)}
>
<View style={styles.paymentMethodInfo}>
<Ionicons name={method.icon as any} size={24} color="#333" />
<Text style={styles.paymentMethodName}>{method.name}</Text>
</View>
{selectedPayment === method.id && (
<Ionicons name="checkmark-circle" size={24} color="#ff6b6b" />
)}
</TouchableOpacity>
))}
</View>
{/* Order Summary */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Chi tiết thanh toán</Text>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Tạm tính</Text>
<Text style={styles.summaryValue}>
{formatPrice(cart.totalAmount)}
</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(cart.totalAmount)}
</Text>
</View>
</View>
<View style={{ height: 100 }} />
</ScrollView>
{/* Order Button */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.orderButton, loading && styles.orderButtonDisabled]}
onPress={handleCreateOrder}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<>
<Text style={styles.orderButtonText}>Đt hàng</Text>
<Text style={styles.orderButtonSubtext}>
{formatPrice(cart.totalAmount)}
</Text>
</>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</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,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
section: {
backgroundColor: '#fff',
marginTop: 12,
padding: 16,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
marginLeft: 8,
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 8,
padding: 12,
marginBottom: 12,
fontSize: 15,
backgroundColor: '#fafafa',
},
cartItem: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
itemInfo: {
flex: 1,
marginRight: 12,
},
itemName: {
fontSize: 15,
marginBottom: 4,
color: '#333',
},
itemPrice: {
fontSize: 13,
color: '#666',
},
itemTotal: {
fontSize: 15,
fontWeight: '700',
color: '#ff6b6b',
},
paymentMethod: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 14,
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 8,
marginBottom: 10,
backgroundColor: '#fafafa',
},
paymentMethodActive: {
borderColor: '#ff6b6b',
backgroundColor: '#fff5f5',
},
paymentMethodInfo: {
flexDirection: 'row',
alignItems: 'center',
},
paymentMethodName: {
marginLeft: 12,
fontSize: 15,
color: '#333',
},
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',
},
orderButton: {
backgroundColor: '#ff6b6b',
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
orderButtonDisabled: {
opacity: 0.6,
},
orderButtonText: {
color: '#fff',
fontSize: 17,
fontWeight: '700',
},
orderButtonSubtext: {
color: '#fff',
fontSize: 14,
marginTop: 4,
},
});