Files
dashboards/apps/business/src/components/OrderTransactionsModal.tsx
T
Alireza HassaniandCursor 5e799eddf9 Localize order modals, add dual logos, and improve admin order tools.
Farsi business order/variation UI, light/dark logo upload on business profile, payment order steps, variation editing, super-admin site links, and checkout/auth fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 00:21:48 +03:30

220 lines
7.3 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { X } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import type { Order, OrderTransaction, TransactionStatus, TransactionType } from '../services/orderService'
import { formatIrtPrice } from '../utils/irtPrice'
import modalStyles from './VariationsModal.module.css'
import styles from './OrderTransactionsModal.module.css'
interface OrderTransactionsModalProps {
open: boolean
order: Order | null
onClose: () => void
}
const ANIMATION_MS = 220
const TYPE_KEYS: Record<TransactionType, BusinessMessageKey> = {
pos: 'orderTransactions.type.pos',
cash: 'orderTransactions.type.cash',
transfer: 'orderTransactions.type.transfer',
e_payment_gate: 'orderTransactions.type.e_payment_gate',
}
const STATUS_KEYS: Record<TransactionStatus, BusinessMessageKey> = {
pending: 'orderTransactions.status.pending',
completed: 'orderTransactions.status.completed',
failed: 'orderTransactions.status.failed',
refunded: 'orderTransactions.status.refunded',
}
function statusClass(status: TransactionStatus) {
switch (status) {
case 'completed':
return styles.statusCompleted
case 'failed':
return styles.statusFailed
case 'refunded':
return styles.statusRefunded
case 'pending':
default:
return styles.statusPending
}
}
function formatDateTime(value: string, locale: 'en' | 'fa') {
const d = new Date(value)
if (Number.isNaN(d.getTime())) return value
return d.toLocaleString(locale === 'fa' ? 'fa-IR' : 'en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
export function OrderTransactionsModal({ open, order, onClose }: OrderTransactionsModalProps) {
const t = useT()
const { locale } = useLocale()
const dateLocale = locale === 'fa' ? 'fa' : 'en'
const [mounted, setMounted] = useState(open)
const [closing, setClosing] = useState(false)
const transactions = order?.transactions ?? []
const totalPaid = useMemo(
() => transactions.reduce((sum, tx) => sum + tx.amount, 0),
[transactions],
)
useEffect(() => {
if (open) {
setMounted(true)
setClosing(false)
} else if (mounted) {
setClosing(true)
const timer = setTimeout(() => {
setMounted(false)
setClosing(false)
}, ANIMATION_MS)
return () => clearTimeout(timer)
}
}, [open, mounted])
useEffect(() => {
if (!mounted || closing) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
}, [mounted, closing, onClose])
if (!mounted || !order) return null
function transactionDetails(transaction: OrderTransaction) {
const rows: { label: string; value: string; isStatus?: boolean }[] = [
{
label: t('orderTransactions.status'),
value: t(STATUS_KEYS[transaction.status] ?? 'orderTransactions.status.pending'),
isStatus: true,
},
{
label: t('orderTransactions.date'),
value: formatDateTime(transaction.createdAt, dateLocale),
},
]
if (transaction.type === 'pos' && transaction.posType) {
rows.push({ label: t('orderTransactions.posType'), value: transaction.posType })
}
if (transaction.type === 'transfer') {
if (transaction.transferAccount) {
rows.push({ label: t('orderTransactions.account'), value: transaction.transferAccount })
}
if (transaction.transferRefNumber) {
rows.push({ label: t('orderTransactions.refCode'), value: transaction.transferRefNumber })
}
}
if (transaction.type === 'e_payment_gate' && transaction.gatewayType) {
rows.push({ label: t('orderTransactions.gateway'), value: transaction.gatewayType })
}
if (transaction.notes) {
rows.push({ label: t('orderTransactions.notes'), value: transaction.notes })
}
return rows
}
return createPortal(
<div
className={`${modalStyles.overlay} ${closing ? modalStyles.overlayOut : modalStyles.overlayIn}`}
onClick={onClose}
>
<div
className={`${modalStyles.modal} ${styles.modalWide} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="order-transactions-title"
>
<div className={modalStyles.header}>
<div>
<h2 id="order-transactions-title" className={modalStyles.title}>
{t('orderTransactions.title')}
</h2>
<p className={modalStyles.subtitle}>{order.orderNumber}</p>
</div>
<button
type="button"
className={modalStyles.closeBtn}
onClick={onClose}
aria-label={t('orderTransactions.close')}
>
<X size={18} />
</button>
</div>
<div className={`${modalStyles.body} ${styles.body}`}>
{transactions.length === 0 ? (
<p className={styles.empty}>{t('orderTransactions.empty')}</p>
) : (
<ul className={styles.txList}>
{transactions.map((transaction) => {
const details = transactionDetails(transaction)
return (
<li key={transaction.id} className={styles.txCard}>
<div className={styles.txHeader}>
<div className={styles.txType}>
{t(TYPE_KEYS[transaction.type] ?? 'orderTransactions.type.e_payment_gate')}
</div>
<div className={styles.txAmount}>{formatIrtPrice(transaction.amount)}</div>
</div>
<div className={styles.txMeta}>
{details.map((detail) => (
<div key={detail.label} className={styles.txMetaItem}>
{detail.label}:{' '}
{detail.isStatus ? (
<span className={`${styles.statusBadge} ${statusClass(transaction.status)}`}>
{detail.value}
</span>
) : (
<strong>{detail.value}</strong>
)}
</div>
))}
</div>
</li>
)
})}
</ul>
)}
<div className={styles.summary}>
<span className={styles.summaryLabel}>
{t('orderTransactions.summary', {
count: transactions.length,
txLabel:
transactions.length === 1
? t('orderTransactions.one')
: t('orderTransactions.many'),
})}
</span>
<span className={styles.summaryValue}>{formatIrtPrice(totalPaid)}</span>
</div>
<div className={modalStyles.actions}>
<button type="button" className={modalStyles.cancelBtn} onClick={onClose}>
{t('orderTransactions.close')}
</button>
</div>
</div>
</div>
</div>,
document.body,
)
}