147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
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>
|
||
) : null}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={handleClose} disabled={isPending}>
|
||
Отмена
|
||
</Button>
|
||
<Button variant="contained" disabled={isPending} onClick={handleSubmit}>
|
||
Подтвердить оплату
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
)
|
||
}
|