mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
6.1 KiB
TypeScript
193 lines
6.1 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { X } from 'lucide-react'
|
|
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
|
|
|
|
function transactionTypeLabel(type: TransactionType) {
|
|
switch (type) {
|
|
case 'pos':
|
|
return 'POS'
|
|
case 'cash':
|
|
return 'Cash'
|
|
case 'transfer':
|
|
return 'Transfer'
|
|
case 'e_payment_gate':
|
|
return 'E-payment gateway'
|
|
default:
|
|
return type
|
|
}
|
|
}
|
|
|
|
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) {
|
|
const d = new Date(value)
|
|
if (Number.isNaN(d.getTime())) return value
|
|
return d.toLocaleString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
function transactionDetails(transaction: OrderTransaction) {
|
|
const rows: { label: string; value: string }[] = [
|
|
{ label: 'Status', value: transaction.status },
|
|
{ label: 'Date', value: formatDateTime(transaction.createdAt) },
|
|
]
|
|
|
|
if (transaction.type === 'pos' && transaction.posType) {
|
|
rows.push({ label: 'POS type', value: transaction.posType })
|
|
}
|
|
if (transaction.type === 'transfer') {
|
|
if (transaction.transferAccount) {
|
|
rows.push({ label: 'Account number', value: transaction.transferAccount })
|
|
}
|
|
if (transaction.transferRefNumber) {
|
|
rows.push({ label: 'Ref code', value: transaction.transferRefNumber })
|
|
}
|
|
}
|
|
if (transaction.type === 'e_payment_gate' && transaction.gatewayType) {
|
|
rows.push({ label: 'Gateway', value: transaction.gatewayType })
|
|
}
|
|
if (transaction.notes) {
|
|
rows.push({ label: 'Notes', value: transaction.notes })
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
export function OrderTransactionsModal({ open, order, onClose }: OrderTransactionsModalProps) {
|
|
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
|
|
|
|
return (
|
|
<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}>
|
|
Transaction details
|
|
</h2>
|
|
<p className={modalStyles.subtitle}>{order.orderNumber}</p>
|
|
</div>
|
|
<button type="button" className={modalStyles.closeBtn} onClick={onClose} aria-label="Close">
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className={`${modalStyles.body} ${styles.body}`}>
|
|
{transactions.length === 0 ? (
|
|
<p className={styles.empty}>No payment transactions recorded for this order.</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}>{transactionTypeLabel(transaction.type)}</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.label === 'Status' ? (
|
|
<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}>
|
|
{transactions.length} {transactions.length === 1 ? 'transaction' : 'transactions'}
|
|
</span>
|
|
<span className={styles.summaryValue}>{formatIrtPrice(totalPaid)}</span>
|
|
</div>
|
|
|
|
<div className={modalStyles.actions}>
|
|
<button type="button" className={modalStyles.cancelBtn} onClick={onClose}>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|