Merge branch 'refactor'

This commit is contained in:
@kirill.komarov
2026-05-13 22:07:46 +05:00
parent 3c9797af4a
commit a06f9cf2c4
85 changed files with 3762 additions and 2072 deletions
@@ -0,0 +1,146 @@
import { useEffect, useMemo, useState } from 'react'
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
import Stack from '@mui/material/Stack'
import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
import axios from 'axios'
import { PAYMENT_TRANSFER_INSTRUCTIONS_PLAIN } from '@/shared/constants/payment-instructions'
type Props = {
open: boolean
isPending: boolean
error: unknown
onClose: () => void
onSubmit: (params: { detail: string; receiptFile: File | null }) => void
}
function paySubmitErrorMessage(err: unknown): string {
if (axios.isAxiosError(err)) {
const raw = err.response?.data
const apiMsg =
raw && typeof raw === 'object' && 'error' in raw && typeof (raw as { error: unknown }).error === 'string'
? (raw as { error: string }).error
: null
return apiMsg || err.message || 'Не удалось отправить данные оплаты'
}
if (err instanceof Error) return err.message
return 'Не удалось отправить данные оплаты'
}
export function PaymentDialog({ open, isPending, error, onClose, onSubmit }: Props) {
const [detail, setDetail] = useState('')
const [receiptFile, setReceiptFile] = useState<File | null>(null)
const [clientError, setClientError] = useState<string | null>(null)
const receiptPreviewUrl = useMemo(() => {
if (!receiptFile) return null
return URL.createObjectURL(receiptFile)
}, [receiptFile])
useEffect(() => {
if (!receiptPreviewUrl) return
return () => URL.revokeObjectURL(receiptPreviewUrl)
}, [receiptPreviewUrl])
const reset = () => {
setDetail('')
setReceiptFile(null)
setClientError(null)
}
const handleClose = () => {
if (isPending) return
reset()
onClose()
}
const handleSubmit = () => {
const hasText = detail.trim().length > 0
const hasFile = Boolean(receiptFile)
if (!hasText && !hasFile) {
setClientError('Укажите комментарий и/или прикрепите чек.')
return
}
setClientError(null)
onSubmit({ detail: detail.trim(), receiptFile })
}
return (
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
<DialogTitle>Подтверждение оплаты</DialogTitle>
<DialogContent>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', mb: 2 }}>
{PAYMENT_TRANSFER_INSTRUCTIONS_PLAIN}
</Typography>
<TextField
label="Комментарий об оплате (сумма, время перевода и т.д.)"
value={detail}
onChange={(e) => {
setDetail(e.target.value)
setClientError(null)
}}
fullWidth
multiline
minRows={3}
sx={{ mb: 2 }}
/>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mb: 1, alignItems: { sm: 'center' } }}>
<Button component="label" variant="outlined">
Прикрепить чек (png, jpg, webp)
<input
hidden
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={(e) => {
const file = e.target.files?.[0]
setReceiptFile(file ?? null)
setClientError(null)
e.currentTarget.value = ''
}}
/>
</Button>
{receiptFile && (
<Button color="error" variant="text" onClick={() => setReceiptFile(null)}>
Убрать файл
</Button>
)}
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
Нужен текст комментария и/или изображение чека.
</Typography>
{receiptPreviewUrl && (
<Box
component="img"
src={receiptPreviewUrl}
alt="Предпросмотр чека"
sx={{ maxWidth: '100%', maxHeight: 200, borderRadius: 1, border: 1, borderColor: 'divider', mb: 1 }}
/>
)}
{clientError && (
<Alert severity="warning" sx={{ mb: 1 }}>
{clientError}
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{paySubmitErrorMessage(error)}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={isPending}>
Отмена
</Button>
<Button variant="contained" disabled={isPending} onClick={handleSubmit}>
Подтвердить оплату
</Button>
</DialogActions>
</Dialog>
)
}