Add migration to enhance session_payments table with payment_method_uuid and reference fields for split-payment details
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../../test/utils';
|
||||
|
||||
vi.mock('../../lib/api', () => ({
|
||||
@@ -7,8 +7,12 @@ vi.mock('../../lib/api', () => ({
|
||||
ApiError: class extends Error {},
|
||||
}));
|
||||
|
||||
import { api } from '../../lib/api';
|
||||
import ConfirmAppointmentModal from './ConfirmAppointmentModal';
|
||||
|
||||
const get = api.get as ReturnType<typeof vi.fn>;
|
||||
const post = api.post as ReturnType<typeof vi.fn>;
|
||||
|
||||
const appointment = {
|
||||
uuid: 'a1',
|
||||
version: 1,
|
||||
@@ -23,7 +27,17 @@ function render() {
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// payment-methods (pos / bank-accounts) و بقیهٔ GETها
|
||||
get.mockResolvedValue({ success: true, data: [] });
|
||||
post.mockResolvedValue({ success: true, data: {} });
|
||||
});
|
||||
|
||||
/** همهٔ فیلدهای مبلغ (تومان) در ردیفهای پرداخت. */
|
||||
function amountInputs() {
|
||||
return screen.getAllByPlaceholderText('0') as HTMLInputElement[];
|
||||
}
|
||||
|
||||
describe('ConfirmAppointmentModal', () => {
|
||||
it('بیمار، اقلام هزینه و جمع کل را نشان میدهد', () => {
|
||||
@@ -34,33 +48,70 @@ describe('ConfirmAppointmentModal', () => {
|
||||
expect(screen.getByText('جمع کل')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('مبلغ پرداختی پیشفرض برابر باقیمانده (کل هزینه) است', () => {
|
||||
it('ردیفِ اول پیشفرض برابر کل هزینه است و وضعیت «تسویه کامل» میشود', () => {
|
||||
render();
|
||||
// ۳٬۰۰۰٬۰۰۰ ریال = ۳۰۰٬۰۰۰ تومان
|
||||
expect(screen.getByPlaceholderText('0')).toHaveValue('۳۰۰٬۰۰۰');
|
||||
expect(amountInputs()[0]).toHaveValue('۳۰۰٬۰۰۰');
|
||||
expect(screen.getByText('تسویه کامل')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('با تغییر دستی مبلغ، باقیمانده دوباره محاسبه میشود', () => {
|
||||
it('با تغییر دستی مبلغ به کمتر از کل، وضعیت «پرداخت جزئی» میشود', () => {
|
||||
render();
|
||||
fireEvent.change(screen.getByPlaceholderText('0'), { target: { value: '100000' } });
|
||||
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '100000' } });
|
||||
expect(screen.getByText('پرداخت جزئی')).toBeInTheDocument();
|
||||
// ۳۰۰٬۰۰۰ − ۱۰۰٬۰۰۰ تومان باقیمانده ⇒ ۲٬۰۰۰٬۰۰۰ ریال
|
||||
expect(screen.getByText(/باقیمانده پس از این پرداخت/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'بدون پرداخت' }));
|
||||
expect(screen.getByText('بدون پرداخت', { selector: 'span' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('دکمهٔ تأیید فقط با مبلغ بیشتر از جمع کل غیرفعال میشود', () => {
|
||||
it('دکمهٔ تأیید فقط با مجموعِ بیشتر از جمع کل غیرفعال میشود', () => {
|
||||
render();
|
||||
const submit = screen.getByRole('button', { name: 'تأیید و قطعی کردن' });
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
// مبلغ بالاتر از جمع کل (۳٬۰۰۰٬۰۰۰ ریال = ۳۰۰٬۰۰۰ تومان < کل؛ پس عدد بزرگتر میدهیم)
|
||||
fireEvent.change(screen.getByPlaceholderText('0'), { target: { value: '9000000' } });
|
||||
expect(screen.getByText('مبلغ پرداخت از جمع کل بیشتر است.')).toBeInTheDocument();
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '9000000' } });
|
||||
expect(screen.getByText('مجموع پرداختها از جمع کل بیشتر است.')).toBeInTheDocument();
|
||||
expect(submit).toBeDisabled();
|
||||
});
|
||||
|
||||
it('پرداخت جزئی مجاز است و همان یک روش را ثبت میکند', async () => {
|
||||
render();
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '100000' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/appointment/a1/confirm', {
|
||||
version: 1,
|
||||
payments: [{ method: 'cash', amount_rials: 1_000_000 }],
|
||||
}));
|
||||
});
|
||||
|
||||
it('تقسیم پرداخت بین دو روش: مجموع ردیفها بهصورت آرایه ثبت میشود', async () => {
|
||||
render();
|
||||
// ردیف اول را به ۲۰۰٬۰۰۰ تومان کم میکنیم
|
||||
fireEvent.change(amountInputs()[0], { target: { value: '200000' } });
|
||||
// افزودن روش دوم — پیشفرض با باقیماندهٔ ۱۰۰٬۰۰۰ تومان پر میشود
|
||||
fireEvent.click(screen.getByRole('button', { name: /افزودن روش/ }));
|
||||
|
||||
const inputs = amountInputs();
|
||||
expect(inputs).toHaveLength(2);
|
||||
expect(inputs[1]).toHaveValue('۱۰۰٬۰۰۰');
|
||||
|
||||
// مجموع = ۳۰۰٬۰۰۰ تومان = کل ⇒ تسویه کامل
|
||||
expect(screen.getByText('تسویه کامل')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' }));
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/appointment/a1/confirm', {
|
||||
version: 1,
|
||||
payments: [
|
||||
{ method: 'cash', amount_rials: 2_000_000 },
|
||||
{ method: 'cash', amount_rials: 1_000_000 },
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
it('حذف ردیف اضافهشده مجموع را دوباره محاسبه میکند', () => {
|
||||
render();
|
||||
fireEvent.click(screen.getByRole('button', { name: /افزودن روش/ }));
|
||||
expect(amountInputs()).toHaveLength(2);
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText('حذف روش پرداخت')[0]);
|
||||
expect(amountInputs()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { UserCircleIcon } from '@heroicons/react/24/outline';
|
||||
import { UserCircleIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { api } from '../../lib/api';
|
||||
import type { ApiResponse } from '../../lib/api';
|
||||
import type { BankAccount, Pos } from '../../hooks/usePaymentMethods';
|
||||
import { formatRial, rialToToman, tomanToRial } from '../../lib/utils';
|
||||
import Modal from '../ui/Modal';
|
||||
import PriceInput from '../ui/PriceInput';
|
||||
@@ -41,6 +42,17 @@ interface Props {
|
||||
queryKey?: unknown[];
|
||||
}
|
||||
|
||||
/** یک ردیفِ پرداخت در تسویهٔ چندروشی. */
|
||||
interface PaymentRow {
|
||||
id: number;
|
||||
method: string;
|
||||
amountToman: number;
|
||||
/** uuid کارتخوان (pos) یا حساب بانکیِ (card) ثبتشده. */
|
||||
methodUuid: string;
|
||||
/** شناسه تراکنش / شماره پیگیری. */
|
||||
reference: string;
|
||||
}
|
||||
|
||||
const rowStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
@@ -59,10 +71,11 @@ const STATE_TONE: Record<string, { fg: string; bg: string }> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* «قطعی کردن نوبت» — هزینههای نوبت را نشان میدهد، پرداخت کامل یا جزئی میگیرد و
|
||||
* نوبت را از «ثبت شده» به «قطعی شده» میبرد.
|
||||
* «قطعی کردن نوبت» — هزینههای نوبت را نشان میدهد، پرداخت را بین چند روش
|
||||
* (نقدی/کارتخوان/کارتبهکارت/کیف پول) تقسیم میکند و نوبت را «قطعی» میکند.
|
||||
*
|
||||
* سرور همین یک درخواست را اتمیک انجام میدهد: وضعیت + پرونده/مراجعه + پرداختها.
|
||||
* مبلغ پرداختی میتواند کمتر از جمع کل باشد (پرداخت جزئی)، ولی نباید بیشتر شود.
|
||||
*/
|
||||
export default function ConfirmAppointmentModal({
|
||||
open,
|
||||
@@ -72,9 +85,12 @@ export default function ConfirmAppointmentModal({
|
||||
queryKey,
|
||||
}: Props) {
|
||||
const qc = useQueryClient();
|
||||
const [method, setMethod] = useState('cash');
|
||||
const [amountToman, setAmountToman] = useState(0);
|
||||
/** تا وقتی کاربر مبلغ را دست نزده، فیلد با باقیماندهٔ نوبت پر میماند. */
|
||||
const nextId = useRef(1);
|
||||
const makeRow = (over: Partial<PaymentRow> = {}): PaymentRow => ({
|
||||
id: nextId.current++, method: 'cash', amountToman: 0, methodUuid: '', reference: '', ...over,
|
||||
});
|
||||
const [rows, setRows] = useState<PaymentRow[]>([makeRow()]);
|
||||
/** تا وقتی کاربر مبلغ را دست نزده، ردیفِ اول با کل مبلغ پر میماند. */
|
||||
const [touched, setTouched] = useState(false);
|
||||
|
||||
// وقتی صفحهی میزبان نوبت را ندارد (مثل ردیف لیست) خودمان جزئیات را میگیریم:
|
||||
@@ -85,6 +101,30 @@ export default function ConfirmAppointmentModal({
|
||||
enabled: open && !appointment,
|
||||
});
|
||||
|
||||
// روشهای پرداختِ ثبتشده — فقط وقتی مودال باز است.
|
||||
const posQuery = useQuery<ApiResponse<Pos[]>>({
|
||||
queryKey: ['payment-methods', 'pos'],
|
||||
queryFn: () => api.get('/api/v1/my/payment-methods/pos'),
|
||||
enabled: open,
|
||||
});
|
||||
const bankQuery = useQuery<ApiResponse<BankAccount[]>>({
|
||||
queryKey: ['payment-methods', 'bank-accounts'],
|
||||
queryFn: () => api.get('/api/v1/my/payment-methods/bank-accounts'),
|
||||
enabled: open,
|
||||
});
|
||||
const posOptions = useMemo(
|
||||
() => ((posQuery.data?.data ?? []) as Pos[])
|
||||
.filter(p => p.is_active)
|
||||
.map(p => ({ value: p.uuid, label: `${p.bank_name} — ${p.terminal_number}` })),
|
||||
[posQuery.data],
|
||||
);
|
||||
const bankOptions = useMemo(
|
||||
() => ((bankQuery.data?.data ?? []) as BankAccount[])
|
||||
.filter(b => b.is_active)
|
||||
.map(b => ({ value: b.uuid, label: `${b.bank_name}${b.card_number ? ` — ${b.card_number}` : ''}` })),
|
||||
[bankQuery.data],
|
||||
);
|
||||
|
||||
const appt: AppointmentLike | null = appointment
|
||||
?? ((detailQuery.data?.data as any)?.data ?? detailQuery.data?.data ?? null);
|
||||
|
||||
@@ -96,18 +136,21 @@ export default function ConfirmAppointmentModal({
|
||||
);
|
||||
const total = visitPrice + servicesTotal;
|
||||
|
||||
// جمع کل تا لحظهای که کاربر مبلغ را دستی تغییر ندهد پیشفرضِ «پرداخت کامل» است؛
|
||||
// ردیفِ اول تا لحظهای که کاربر مبلغ را دستی تغییر ندهد پیشفرضِ «پرداخت کامل» است؛
|
||||
// نوبت هنوز session ندارد، پس باقیماندهاش برابر کل هزینه است.
|
||||
useEffect(() => {
|
||||
if (!open || touched || total <= 0) return;
|
||||
setAmountToman(rialToToman(total));
|
||||
setRows(prev => prev.map((r, i) => (i === 0 ? { ...r, amountToman: rialToToman(total) } : r)));
|
||||
}, [open, touched, total]);
|
||||
|
||||
const amountRials = tomanToRial(amountToman);
|
||||
const remaining = Math.max(0, total - amountRials);
|
||||
const overpaid = amountRials > total;
|
||||
const paidRials = useMemo(
|
||||
() => rows.reduce((sum, r) => sum + tomanToRial(r.amountToman), 0),
|
||||
[rows],
|
||||
);
|
||||
const remaining = Math.max(0, total - paidRials);
|
||||
const overpaid = paidRials > total;
|
||||
|
||||
const paymentState = amountRials === 0
|
||||
const paymentState = paidRials === 0
|
||||
? 'بدون پرداخت'
|
||||
: remaining === 0
|
||||
? 'تسویه کامل'
|
||||
@@ -117,7 +160,14 @@ export default function ConfirmAppointmentModal({
|
||||
mutationFn: () =>
|
||||
api.post<ApiResponse<unknown>>(`/api/v1/appointment/${appointmentUuid}/confirm`, {
|
||||
version: appt?.version,
|
||||
payments: amountRials > 0 ? [{ method, amount_rials: amountRials }] : [],
|
||||
payments: rows
|
||||
.filter(r => tomanToRial(r.amountToman) > 0)
|
||||
.map(r => ({
|
||||
method: r.method,
|
||||
amount_rials: tomanToRial(r.amountToman),
|
||||
...(r.methodUuid ? { payment_method_uuid: r.methodUuid } : {}),
|
||||
...(r.reference.trim() ? { reference: r.reference.trim() } : {}),
|
||||
})),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('نوبت قطعی شد');
|
||||
@@ -131,15 +181,30 @@ export default function ConfirmAppointmentModal({
|
||||
});
|
||||
|
||||
function reset() {
|
||||
setAmountToman(0);
|
||||
setMethod('cash');
|
||||
nextId.current = 1;
|
||||
setRows([makeRow()]);
|
||||
setTouched(false);
|
||||
}
|
||||
|
||||
/** تغییر دستی مبلغ: از این به بعد پیشفرضِ خودکار دیگر روی فیلد ننشیند. */
|
||||
function changeAmount(next: number) {
|
||||
function patchRow(id: number, patch: Partial<PaymentRow>) {
|
||||
setTouched(true);
|
||||
setAmountToman(next);
|
||||
setRows(prev => prev.map(r => (r.id === id ? { ...r, ...patch } : r)));
|
||||
}
|
||||
|
||||
/** روش که عوض شد، جزئیاتِ مخصوصِ روشِ قبلی بیمعنا میشود. */
|
||||
function changeMethod(id: number, method: string) {
|
||||
patchRow(id, { method, methodUuid: '', reference: '' });
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
setTouched(true);
|
||||
// ردیفِ جدید پیشفرض با باقیمانده پر میشود تا تسویه سریعتر باشد.
|
||||
setRows(prev => [...prev, makeRow({ amountToman: rialToToman(remaining) })]);
|
||||
}
|
||||
|
||||
function removeRow(id: number) {
|
||||
setTouched(true);
|
||||
setRows(prev => (prev.length > 1 ? prev.filter(r => r.id !== id) : prev));
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
@@ -217,51 +282,122 @@ export default function ConfirmAppointmentModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* پرداخت */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: 14, marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<div className="field-block">
|
||||
<label>روش پرداخت</label>
|
||||
<SearchableSelect
|
||||
value={method}
|
||||
onChange={(v) => setMethod(String(v ?? 'cash'))}
|
||||
options={METHOD_OPTIONS}
|
||||
placeholder="روش پرداخت"
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field-block">
|
||||
<label>مبلغ پرداختی (تومان)</label>
|
||||
<div className="field" style={overpaid ? { borderColor: 'var(--danger)' } : undefined}>
|
||||
<PriceInput value={amountToman} onChange={changeAmount} suffix="تومان" max={rialToToman(total)} />
|
||||
</div>
|
||||
<span className="field-hint">باقیمانده پس از این پرداخت: {formatRial(remaining)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn soft sm"
|
||||
onClick={() => changeAmount(rialToToman(total))}
|
||||
>
|
||||
پرداخت کامل
|
||||
</button>
|
||||
{amountToman > 0 && (
|
||||
<button type="button" className="btn ghost sm" onClick={() => changeAmount(0)}>
|
||||
بدون پرداخت
|
||||
{/* پرداختها — تقسیم بین چند روش */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<label style={{ fontSize: 13.5, fontWeight: 700, color: 'var(--text)' }}>روشهای پرداخت</label>
|
||||
<button type="button" className="btn ghost sm" onClick={addRow}>
|
||||
<PlusIcon style={{ width: 15, height: 15 }} />
|
||||
افزودن روش
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{rows.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
style={{
|
||||
border: '1px solid var(--border)', borderRadius: 'var(--r-sm)',
|
||||
padding: 12, display: 'flex', flexDirection: 'column', gap: 10,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label>روش پرداخت</label>
|
||||
<SearchableSelect
|
||||
value={r.method}
|
||||
onChange={(v) => changeMethod(r.id, String(v ?? 'cash'))}
|
||||
options={METHOD_OPTIONS}
|
||||
placeholder="روش پرداخت"
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label>مبلغ (تومان)</label>
|
||||
<div className="field">
|
||||
<PriceInput
|
||||
value={r.amountToman}
|
||||
onChange={(v) => patchRow(r.id, { amountToman: v })}
|
||||
suffix="تومان"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{rows.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost sm"
|
||||
aria-label="حذف روش پرداخت"
|
||||
onClick={() => removeRow(r.id)}
|
||||
style={{ marginBottom: 2, color: 'var(--danger)' }}
|
||||
>
|
||||
<TrashIcon style={{ width: 16, height: 16 }} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* جزئیاتِ کارتخوان: انتخاب دستگاهِ ثبتشده + شناسه تراکنش */}
|
||||
{r.method === 'pos' && (
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label>کارتخوان</label>
|
||||
<SearchableSelect
|
||||
value={r.methodUuid}
|
||||
onChange={(v) => patchRow(r.id, { methodUuid: String(v ?? '') })}
|
||||
options={posOptions}
|
||||
placeholder={posOptions.length ? 'انتخاب کارتخوان' : 'کارتخوانی ثبت نشده'}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label>شناسه تراکنش (اختیاری)</label>
|
||||
<div className="field">
|
||||
<input
|
||||
type="text"
|
||||
value={r.reference}
|
||||
onChange={(e) => patchRow(r.id, { reference: e.target.value })}
|
||||
placeholder="شماره پیگیری"
|
||||
style={{ direction: 'ltr' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* جزئیاتِ کارتبهکارت: انتخاب حساب بانکیِ ثبتشده + شناسه تراکنش */}
|
||||
{r.method === 'card' && (
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label>حساب بانکی (اختیاری)</label>
|
||||
<SearchableSelect
|
||||
value={r.methodUuid}
|
||||
onChange={(v) => patchRow(r.id, { methodUuid: String(v ?? '') })}
|
||||
options={bankOptions}
|
||||
placeholder={bankOptions.length ? 'انتخاب حساب' : 'حسابی ثبت نشده'}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="field-block" style={{ flex: 1 }}>
|
||||
<label>شناسه تراکنش (اختیاری)</label>
|
||||
<div className="field">
|
||||
<input
|
||||
type="text"
|
||||
value={r.reference}
|
||||
onChange={(e) => patchRow(r.id, { reference: e.target.value })}
|
||||
placeholder="شماره پیگیری"
|
||||
style={{ direction: 'ltr' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{overpaid && (
|
||||
<p className="field-err" style={{ marginBottom: 14 }}>
|
||||
مبلغ پرداخت از جمع کل بیشتر است.
|
||||
مجموع پرداختها از جمع کل بیشتر است.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -274,7 +410,7 @@ export default function ConfirmAppointmentModal({
|
||||
>
|
||||
<div style={rowStyle}>
|
||||
<span>پرداختشده</span>
|
||||
<strong style={{ color: 'var(--text)' }}>{formatRial(Math.min(amountRials, total))}</strong>
|
||||
<strong style={{ color: 'var(--text)' }}>{formatRial(Math.min(paidRials, total))}</strong>
|
||||
</div>
|
||||
<div style={{ ...rowStyle, borderTop: '1px solid var(--border)' }}>
|
||||
<span>باقیمانده</span>
|
||||
|
||||
Reference in New Issue
Block a user