Files
frontend/src/pages/transactions/ui/TransactionsPage.tsx
2026-05-22 22:34:00 +03:00

228 lines
8.5 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 { WalletHeader } from '@widgets/wallet-header'
import { Footer } from '@widgets/footer'
import { useOrders } from '@features/payment'
import type { OrderWithPayment, OrderStatus } from '@features/payment'
import styles from './TransactionsPage.module.css'
const ORDER_STATUS_LABELS: Record<OrderStatus, string> = {
pending: 'Ожидание',
completed: 'Выполнена',
cancelled: 'Отменена',
failed: 'Ошибка',
}
const PAYMENT_STATUS_LABELS: Record<OrderStatus, string> = {
pending: 'Ожидание',
completed: 'Оплачен',
cancelled: 'Отменён',
failed: 'Ошибка',
}
function formatDate(iso: string) {
if (!iso) return '—'
return new Date(iso).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function truncateHash(hash: string) {
if (!hash) return '—'
if (hash.length <= 16) return hash
return `${hash.slice(0, 8)}${hash.slice(-6)}`
}
function StatusBadge({ status, labels }: { status: OrderStatus; labels: Record<OrderStatus, string> }) {
return (
<span className={`${styles.statusBadge} ${styles[`status_${status}`]}`}>
{labels[status] ?? status}
</span>
)
}
function CopyButton({ value }: { value: string }) {
const [copied, setCopied] = useState(false)
function handleCopy() {
navigator.clipboard.writeText(value).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
return (
<button className={styles.copyBtn} onClick={handleCopy} type="button" title="Скопировать">
{copied ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
)}
</button>
)
}
function OrderAccordion({ item }: { item: OrderWithPayment }) {
const [isOpen, setIsOpen] = useState(false)
const { order, payment } = item
return (
<div className={styles.accordionItem}>
<button
className={styles.summaryRow}
onClick={() => setIsOpen(v => !v)}
type="button"
aria-expanded={isOpen}
>
<div className={styles.summaryLeft}>
<span className={styles.summaryDate}>{formatDate(order.created_at)}</span>
<StatusBadge status={order.status} labels={ORDER_STATUS_LABELS} />
<StatusBadge status={payment.status} labels={PAYMENT_STATUS_LABELS} />
</div>
<div className={styles.summaryRight}>
<span className={styles.amount}>{order.usdt_amount} USDT</span>
<span className={styles.totalAmount}>{Number(order.total_price).toLocaleString('ru-RU')} </span>
<svg
className={`${styles.chevron} ${isOpen ? styles.chevronOpen : ''}`}
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
</button>
<div className={`${styles.bodyOuter} ${isOpen ? styles.bodyOuterOpen : ''}`}>
<div className={styles.bodyInner}>
<div className={styles.body}>
<div className={styles.col}>
<p className={styles.colTitle}>Заказ</p>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Сумма USDT</span>
<span className={styles.infoValue}>{order.usdt_amount} USDT</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Курс обмена</span>
<span className={styles.infoValue}>1 USDT = {order.usdt_exchange_rate} </span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Сервисный сбор</span>
<span className={styles.infoValue}>{order.service_fee} USDT</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Комиссия за газ</span>
<span className={styles.infoValue}>{order.gas_fee} USDT</span>
</div>
<div className={`${styles.infoRow} ${styles.infoRowTotal}`}>
<span className={styles.infoLabel}>Итого к оплате</span>
<span className={styles.infoValue}>{Number(order.total_price).toLocaleString('ru-RU')} </span>
</div>
</div>
<div className={styles.col}>
<p className={styles.colTitle}>Платёж</p>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Статус</span>
<StatusBadge status={payment.status} labels={PAYMENT_STATUS_LABELS} />
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Оплачено</span>
<span className={styles.infoValue}>{payment.itpay_paid_amount || '—'}</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Истекает</span>
<span className={styles.infoValue}>{formatDate(payment.expired_date)}</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Хэш транзакции</span>
<span className={styles.infoValueRow}>
<span className={styles.infoValue} title={payment.web3_transaction_hash || undefined}>
{truncateHash(payment.web3_transaction_hash)}
</span>
{payment.web3_transaction_hash && (
<CopyButton value={payment.web3_transaction_hash} />
)}
</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Чек</span>
{payment.receipt_cloudekassir_link ? (
<a
href={payment.receipt_cloudekassir_link}
target="_blank"
rel="noopener noreferrer"
className={styles.infoLink}
>
Открыть
</a>
) : (
<span className={styles.infoValue}></span>
)}
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export function TransactionsPage() {
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } = useOrders()
const items: OrderWithPayment[] = data?.pages.flatMap(p => p.orders) ?? []
return (
<div className={styles.page}>
<WalletHeader />
<main className={styles.main}>
<div className={styles.glow} />
<h1 className={styles.title}>Транзакции</h1>
{isLoading && <p className={styles.status}>Загрузка...</p>}
{isError && <p className={styles.statusError}>Не удалось загрузить транзакции. Попробуйте обновить страницу.</p>}
{!isLoading && !isError && items.length === 0 && (
<p className={styles.empty}>У вас пока нет транзакций.</p>
)}
{items.length > 0 && (
<div className={styles.list}>
{items.map((item) => (
<OrderAccordion key={item.order.id} item={item} />
))}
</div>
)}
{hasNextPage && (
<div className={styles.loadMore}>
<button
className={styles.loadMoreBtn}
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
type="button"
>
{isFetchingNextPage ? 'Загрузка...' : 'Загрузить ещё'}
</button>
</div>
)}
</main>
<Footer />
</div>
)
}