test commit

This commit is contained in:
Kirill
2026-05-21 12:02:29 +05:00
parent ae6f86041a
commit 1837b36b14
10 changed files with 113 additions and 45 deletions
@@ -20,8 +20,8 @@ type Props = {
error: unknown
uploadError: unknown
onClose: () => void
onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => void
onUploadImage: (file: File) => void
onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => Promise<void>
onUploadImage: (file: File) => Promise<{ url: string }>
}
function reviewSubmitErrorMessage(err: unknown): string {
@@ -55,11 +55,13 @@ export function ReviewDialog({
const [rating, setRating] = useState<number>(5)
const [text, setText] = useState('')
const [imageUrl, setImageUrl] = useState<string | null>(null)
const [localUploadError, setLocalUploadError] = useState<string | null>(null)
const reset = () => {
setRating(5)
setText('')
setImageUrl(null)
setLocalUploadError(null)
}
const handleClose = () => {
@@ -68,9 +70,9 @@ export function ReviewDialog({
onClose()
}
const handleSubmit = () => {
const handleSubmit = async () => {
if (isPending) return
onSubmit({ rating, text: text.trim(), imageUrl })
await onSubmit({ rating, text: text.trim(), imageUrl })
}
return (
@@ -96,11 +98,19 @@ export function ReviewDialog({
hidden
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={(e) => {
onChange={async (e) => {
const file = e.target.files?.[0]
if (!file) return
onUploadImage(file)
e.currentTarget.value = ''
setLocalUploadError(null)
try {
const result = await onUploadImage(file)
setImageUrl(result.url)
} catch (err) {
setLocalUploadError(
err instanceof Error ? err.message : 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.',
)
}
}}
/>
</Button>
@@ -126,11 +136,13 @@ export function ReviewDialog({
}}
/>
)}
{uploadError ? (
{uploadError || localUploadError ? (
<Alert severity="error" sx={{ mt: 2 }}>
{uploadError instanceof Error
? uploadError.message
: 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.'}
{localUploadError
? localUploadError
: uploadError instanceof Error
? uploadError.message
: 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.'}
</Alert>
) : null}
{error ? (
@@ -17,7 +17,12 @@ type Props = {
isUploadPending: boolean
submitError: unknown
uploadError: unknown
onSubmitReview: (params: { productId: string; rating: number; text: string; imageUrl: string | null }) => void
onSubmitReview: (params: {
productId: string
rating: number
text: string
imageUrl: string | null
}) => Promise<void>
onUploadImage: (file: File) => Promise<{ url: string }>
}
@@ -75,17 +80,20 @@ export function ReviewSection({
setTarget(null)
setUploadedImageUrl(null)
}}
onSubmit={(params) => {
onSubmit={async (params) => {
if (!target) return
onSubmitReview({
await onSubmitReview({
productId: target.productId,
...params,
imageUrl: uploadedImageUrl,
})
setTarget(null)
setUploadedImageUrl(null)
}}
onUploadImage={async (file) => {
const result = await onUploadImage(file)
setUploadedImageUrl(result.url)
return result
}}
/>
</Box>
@@ -9,6 +9,7 @@ import TableRow from '@mui/material/TableRow'
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 { Controller, useForm } from 'react-hook-form'
import { Link as RouterLink } from 'react-router-dom'
import { createAdminUser, deleteAdminUser, fetchAdminUsers, updateAdminUser } from '@/entities/user/api/user-api'
@@ -16,6 +17,7 @@ import type { AdminUser } from '@/entities/user/model/types'
import { getErrorMessage } from '@/shared/lib/get-error-message'
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
import { useEditDialogState } from '@/shared/lib/use-edit-dialog-state'
import { $user } from '@/shared/model/auth'
import { AdminDialog } from '@/shared/ui/AdminDialog/AdminDialog'
import { AdminTable } from '@/shared/ui/AdminTable'
import { EntityRowActions } from '@/shared/ui/EntityRowActions'
@@ -44,6 +46,8 @@ export function AdminUsersPage() {
const [q, setQ] = useState('')
const [page, setPage] = useState(0)
const [rowsPerPage, setRowsPerPage] = useState(20)
const currentUser = useUnit($user)
const currentUserId = currentUser?.id
const userForm = useForm<UserFormState>({
defaultValues: emptyUserForm(),
@@ -192,7 +196,7 @@ export function AdminUsersPage() {
<TableCell align="right">
<EntityRowActions
onEdit={() => openEdit(u)}
onDelete={() => deleteMut.mutate(u.id)}
onDelete={u.id === currentUserId ? undefined : () => deleteMut.mutate(u.id)}
deleteDisabled={deleteMut.isPending}
confirmDeleteMessage={`Удалить пользователя ${u.email}?`}
/>
@@ -237,7 +241,15 @@ export function AdminUsersPage() {
<Controller
control={userForm.control}
name="email"
render={({ field }) => <TextField label="Почта" fullWidth required {...field} />}
render={({ field }) => (
<TextField
label="Почта"
fullWidth
required
disabled={Boolean(editing && editing.id === currentUserId)}
{...field}
/>
)}
/>
<Controller
control={userForm.control}
@@ -10,14 +10,18 @@ import {
fetchUserNotificationSettings,
updateUserNotificationSettings,
} from '@/entities/notification/api/notifications-api'
import type { UserNotificationSettings } from '@/entities/notification/api/notifications-api'
const eventFields = [
{ key: 'orderCreated' as const, label: 'Заказ создан' },
{ key: 'orderStatusChanged' as const, label: 'Изменение статуса заказа' },
{ key: 'orderMessageReceived' as const, label: 'Сообщение в чате заказа' },
{ key: 'paymentStatusChanged' as const, label: 'Изменение статуса оплаты' },
{ key: 'deliveryFeeAdjusted' as const, label: 'Корректировка стоимости доставки' },
]
function isOrderStatusChangesOn(s: UserNotificationSettings): boolean {
return s.orderCreated && s.orderStatusChanged && s.paymentStatusChanged && s.deliveryFeeAdjusted
}
const orderStatusChangesPayload = (on: boolean) => ({
orderCreated: on,
orderStatusChanged: on,
paymentStatusChanged: on,
deliveryFeeAdjusted: on,
})
export function NotificationsPage() {
const queryClient = useQueryClient()
@@ -45,9 +49,11 @@ export function NotificationsPage() {
const handleToggle = (field: string, value: boolean) => {
setError(null)
mutation.mutate({ [field]: value } as Record<string, boolean>)
mutation.mutate({ [field]: value })
}
const statusChangesOn = isOrderStatusChangesOn(settings)
return (
<Box>
<Typography variant="h4" gutterBottom>
@@ -80,19 +86,26 @@ export function NotificationsPage() {
</Box>
<Box sx={{ pl: 4 }}>
{eventFields.map(({ key, label }) => (
<FormControlLabel
key={key}
control={
<Switch
checked={settings[key]}
disabled={!settings.globalEnabled}
onChange={(e) => handleToggle(key, e.target.checked)}
/>
}
label={label}
/>
))}
<FormControlLabel
control={
<Switch
checked={statusChangesOn}
disabled={!settings.globalEnabled}
onChange={(e) => mutation.mutate(orderStatusChangesPayload(e.target.checked))}
/>
}
label="Изменения статуса заказа"
/>
<FormControlLabel
control={
<Switch
checked={settings.orderMessageReceived}
disabled={!settings.globalEnabled}
onChange={(e) => handleToggle('orderMessageReceived', e.target.checked)}
/>
}
label="Сообщения в чате заказа"
/>
</Box>
</Stack>
</Box>
@@ -7,7 +7,7 @@ import Link from '@mui/material/Link'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link as RouterLink, useParams, useSearchParams } from 'react-router-dom'
import { Link as RouterLink, useNavigate, useParams, useSearchParams } from 'react-router-dom'
import {
confirmOrderReceived,
createOrderPayment,
@@ -30,6 +30,7 @@ import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
export function OrderDetailPage() {
const { id } = useParams()
const qc = useQueryClient()
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const paidParam = searchParams.get('paid')
@@ -58,6 +59,14 @@ export function OrderDetailPage() {
},
})
useEffect(() => {
const data = paymentStatusQuery.data
if (data && (data.paid || data.status === 'canceled') && paidParam === '1') {
qc.invalidateQueries({ queryKey: ['me', 'orders', id] })
navigate(`/me/orders/${id}`, { replace: true })
}
}, [paymentStatusQuery.data, paidParam, qc, id, navigate])
const confirmMut = useMutation({
mutationFn: () => confirmOrderReceived(id!),
onSuccess: () =>
@@ -224,7 +233,7 @@ export function OrderDetailPage() {
{PICKUP_ADDRESS_FULL}
</Typography>
<Typography color="text.secondary" variant="body2">
Заберите заказ точно ко времени, которое согласуем по телефону или в чате заказа.
Заберите заказ ко времени, которое согласуем в чате заказа.
</Typography>
</Stack>
)}
@@ -274,7 +283,9 @@ export function OrderDetailPage() {
isUploadPending={uploadReviewImageMut.isPending}
submitError={reviewMut.error}
uploadError={uploadReviewImageMut.error}
onSubmitReview={(params) => reviewMut.mutate(params)}
onSubmitReview={async (params) => {
await reviewMut.mutateAsync(params)
}}
onUploadImage={async (file) => {
const result = await uploadReviewImageMut.mutateAsync(file)
return result
@@ -96,7 +96,7 @@ export function ReviewsBlock() {
<OptimizedImage
src={r.imageUrl}
alt="Фото к отзыву"
widths={[160, 320]}
widths={[320, 640]}
sizes="80px"
sx={{
width: '100%',