222 lines
7.0 KiB
TypeScript
222 lines
7.0 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 Chip from '@mui/material/Chip'
|
||
import Stack from '@mui/material/Stack'
|
||
import Typography from '@mui/material/Typography'
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||
import { useForm } from 'react-hook-form'
|
||
import {
|
||
createMyAddress,
|
||
deleteMyAddress,
|
||
fetchMyAddresses,
|
||
setMyAddressDefault,
|
||
updateMyAddress,
|
||
} from '@/entities/user/api/address-api'
|
||
import type { ShippingAddress } from '@/entities/user/model/types'
|
||
import { AddressFormDialog, type AddressFormValues } from '@/features/address-form'
|
||
|
||
const defaultAddressForm = (isDefault: boolean): AddressFormValues => ({
|
||
label: '',
|
||
recipientName: '',
|
||
recipientPhone: '',
|
||
addressLine: '',
|
||
comment: '',
|
||
lat: null,
|
||
lng: null,
|
||
isDefault,
|
||
})
|
||
|
||
export function AddressesPage() {
|
||
const queryClient = useQueryClient()
|
||
const [open, setOpen] = useState(false)
|
||
const [editing, setEditing] = useState<ShippingAddress | null>(null)
|
||
|
||
const addressesQuery = useQuery({
|
||
queryKey: ['me', 'addresses'],
|
||
queryFn: fetchMyAddresses,
|
||
})
|
||
|
||
const form = useForm<AddressFormValues>({
|
||
defaultValues: defaultAddressForm(false),
|
||
mode: 'onChange',
|
||
})
|
||
|
||
const createMut = useMutation({
|
||
mutationFn: async () => {
|
||
const v = form.getValues()
|
||
if (v.lat === null || v.lng === null) throw new Error('Выберите точку на карте')
|
||
await createMyAddress({
|
||
label: v.label.trim() || null,
|
||
recipientName: v.recipientName.trim(),
|
||
recipientPhone: v.recipientPhone.trim(),
|
||
addressLine: v.addressLine.trim(),
|
||
comment: v.comment.trim() || null,
|
||
lat: v.lat,
|
||
lng: v.lng,
|
||
isDefault: v.isDefault,
|
||
})
|
||
},
|
||
onSuccess: () => {
|
||
void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] })
|
||
setOpen(false)
|
||
},
|
||
})
|
||
|
||
const updateMut = useMutation({
|
||
mutationFn: async () => {
|
||
const v = form.getValues()
|
||
if (v.lat === null || v.lng === null) throw new Error('Выберите точку на карте')
|
||
await updateMyAddress(editing!.id, {
|
||
label: v.label.trim() || null,
|
||
recipientName: v.recipientName.trim(),
|
||
recipientPhone: v.recipientPhone.trim(),
|
||
addressLine: v.addressLine.trim(),
|
||
comment: v.comment.trim() || null,
|
||
lat: v.lat,
|
||
lng: v.lng,
|
||
isDefault: v.isDefault,
|
||
})
|
||
},
|
||
onSuccess: () => {
|
||
void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] })
|
||
setOpen(false)
|
||
},
|
||
})
|
||
|
||
const deleteMut = useMutation({
|
||
mutationFn: (id: string) => deleteMyAddress(id),
|
||
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] }),
|
||
})
|
||
|
||
const defaultMut = useMutation({
|
||
mutationFn: (id: string) => setMyAddressDefault(id),
|
||
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] }),
|
||
})
|
||
|
||
const error = createMut.error ?? updateMut.error ?? deleteMut.error ?? defaultMut.error
|
||
|
||
const items = addressesQuery.data?.items ?? []
|
||
|
||
const openCreate = () => {
|
||
setEditing(null)
|
||
form.reset(defaultAddressForm(items.length === 0))
|
||
setOpen(true)
|
||
}
|
||
|
||
const openEdit = (a: ShippingAddress) => {
|
||
setEditing(a)
|
||
form.reset({
|
||
label: a.label ?? '',
|
||
recipientName: a.recipientName,
|
||
recipientPhone: a.recipientPhone,
|
||
addressLine: a.addressLine,
|
||
comment: a.comment ?? '',
|
||
lat: a.lat,
|
||
lng: a.lng,
|
||
isDefault: a.isDefault,
|
||
})
|
||
setOpen(true)
|
||
}
|
||
|
||
const handleSubmit = () => {
|
||
if (editing) updateMut.mutate()
|
||
else createMut.mutate()
|
||
}
|
||
|
||
return (
|
||
<Box>
|
||
<Typography variant="h4" gutterBottom>
|
||
Адреса доставки
|
||
</Typography>
|
||
|
||
<Typography color="text.secondary" sx={{ mb: 2 }}>
|
||
Можно добавить несколько адресов. Для каждого адреса задаются ФИО и телефон получателя отдельно.
|
||
</Typography>
|
||
|
||
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
|
||
<Button variant="contained" onClick={openCreate}>
|
||
Добавить адрес
|
||
</Button>
|
||
</Stack>
|
||
|
||
{addressesQuery.isError && (
|
||
<Alert severity="error" sx={{ mb: 2 }}>
|
||
Не удалось загрузить адреса. Проверьте, что вы вошли в аккаунт и сервер запущен.
|
||
</Alert>
|
||
)}
|
||
{error && (
|
||
<Alert severity="error" sx={{ mb: 2 }}>
|
||
{(error as Error).message}
|
||
</Alert>
|
||
)}
|
||
|
||
<Stack spacing={2}>
|
||
{items.map((a) => (
|
||
<Box
|
||
key={a.id}
|
||
sx={{
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
borderRadius: 2,
|
||
p: 2,
|
||
bgcolor: 'background.paper',
|
||
}}
|
||
>
|
||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'center' } }}>
|
||
<Typography sx={{ fontWeight: 700, flexGrow: 1 }}>{a.label?.trim() ? a.label : 'Адрес'}</Typography>
|
||
{a.isDefault && <Chip label="По умолчанию" color="primary" size="small" />}
|
||
</Stack>
|
||
|
||
<Typography sx={{ mt: 1 }}>{a.addressLine}</Typography>
|
||
<Typography color="text.secondary" variant="body2" sx={{ mt: 0.5 }}>
|
||
Получатель: {a.recipientName} · {a.recipientPhone}
|
||
</Typography>
|
||
{a.comment && (
|
||
<Typography color="text.secondary" variant="body2" sx={{ mt: 0.5 }}>
|
||
Комментарий: {a.comment}
|
||
</Typography>
|
||
)}
|
||
|
||
<Stack direction="row" spacing={1} sx={{ mt: 1.5, flexWrap: 'wrap' }}>
|
||
{!a.isDefault && (
|
||
<Button size="small" onClick={() => defaultMut.mutate(a.id)} disabled={defaultMut.isPending}>
|
||
Сделать основным
|
||
</Button>
|
||
)}
|
||
<Button size="small" onClick={() => openEdit(a)}>
|
||
Изменить
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
color="error"
|
||
disabled={deleteMut.isPending}
|
||
onClick={() => {
|
||
if (!confirm('Удалить адрес?')) return
|
||
deleteMut.mutate(a.id)
|
||
}}
|
||
>
|
||
Удалить
|
||
</Button>
|
||
</Stack>
|
||
</Box>
|
||
))}
|
||
|
||
{addressesQuery.isSuccess && items.length === 0 && (
|
||
<Alert severity="info">Адресов пока нет — добавьте первый адрес доставки.</Alert>
|
||
)}
|
||
</Stack>
|
||
|
||
<AddressFormDialog
|
||
open={open}
|
||
onClose={() => setOpen(false)}
|
||
editing={Boolean(editing)}
|
||
form={form}
|
||
onSubmit={handleSubmit}
|
||
isPending={createMut.isPending || updateMut.isPending}
|
||
/>
|
||
</Box>
|
||
)
|
||
}
|