dressed_for_succes_store/frontend old/pages/checkout.tsx
2025-03-11 22:42:30 +07:00

822 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Image from 'next/image';
import { ShoppingBag, CreditCard, Truck, Check, Plus, X } from 'lucide-react';
import Header from '../components/Header';
import Footer from '../components/Footer';
import cartService, { Cart } from '../services/cart';
import { orderService } from '../services/orders';
import authService from '../services/auth';
import { userService, Address, AddressCreate } from '../services/users';
// Типы для формы оформления заказа
interface CheckoutForm {
shipping_address_id: number;
payment_method: string;
notes: string;
}
// Типы для формы нового адреса
interface AddressForm {
address_line1: string;
address_line2?: string;
city: string;
state: string;
postal_code: string;
country: string;
is_default: boolean;
}
export default function CheckoutPage() {
const router = useRouter();
const [cart, setCart] = useState<Cart | null>(null);
const [addresses, setAddresses] = useState<Address[]>([]);
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [orderId, setOrderId] = useState<number | null>(null);
const [showAddressForm, setShowAddressForm] = useState(false);
const [addressFormSubmitting, setAddressFormSubmitting] = useState(false);
const [addressFormError, setAddressFormError] = useState<string | null>(null);
const [formValidated, setFormValidated] = useState(false);
// Состояние формы заказа
const [form, setForm] = useState<CheckoutForm>({
shipping_address_id: 0,
payment_method: 'credit_card',
notes: ''
});
// Состояние формы нового адреса
const [addressForm, setAddressForm] = useState<AddressForm>({
address_line1: '',
address_line2: '',
city: '',
state: '',
postal_code: '',
country: 'Россия',
is_default: false
});
// Проверка валидности формы
useEffect(() => {
// Форма валидна, если выбран адрес доставки и способ оплаты
const isValid = form.shipping_address_id > 0 && !!form.payment_method;
setFormValidated(isValid);
}, [form.shipping_address_id, form.payment_method]);
// Загрузка корзины и адресов при монтировании компонента
useEffect(() => {
const fetchData = async () => {
try {
// Проверяем, авторизован ли пользователь
if (!authService.isAuthenticated()) {
router.push('/login?redirect=/checkout');
return;
}
setLoading(true);
// Загружаем корзину
const cartData = await cartService.getCart();
setCart(cartData);
// Если корзина пуста, перенаправляем на страницу корзины
if (cartData.items.length === 0) {
router.push('/cart');
return;
}
try {
// Загружаем адреса пользователя
const userData = await userService.getCurrentUser();
if (userData.addresses && userData.addresses.length > 0) {
setAddresses(userData.addresses);
// Устанавливаем адрес по умолчанию, если он есть
const defaultAddress = userData.addresses.find(addr => addr.is_default);
if (defaultAddress) {
setForm(prev => ({ ...prev, shipping_address_id: defaultAddress.id }));
} else {
setForm(prev => ({ ...prev, shipping_address_id: userData.addresses[0].id }));
}
} else {
// Если у пользователя нет адресов, показываем форму добавления адреса
setShowAddressForm(true);
}
} catch (addressErr) {
console.error('Ошибка при загрузке адресов:', addressErr);
// Не показываем ошибку пользователю, просто предлагаем добавить адрес
setShowAddressForm(true);
}
setError(null);
} catch (err) {
console.error('Ошибка при загрузке данных:', err);
setError('Не удалось загрузить данные. Пожалуйста, попробуйте позже.');
} finally {
setLoading(false);
}
};
fetchData();
}, [router]);
// Обработчик изменения полей формы заказа
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
// Обработчик изменения полей формы адреса
const handleAddressFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
// Для чекбокса используем checked, для остальных полей - value
if (e.target instanceof HTMLInputElement && e.target.type === 'checkbox') {
const checked = e.target.checked;
setAddressForm(prev => ({
...prev,
[name]: checked
}));
} else {
setAddressForm(prev => ({
...prev,
[name]: value
}));
}
};
// Обработчик отправки формы заказа
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formValidated) {
setError('Пожалуйста, заполните все обязательные поля');
// Если нет адресов, показываем форму добавления адреса
if (addresses.length === 0) {
setShowAddressForm(true);
}
return;
}
// Проверяем, что в корзине есть товары
if (!cart || cart.items.length === 0) {
setError('Ваша корзина пуста. Добавьте товары в корзину, чтобы оформить заказ.');
return;
}
try {
setSubmitting(true);
setError(null);
console.log('Отправка заказа на сервер:', {
shipping_address_id: form.shipping_address_id,
payment_method: form.payment_method,
notes: form.notes
});
// Создаем заказ
const orderResponse = await orderService.createOrder({
shipping_address_id: form.shipping_address_id,
payment_method: form.payment_method,
notes: form.notes
});
console.log('Получен ответ от сервера:', orderResponse);
// Извлекаем ID заказа из ответа
let orderId = null;
// Проверяем разные форматы ответа
const responseObj = orderResponse as any;
if (responseObj && responseObj.id) {
orderId = responseObj.id;
} else if (responseObj && responseObj.order && responseObj.order.id) {
orderId = responseObj.order.id;
}
// Устанавливаем флаг успешного создания заказа
setSuccess(true);
setOrderId(orderId);
// Очищаем корзину после успешного оформления заказа
try {
await cartService.clearCart();
} catch (clearErr) {
console.error('Ошибка при очистке корзины:', clearErr);
// Не показываем эту ошибку пользователю, так как заказ уже создан
}
// Проверяем, что ID заказа существует и является числом
if (orderId && !isNaN(orderId)) {
// Перенаправляем на страницу успешного оформления заказа
router.push(`/order-success?id=${orderId}`);
} else {
console.error('Ошибка: ID заказа отсутствует или некорректен', orderResponse);
// Перенаправляем на страницу заказов без указания ID
router.push('/account/orders');
}
} catch (err) {
console.error('Ошибка при оформлении заказа:', err);
// Выводим подробную информацию об ошибке в консоль
if (err.response) {
console.error('Статус ответа:', err.response.status);
console.error('Данные ответа:', err.response.data);
}
// Проверяем, есть ли в ответе сервера сообщение об ошибке
let errorMessage = 'Не удалось оформить заказ. Пожалуйста, попробуйте позже.';
if (err.response && err.response.data) {
if (typeof err.response.data === 'string') {
errorMessage = err.response.data;
} else if (err.response.data.detail) {
if (Array.isArray(err.response.data.detail)) {
// Если detail - это массив ошибок валидации
const validationErrors = err.response.data.detail.map(error =>
`${error.loc.join('.')}: ${error.msg}`
).join('; ');
errorMessage = `Ошибка валидации: ${validationErrors}`;
} else {
errorMessage = err.response.data.detail;
}
}
}
setError(errorMessage);
// Прокручиваем страницу вверх, чтобы показать сообщение об ошибке
window.scrollTo({ top: 0, behavior: 'smooth' });
} finally {
setSubmitting(false);
}
};
// Обработчик отправки формы нового адреса
const handleAddressSubmit = async (e: React.FormEvent) => {
e.preventDefault(); // Предотвращаем стандартное поведение формы
// Валидация формы
if (!addressForm.address_line1 || !addressForm.city || !addressForm.state || !addressForm.postal_code) {
setAddressFormError('Пожалуйста, заполните все обязательные поля');
return;
}
try {
setAddressFormSubmitting(true);
setAddressFormError(null);
console.log('Отправка адреса на сервер:', addressForm);
// Создаем новый адрес
const newAddress = await userService.addAddress(addressForm);
console.log('Получен ответ от сервера:', newAddress);
// Добавляем новый адрес в список и выбираем его
setAddresses(prev => [...prev, newAddress]);
setForm(prev => ({ ...prev, shipping_address_id: newAddress.id }));
// Скрываем форму добавления адреса
setShowAddressForm(false);
// Сбрасываем форму
setAddressForm({
address_line1: '',
address_line2: '',
city: '',
state: '',
postal_code: '',
country: 'Россия',
is_default: false
});
} catch (err) {
console.error('Ошибка при добавлении адреса:', err);
// Выводим подробную информацию об ошибке в консоль
if (err.response) {
console.error('Ответ сервера:', err.response.status, err.response.data);
}
// Проверяем, есть ли в ответе сервера сообщение об ошибке
let errorMessage = 'Не удалось добавить адрес. Пожалуйста, попробуйте позже.';
if (err.response && err.response.data) {
if (typeof err.response.data === 'string') {
errorMessage = err.response.data;
} else if (err.response.data.detail) {
if (Array.isArray(err.response.data.detail)) {
// Если detail - это массив ошибок валидации
const validationErrors = err.response.data.detail.map(error =>
`${error.loc.join('.')}: ${error.msg}`
).join('; ');
errorMessage = `Ошибка валидации: ${validationErrors}`;
} else {
errorMessage = err.response.data.detail;
}
}
}
setAddressFormError(errorMessage);
} finally {
setAddressFormSubmitting(false);
}
};
// Форматирование цены
const formatPrice = (price: number): string => {
return price.toLocaleString('ru-RU') + ' ₽';
};
// Функция для корректного отображения URL изображения
const getImageUrl = (imageUrl: string | undefined): string => {
if (!imageUrl) return '/placeholder-image.jpg';
// Проверяем, начинается ли URL с http или https
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return imageUrl;
}
// Формируем базовый URL для изображений
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8000/api';
const apiBaseUrl = baseUrl.replace(/\/api$/, ''); // Убираем '/api' в конце, если есть
// Если URL начинается с /, добавляем базовый URL API
if (imageUrl.startsWith('/')) {
return `${apiBaseUrl}${imageUrl}`;
}
// В остальных случаях добавляем базовый URL API и /
return `${apiBaseUrl}/${imageUrl}`;
};
// Форматирование адреса для отображения
const formatAddress = (address: Address): string => {
let formattedAddress = address.address_line1;
if (address.address_line2) {
formattedAddress += `, ${address.address_line2}`;
}
return `${formattedAddress}, ${address.city}, ${address.state}, ${address.postal_code}, ${address.country}`;
};
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-grow pt-24 pb-16">
<div className="container mx-auto px-4">
<h1 className="text-2xl md:text-3xl font-bold mb-8 text-center">Оформление заказа</h1>
{loading ? (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
</div>
) : error && !success ? (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
) : success ? (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-8 rounded mb-4 text-center">
<div className="flex justify-center mb-4">
<div className="bg-green-500 rounded-full p-2">
<Check className="w-8 h-8 text-white" />
</div>
</div>
<h2 className="text-xl font-semibold mb-2">Заказ успешно оформлен!</h2>
<p className="mb-4">Номер вашего заказа: {orderId}</p>
<p>Вы будете перенаправлены на страницу заказа через несколько секунд...</p>
</div>
) : cart ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Форма оформления заказа */}
<div className="lg:col-span-2">
<form id="checkout-form" onSubmit={handleSubmit}>
{/* Адрес доставки */}
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold flex items-center">
<Truck className="w-5 h-5 mr-2" />
Адрес доставки
</h2>
{!showAddressForm && (
<button
type="button"
onClick={() => setShowAddressForm(true)}
className="text-sm flex items-center text-indigo-600 hover:text-indigo-800"
>
<Plus className="w-4 h-4 mr-1" />
Добавить новый адрес
</button>
)}
</div>
{showAddressForm ? (
<div className="border rounded-md p-4 mb-4">
<div className="flex justify-between items-center mb-3">
<h3 className="font-medium">Новый адрес доставки</h3>
<button
type="button"
onClick={() => setShowAddressForm(false)}
className="text-gray-500 hover:text-gray-700"
>
<X className="w-5 h-5" />
</button>
</div>
{addressFormError && (
<div className="bg-red-100 border border-red-400 text-red-700 px-3 py-2 rounded mb-3 text-sm">
{addressFormError}
</div>
)}
<form onSubmit={handleAddressSubmit} className="space-y-3">
<div>
<label htmlFor="address_line1" className="block text-sm font-medium text-gray-700 mb-1">
Адрес (строка 1)*
</label>
<input
type="text"
id="address_line1"
name="address_line1"
value={addressForm.address_line1}
onChange={handleAddressFormChange}
placeholder="Улица, дом"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
</div>
<div>
<label htmlFor="address_line2" className="block text-sm font-medium text-gray-700 mb-1">
Адрес (строка 2)
</label>
<input
type="text"
id="address_line2"
name="address_line2"
value={addressForm.address_line2}
onChange={handleAddressFormChange}
placeholder="Квартира, офис (необязательно)"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label htmlFor="city" className="block text-sm font-medium text-gray-700 mb-1">
Город*
</label>
<input
type="text"
id="city"
name="city"
value={addressForm.city}
onChange={handleAddressFormChange}
placeholder="Город"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
</div>
<div>
<label htmlFor="state" className="block text-sm font-medium text-gray-700 mb-1">
Область/Регион*
</label>
<input
type="text"
id="state"
name="state"
value={addressForm.state}
onChange={handleAddressFormChange}
placeholder="Область или регион"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label htmlFor="postal_code" className="block text-sm font-medium text-gray-700 mb-1">
Почтовый индекс*
</label>
<input
type="text"
id="postal_code"
name="postal_code"
value={addressForm.postal_code}
onChange={handleAddressFormChange}
placeholder="Индекс"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
</div>
<div>
<label htmlFor="country" className="block text-sm font-medium text-gray-700 mb-1">
Страна*
</label>
<input
type="text"
id="country"
name="country"
value={addressForm.country}
onChange={handleAddressFormChange}
placeholder="Страна"
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
required
/>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="is_default"
name="is_default"
checked={addressForm.is_default}
onChange={handleAddressFormChange}
className="h-4 w-4 text-black focus:ring-black border-gray-300 rounded"
/>
<label htmlFor="is_default" className="ml-2 block text-sm text-gray-700">
Использовать как адрес по умолчанию
</label>
</div>
<div className="flex justify-end pt-2">
<button
type="submit"
disabled={addressFormSubmitting}
className="bg-black text-white px-4 py-2 rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{addressFormSubmitting ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white mr-2"></div>
Сохранение...
</div>
) : (
'Сохранить адрес'
)}
</button>
</div>
</form>
</div>
) : null}
{addresses.length > 0 ? (
<div className="space-y-4">
{addresses.map(address => (
<div key={address.id} className="border rounded-md p-4">
<div className="flex items-start">
<input
type="radio"
id={`address-${address.id}`}
name="shipping_address_id"
value={address.id}
checked={form.shipping_address_id === address.id}
onChange={handleChange}
className="mt-1 mr-3"
/>
<label htmlFor={`address-${address.id}`} className="flex-grow cursor-pointer">
<div className="font-medium">Адрес доставки</div>
<div className="text-sm text-gray-600 mt-1">{formatAddress(address)}</div>
{address.is_default && (
<div className="text-sm text-green-600 mt-1">Адрес по умолчанию</div>
)}
</label>
</div>
</div>
))}
</div>
) : !showAddressForm ? (
<div className="text-center py-4">
<p className="text-gray-600 mb-4">У вас еще нет сохраненных адресов</p>
<button
type="button"
onClick={() => setShowAddressForm(true)}
className="text-blue-600 hover:text-blue-800 transition-colors"
>
Добавить новый адрес
</button>
</div>
) : null}
</div>
</div>
{/* Способ оплаты */}
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
<div className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center">
<CreditCard className="w-5 h-5 mr-2" />
Способ оплаты
</h2>
<div className="space-y-4">
<div className="border rounded-md p-4">
<div className="flex items-start">
<input
type="radio"
id="payment-card"
name="payment_method"
value="credit_card"
checked={form.payment_method === 'credit_card'}
onChange={handleChange}
className="mt-1 mr-3"
/>
<label htmlFor="payment-card" className="flex-grow cursor-pointer">
<div className="font-medium">Банковская карта</div>
<div className="text-sm text-gray-600 mt-1">Оплата картой при получении</div>
</label>
</div>
</div>
<div className="border rounded-md p-4">
<div className="flex items-start">
<input
type="radio"
id="payment-cash"
name="payment_method"
value="cash_on_delivery"
checked={form.payment_method === 'cash_on_delivery'}
onChange={handleChange}
className="mt-1 mr-3"
/>
<label htmlFor="payment-cash" className="flex-grow cursor-pointer">
<div className="font-medium">Наличные</div>
<div className="text-sm text-gray-600 mt-1">Оплата наличными при получении</div>
</label>
</div>
</div>
<div className="border rounded-md p-4">
<div className="flex items-start">
<input
type="radio"
id="payment-paypal"
name="payment_method"
value="paypal"
checked={form.payment_method === 'paypal'}
onChange={handleChange}
className="mt-1 mr-3"
/>
<label htmlFor="payment-paypal" className="flex-grow cursor-pointer">
<div className="font-medium">PayPal</div>
<div className="text-sm text-gray-600 mt-1">Оплата через PayPal</div>
</label>
</div>
</div>
<div className="border rounded-md p-4">
<div className="flex items-start">
<input
type="radio"
id="payment-bank"
name="payment_method"
value="bank_transfer"
checked={form.payment_method === 'bank_transfer'}
onChange={handleChange}
className="mt-1 mr-3"
/>
<label htmlFor="payment-bank" className="flex-grow cursor-pointer">
<div className="font-medium">Банковский перевод</div>
<div className="text-sm text-gray-600 mt-1">Оплата банковским переводом</div>
</label>
</div>
</div>
</div>
</div>
</div>
{/* Комментарий к заказу */}
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
<div className="p-6">
<h2 className="text-lg font-semibold mb-4">Комментарий к заказу</h2>
<textarea
name="notes"
value={form.notes}
onChange={handleChange}
placeholder="Дополнительная информация к заказу"
className="w-full border border-gray-300 rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-black"
rows={3}
/>
</div>
</div>
<div className="lg:hidden">
<button
type="submit"
disabled={submitting || !formValidated}
className="w-full bg-black text-white py-3 rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitting ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
Оформление...
</div>
) : (
'Оформить заказ'
)}
</button>
</div>
</form>
</div>
{/* Сводка заказа */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-md p-6 sticky top-24">
<h2 className="text-lg font-semibold mb-6">Ваш заказ</h2>
<div className="space-y-4 mb-6 max-h-80 overflow-y-auto">
{cart.items.map((item) => (
<div key={item.id} className="flex items-start py-2">
<div className="flex-shrink-0 w-16 h-16 mr-4 relative">
{item.product_image ? (
<Image
src={getImageUrl(item.product_image)}
alt={item.product_name}
fill
className="object-cover rounded-md"
/>
) : (
<div className="w-full h-full bg-gray-200 rounded-md flex items-center justify-center">
<ShoppingBag className="w-6 h-6 text-gray-400" />
</div>
)}
</div>
<div className="flex-grow">
<h3 className="text-sm font-medium">{item.product_name}</h3>
<p className="text-xs text-gray-500">Вариант: {item.variant_name}</p>
<div className="flex justify-between mt-1">
<span className="text-xs text-gray-500">Кол-во: {item.quantity}</span>
<span className="text-sm font-medium">{item.total_price.toLocaleString('ru-RU')} </span>
</div>
</div>
</div>
))}
</div>
<div className="border-t border-gray-200 pt-4 space-y-4">
<div className="flex justify-between">
<span className="text-gray-600">Товары ({cart.items_count})</span>
<span>{cart.total_amount.toLocaleString('ru-RU')} </span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Доставка</span>
<span>Бесплатно</span>
</div>
<div className="border-t border-gray-200 pt-4 flex justify-between font-semibold">
<span>Итого</span>
<span>{cart.total_amount.toLocaleString('ru-RU')} </span>
</div>
</div>
<div className="hidden lg:block">
<button
type="submit"
form="checkout-form"
disabled={submitting || !formValidated}
className="w-full bg-black text-white py-3 rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{submitting ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
Оформление...
</div>
) : (
'Оформить заказ'
)}
</button>
</div>
<div className="mt-6">
<Link href="/cart" className="text-center block text-gray-600 hover:text-gray-800 transition-colors">
Вернуться в корзину
</Link>
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-16">
<div className="flex justify-center mb-6">
<ShoppingBag className="w-16 h-16 text-gray-400" />
</div>
<h2 className="text-xl font-semibold mb-2">Ваша корзина пуста</h2>
<p className="text-gray-600 mb-8">Добавьте товары в корзину, чтобы оформить заказ</p>
<Link href="/all-products" className="bg-black text-white px-6 py-3 rounded-md hover:bg-gray-800 transition-colors">
Перейти к покупкам
</Link>
</div>
)}
</div>
</main>
<Footer />
</div>
);
}