Files
shop-server/client/src/pages/me/ui/sections/AddressesPage.tsx
T
2026-05-19 11:25:23 +05:00

222 lines
7.0 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 } 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>
)
}