diff --git a/assets/admin/components/WalletTransactionModal.test.tsx b/assets/admin/components/WalletTransactionModal.test.tsx new file mode 100644 index 00000000..4d4868e2 --- /dev/null +++ b/assets/admin/components/WalletTransactionModal.test.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect, vi } from 'vitest'; +import { screen, fireEvent } from '@testing-library/react'; +import { renderWithProviders } from '../test/utils'; +import WalletTransactionModal from './WalletTransactionModal'; + +// موجودی نمونه: ۳۰۰٬۰۰۰ ریال = ۳۰٬۰۰۰ تومان +const BALANCE_RIALS = 300_000; + +function open(onSubmit = vi.fn()) { + renderWithProviders( + , + ); + return onSubmit; +} + +describe('WalletTransactionModal (شارژ/برداشت کیف پول)', () => { + it('renders both mode tabs, the balance banner and the quick amounts', () => { + open(); + expect(screen.getByRole('button', { name: 'شارژ کیف پول' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'برداشت از کیف پول' })).toBeInTheDocument(); + expect(screen.getByText('موجودی کیف پول')).toBeInTheDocument(); + // چهار چیپ مبلغ سریع، هر کدام «... تومان» + expect(screen.getAllByRole('button', { name: /تومان/ }).length).toBe(4); + }); + + it('submits a charge with the amount converted from toman to rials', () => { + const onSubmit = open(); + fireEvent.change(screen.getByPlaceholderText('مبلغ دلخواه (تومان)'), { target: { value: '100000' } }); + fireEvent.click(screen.getByRole('button', { name: 'ثبت تراکنش' })); + expect(onSubmit).toHaveBeenCalledWith({ mode: 'charge', amount_rials: 1_000_000 }); + }); + + it('submits a withdraw (debit) when the برداشت tab is active and amount is within balance', () => { + const onSubmit = open(); + fireEvent.click(screen.getByRole('button', { name: 'برداشت از کیف پول' })); + fireEvent.change(screen.getByPlaceholderText('مبلغ دلخواه (تومان)'), { target: { value: '20000' } }); + fireEvent.click(screen.getByRole('button', { name: 'ثبت تراکنش' })); + expect(onSubmit).toHaveBeenCalledWith({ mode: 'withdraw', amount_rials: 200_000 }); + }); + + it('blocks a withdraw above the balance and shows the insufficient-funds hint', () => { + const onSubmit = open(); + fireEvent.click(screen.getByRole('button', { name: 'برداشت از کیف پول' })); + // ۴۰٬۰۰۰ تومان = ۴۰۰٬۰۰۰ ریال > موجودی ۳۰۰٬۰۰۰ ریال + fireEvent.change(screen.getByPlaceholderText('مبلغ دلخواه (تومان)'), { target: { value: '40000' } }); + expect(screen.getByText('موجودی کیف پول کافی نیست')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'ثبت تراکنش' })).toBeDisabled(); + fireEvent.click(screen.getByRole('button', { name: 'ثبت تراکنش' })); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('keeps submit disabled when no amount is entered', () => { + open(); + expect(screen.getByRole('button', { name: 'ثبت تراکنش' })).toBeDisabled(); + }); +}); diff --git a/assets/admin/components/WalletTransactionModal.tsx b/assets/admin/components/WalletTransactionModal.tsx new file mode 100644 index 00000000..9795cec0 --- /dev/null +++ b/assets/admin/components/WalletTransactionModal.tsx @@ -0,0 +1,215 @@ +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { XMarkIcon, WalletIcon } from '@heroicons/react/24/outline'; +import SearchableSelect from './ui/SearchableSelect'; +import PersianDateInput from './ui/PersianDateInput'; +import { formatRial, formatNumber, tomanToRial } from '../lib/utils'; + +export type WalletMode = 'charge' | 'withdraw'; + +interface Props { + open: boolean; + balanceRials: number; + submitting?: boolean; + onClose: () => void; + onSubmit: (payload: { mode: WalletMode; amount_rials: number; description?: string }) => void; +} + +// مبالغ پیشنهادی سریع، به تومان (معادل tauri quickAmounts). +const QUICK_TOMANS = [100_000, 200_000, 300_000, 400_000]; + +// روش پرداخت / حساب مقصد صرفاً UI هستند (در tauri هم mock بودند و بک‌اندی ندارند). +const PAYMENT_METHOD_OPTS = [ + { value: 'card', label: 'کارت به کارت' }, + { value: 'cash', label: 'نقدی' }, + { value: 'pos', label: 'دستگاه پوز' }, + { value: 'gateway', label: 'درگاه اینترنتی' }, +]; +const ACCOUNT_OPTS = [{ value: 'main', label: 'حساب اصلی' }]; + +/** + * مودال شارژ/برداشت کیف پول بیمار — پورت‌شده از tauri AddTransactionModal. + * یک مودال با toggle «شارژ کیف پول / برداشت از کیف پول»، مبالغ سریع، و فیلدهای فرم. + * مبلغ به تومان وارد و هنگام ثبت به ریال (واحد API) تبدیل می‌شود. + * روش پرداخت/حساب/تاریخ/ساعت فقط UI هستند (بدون ذخیره)، مطابق مبدأ. + */ +export default function WalletTransactionModal({ open, balanceRials, submitting, onClose, onSubmit }: Props) { + const [mode, setMode] = useState('charge'); + const [amountToman, setAmountToman] = useState(0); + const [method, setMethod] = useState(''); + const [account, setAccount] = useState(''); + const [date, setDate] = useState(''); + const [time, setTime] = useState(''); + const [description, setDescription] = useState(''); + + // با هر بار باز شدن، فرم ریست شود. + useEffect(() => { + if (open) { + setMode('charge'); setAmountToman(0); setMethod(''); setAccount(''); + setDate(''); setTime(''); setDescription(''); + } + }, [open]); + + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [open, onClose]); + + if (!open) return null; + + const amountRials = tomanToRial(amountToman); + const isWithdraw = mode === 'withdraw'; + const overBalance = isWithdraw && amountRials > balanceRials; + const canSubmit = amountToman > 0 && !overBalance && !submitting; + const amountLabel = isWithdraw ? 'مبلغ برداشت:' : 'مبلغ شارژ:'; + + const submit = () => { + if (!canSubmit) return; + onSubmit({ + mode, + amount_rials: amountRials, + ...(description.trim() ? { description: description.trim() } : {}), + }); + }; + + const tab = (key: WalletMode, label: string) => { + const on = mode === key; + return ( + + ); + }; + + return createPortal( +
+
e.stopPropagation()}> +
+

{isWithdraw ? 'برداشت از کیف پول' : 'شارژ کیف پول'}

+ +
+ +
+ {/* بنر موجودی (معادل هدر گرادیانی مبدأ) */} +
+
+ + موجودی کیف پول +
+
{formatRial(balanceRials)}
+
+ + {/* toggle شارژ/برداشت */} +
+
+ {tab('charge', 'شارژ کیف پول')} + {tab('withdraw', 'برداشت از کیف پول')} +
+ + {/* مبلغ + مبالغ سریع */} +
+ +
+ {QUICK_TOMANS.map((t) => ( + + ))} +
+
+ 0 ? formatNumber(amountToman) : ''} + onChange={(e) => { + const raw = e.target.value.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x06f0)).replace(/[^\d]/g, ''); + setAmountToman(raw ? parseInt(raw, 10) : 0); + }} + placeholder="مبلغ دلخواه (تومان)" + style={{ textAlign: 'center', direction: 'ltr' }} + /> +
+ {overBalance && ( +
موجودی کیف پول کافی نیست
+ )} +
+ + {/* روش پرداخت + حساب مقصد (UI-only) */} +
+ +
+ setMethod(String(v ?? ''))} placeholder="کارت به کارت" height={46} /> +
+ setAccount(String(v ?? ''))} placeholder="حساب مقصد" height={46} /> +
+ + {/* تاریخ + ساعت (UI-only) */} +
+
+ +
+
+
+ +
+ setTime(e.target.value)} dir="ltr" /> +
+
+
+ + {/* توضیحات */} +
+ +
+