271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
import { useState } from 'react'
|
||
import Alert from '@mui/material/Alert'
|
||
import Box from '@mui/material/Box'
|
||
import Button from '@mui/material/Button'
|
||
import FormControl from '@mui/material/FormControl'
|
||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||
import InputLabel from '@mui/material/InputLabel'
|
||
import Link from '@mui/material/Link'
|
||
import MenuItem from '@mui/material/MenuItem'
|
||
import Radio from '@mui/material/Radio'
|
||
import RadioGroup from '@mui/material/RadioGroup'
|
||
import Select from '@mui/material/Select'
|
||
import Stack from '@mui/material/Stack'
|
||
import TextField from '@mui/material/TextField'
|
||
import Typography from '@mui/material/Typography'
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||
import { useUnit } from 'effector-react'
|
||
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||
import { fetchMyCart } from '@/entities/cart/api/cart-api'
|
||
import { createOrder } from '@/entities/order/api/order-api'
|
||
import { fetchMyAddresses } from '@/entities/user/api/address-api'
|
||
import { DELIVERY_CARRIER_OPTIONS, type DeliveryCarrierCode } from '@/shared/constants/delivery-carrier'
|
||
import { formatPriceRub } from '@/shared/lib/format-price'
|
||
import { $user } from '@/shared/model/auth'
|
||
|
||
export function CheckoutPage() {
|
||
const user = useUnit($user)
|
||
const qc = useQueryClient()
|
||
const navigate = useNavigate()
|
||
const [deliveryType, setDeliveryType] = useState<'delivery' | 'pickup'>('delivery')
|
||
const [pickupPayment, setPickupPayment] = useState<'online' | 'on_pickup'>('online')
|
||
const [deliveryCarrier, setDeliveryCarrier] = useState<DeliveryCarrierCode>('RUSSIAN_POST')
|
||
const [addressId, setAddressId] = useState('')
|
||
const [comment, setComment] = useState('')
|
||
|
||
const cartQuery = useQuery({
|
||
queryKey: ['me', 'cart'],
|
||
queryFn: fetchMyCart,
|
||
enabled: Boolean(user),
|
||
})
|
||
|
||
const addressesQuery = useQuery({
|
||
queryKey: ['me', 'addresses'],
|
||
queryFn: fetchMyAddresses,
|
||
enabled: Boolean(user),
|
||
})
|
||
|
||
const defaultAddressId = addressesQuery.data?.items?.find((a) => a.isDefault)?.id ?? ''
|
||
const selectedAddressId = addressId || defaultAddressId
|
||
|
||
const createMut = useMutation({
|
||
mutationFn: () =>
|
||
createOrder({
|
||
deliveryType,
|
||
deliveryCarrier: deliveryType === 'delivery' ? deliveryCarrier : null,
|
||
paymentMethod: deliveryType === 'delivery' ? 'online' : pickupPayment,
|
||
addressId: deliveryType === 'delivery' ? selectedAddressId : null,
|
||
comment: comment.trim() || null,
|
||
}),
|
||
onSuccess: async (res) => {
|
||
await qc.invalidateQueries({ queryKey: ['me', 'cart'] })
|
||
navigate(`/me/orders/${res.orderId}`, { replace: true })
|
||
},
|
||
})
|
||
|
||
if (!user) {
|
||
return (
|
||
<Alert severity="info">
|
||
Чтобы оформить заказ, нужно войти. Перейдите на страницу{' '}
|
||
<Typography component={RouterLink} to="/auth" sx={{ textDecoration: 'underline' }}>
|
||
Вход
|
||
</Typography>
|
||
.
|
||
</Alert>
|
||
)
|
||
}
|
||
|
||
const items = cartQuery.data?.items ?? []
|
||
const itemsSubtotalCents = items.reduce((s, x) => s + x.product.priceCents * x.qty, 0)
|
||
const deliveryFeeCents = deliveryType === 'delivery' && items.length > 0 ? 50000 : 0
|
||
const total = itemsSubtotalCents + deliveryFeeCents
|
||
const addresses = addressesQuery.data?.items ?? []
|
||
const hasOverLimit = items.some((x) => x.qty > x.product.quantity)
|
||
|
||
return (
|
||
<Box>
|
||
<Typography variant="h4" gutterBottom>
|
||
Оформление заказа
|
||
</Typography>
|
||
|
||
{cartQuery.isSuccess && items.length === 0 && (
|
||
<Alert severity="info" sx={{ mb: 2 }}>
|
||
Корзина пуста. Вернитесь в{' '}
|
||
<Typography component={RouterLink} to="/" sx={{ textDecoration: 'underline' }}>
|
||
каталог
|
||
</Typography>
|
||
.
|
||
</Alert>
|
||
)}
|
||
|
||
<Stack spacing={2} sx={{ maxWidth: 720 }}>
|
||
{items.length > 0 && (
|
||
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
|
||
<Typography sx={{ fontWeight: 700, mb: 1 }}>Позиции</Typography>
|
||
<Stack spacing={0.5}>
|
||
{items.map((x) => {
|
||
const available = x.product.quantity
|
||
const over = x.qty > available
|
||
return (
|
||
<Typography key={x.id} color={over ? 'error' : 'text.primary'}>
|
||
{x.product.title}: {x.qty} шт. (доступно {available})
|
||
</Typography>
|
||
)
|
||
})}
|
||
</Stack>
|
||
</Box>
|
||
)}
|
||
|
||
{hasOverLimit && (
|
||
<Alert severity="warning">
|
||
Некоторые позиции превышают доступное количество. Вернитесь в{' '}
|
||
<Typography component={RouterLink} to="/cart" sx={{ textDecoration: 'underline' }}>
|
||
корзину
|
||
</Typography>{' '}
|
||
и скорректируйте количество.
|
||
</Alert>
|
||
)}
|
||
|
||
<FormControl size="small" fullWidth>
|
||
<InputLabel id="delivery-type-label">Способ получения</InputLabel>
|
||
<Select
|
||
labelId="delivery-type-label"
|
||
label="Способ получения"
|
||
value={deliveryType}
|
||
onChange={(e) => {
|
||
const v = String(e.target.value)
|
||
if (v === 'delivery' || v === 'pickup') {
|
||
setDeliveryType(v)
|
||
if (v === 'delivery') setPickupPayment('online')
|
||
}
|
||
}}
|
||
>
|
||
<MenuItem value="delivery">Доставка</MenuItem>
|
||
<MenuItem value="pickup">Самовывоз</MenuItem>
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{deliveryType === 'delivery' && (
|
||
<>
|
||
<Box>
|
||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 0.5 }}>
|
||
Служба доставки
|
||
</Typography>
|
||
<RadioGroup
|
||
value={deliveryCarrier}
|
||
onChange={(e) => {
|
||
const v = String(e.target.value) as DeliveryCarrierCode
|
||
setDeliveryCarrier(v)
|
||
}}
|
||
>
|
||
{DELIVERY_CARRIER_OPTIONS.map((o) => (
|
||
<FormControlLabel key={o.code} value={o.code} control={<Radio size="small" />} label={o.label} />
|
||
))}
|
||
</RadioGroup>
|
||
</Box>
|
||
|
||
<FormControl size="small" fullWidth>
|
||
<InputLabel id="addr-label">Адрес доставки</InputLabel>
|
||
<Select
|
||
labelId="addr-label"
|
||
label="Адрес доставки"
|
||
value={selectedAddressId}
|
||
onChange={(e) => setAddressId(String(e.target.value))}
|
||
>
|
||
{addresses.map((a) => (
|
||
<MenuItem key={a.id} value={a.id}>
|
||
{(a.label?.trim() ? `${a.label}: ` : '') + a.addressLine}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{addresses.length === 0 && (
|
||
<Alert severity="warning">
|
||
У вас нет адресов доставки. Добавьте адрес в{' '}
|
||
<Typography component={RouterLink} to="/me/addresses" sx={{ textDecoration: 'underline' }}>
|
||
кабинете
|
||
</Typography>
|
||
.
|
||
</Alert>
|
||
)}
|
||
|
||
<Alert severity="info">
|
||
Стоимость доставки ориентировочно 300 ₽. Точная цена будет скорректирована после расчёта. В сумме заказа
|
||
сейчас заложено {items.length > 0 ? formatPriceRub(deliveryFeeCents) : '500 ₽'} до уточнения.
|
||
</Alert>
|
||
</>
|
||
)}
|
||
|
||
{deliveryType === 'pickup' && (
|
||
<>
|
||
<Alert severity="info" sx={{ mb: 0 }}>
|
||
Самовывоз: адрес доставки не нужен. Адрес точки выдачи указан на странице{' '}
|
||
<Link component={RouterLink} to="/about" underline="hover" sx={{ fontWeight: 700 }}>
|
||
О нас
|
||
</Link>
|
||
.
|
||
</Alert>
|
||
<Box>
|
||
<Typography variant="subtitle2" gutterBottom sx={{ mt: 0.5 }}>
|
||
Оплата
|
||
</Typography>
|
||
<RadioGroup
|
||
value={pickupPayment}
|
||
onChange={(e) => {
|
||
const v = String(e.target.value)
|
||
if (v === 'online' || v === 'on_pickup') setPickupPayment(v)
|
||
}}
|
||
>
|
||
<FormControlLabel value="online" control={<Radio size="small" />} label="Онлайн-оплата" />
|
||
<FormControlLabel value="on_pickup" control={<Radio size="small" />} label="Оплатить при получении" />
|
||
</RadioGroup>
|
||
{pickupPayment === 'on_pickup' && (
|
||
<Alert severity="info" sx={{ mt: 1 }}>
|
||
Оплатите заказ наличными или картой при выдаче на самовывозе.
|
||
</Alert>
|
||
)}
|
||
</Box>
|
||
</>
|
||
)}
|
||
|
||
<TextField
|
||
label="Комментарий к заказу (необязательно)"
|
||
value={comment}
|
||
onChange={(e) => setComment(e.target.value)}
|
||
fullWidth
|
||
multiline
|
||
minRows={2}
|
||
/>
|
||
|
||
<Stack spacing={0.25}>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Товары: {formatPriceRub(itemsSubtotalCents)}
|
||
</Typography>
|
||
{deliveryType === 'delivery' && (
|
||
<Typography variant="body2" color="text.secondary">
|
||
Доставка: {formatPriceRub(deliveryFeeCents)}
|
||
</Typography>
|
||
)}
|
||
<Typography variant="h6">Итого: {formatPriceRub(total)}</Typography>
|
||
</Stack>
|
||
|
||
<Button
|
||
variant="contained"
|
||
disabled={
|
||
items.length === 0 ||
|
||
hasOverLimit ||
|
||
createMut.isPending ||
|
||
(deliveryType === 'delivery' && (addresses.length === 0 || !selectedAddressId))
|
||
}
|
||
onClick={() => createMut.mutate()}
|
||
>
|
||
Создать заказ
|
||
</Button>
|
||
|
||
{createMut.isError && <Alert severity="error">{(createMut.error as Error).message}</Alert>}
|
||
</Stack>
|
||
</Box>
|
||
)
|
||
}
|