feat: port payment management tab from tauri to admin dashboard
Add per-clinic payment methods (bank accounts + POS/card-reader devices)
under the "مدیریت پرداخت" settings tab at /admin/my-financial, ported from
clinic-pro-tauri's mock-only PaymentManagement tab into a real persisted
feature. These records are referenceable (by uuid) from patient invoices to
record which method a service payment was made with.
Backend (new src/PaymentMethod domain):
- BankAccount + Pos entities, repositories, PaymentMethodService (validation,
ownership scoping, create/update/toggle logic).
- Thin PaymentMethodController exposing /api/v1/my/payment-methods/{bank-accounts,pos}
(GET/POST/PUT + PATCH .../status), guarded to clinic/doctor/secretary/admin.
- Migration for bank_accounts + pos_devices tables.
- Functional tests (success + validation/404/403 + empty boundaries).
- docs/api/payment-method.md.
Frontend:
- Replace MyFinancialPage content with the payment-management UI (two tabs,
tables, add/edit modals, status toggle) using the admin design system.
- usePaymentMethods hook (TanStack Query) + presentational components.
- Update page test to cover tabs, data, empty state and the add modal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import Modal from '../ui/Modal';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import { BANK_OPTIONS } from './banks';
|
||||
import {
|
||||
useCreateBankAccount,
|
||||
useUpdateBankAccount,
|
||||
type BankAccount,
|
||||
} from '../../hooks/usePaymentMethods';
|
||||
|
||||
/**
|
||||
* فرم افزودن/ویرایش حساب بانکی — پورت مبدأ ModalAddBankAccount.jsx.
|
||||
* اگر `account` داده شود حالت ویرایش، وگرنه افزودن.
|
||||
*/
|
||||
export default function BankAccountFormModal({
|
||||
open,
|
||||
account,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
account: BankAccount | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const isEdit = !!account;
|
||||
const [bankName, setBankName] = useState('');
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
const [accountNumber, setAccountNumber] = useState('');
|
||||
const [shabaNumber, setShabaNumber] = useState('');
|
||||
|
||||
const createMut = useCreateBankAccount();
|
||||
const updateMut = useUpdateBankAccount();
|
||||
const pending = createMut.isPending || updateMut.isPending;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setBankName(account?.bank_name ?? '');
|
||||
setCardNumber(account?.card_number ?? '');
|
||||
setAccountNumber(account?.account_number ?? '');
|
||||
setShabaNumber(account?.shaba_number ?? '');
|
||||
}, [open, account]);
|
||||
|
||||
const submit = () => {
|
||||
if (!bankName.trim()) { toast.error('نام بانک الزامی است'); return; }
|
||||
if (!accountNumber.trim()) { toast.error('شماره حساب الزامی است'); return; }
|
||||
|
||||
const body = {
|
||||
bank_name: bankName.trim(),
|
||||
account_number: accountNumber.trim(),
|
||||
card_number: cardNumber.trim(),
|
||||
shaba_number: shabaNumber.trim(),
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
toast.success(isEdit ? 'حساب بانکی ویرایش شد' : 'حساب بانکی اضافه شد');
|
||||
onClose();
|
||||
};
|
||||
const onError = (e: unknown) => toast.error(e instanceof Error ? e.message : 'خطا در ذخیره');
|
||||
|
||||
if (isEdit && account) {
|
||||
updateMut.mutate({ uuid: account.uuid, body }, { onSuccess, onError });
|
||||
} else {
|
||||
createMut.mutate(body, { onSuccess, onError });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={isEdit ? 'ویرایش حساب بانکی' : 'افزودن حساب بانکی'}
|
||||
size="sm"
|
||||
footer={
|
||||
<button className="btn primary" style={{ width: '100%' }} disabled={pending} onClick={submit}>
|
||||
{pending ? '...' : isEdit ? 'ذخیره تغییرات' : 'اضافه کردن حساب بانکی'}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<label className="field-label">نام بانک</label>
|
||||
<SearchableSelect
|
||||
options={BANK_OPTIONS}
|
||||
value={bankName || null}
|
||||
onChange={(v) => setBankName(v != null ? String(v) : '')}
|
||||
placeholder="انتخاب بانک"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="field-label">شماره کارت</label>
|
||||
<input className="input" value={cardNumber} onChange={(e) => setCardNumber(e.target.value)} placeholder="شماره کارت" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="field-label">شماره حساب</label>
|
||||
<input className="input" value={accountNumber} onChange={(e) => setAccountNumber(e.target.value)} placeholder="شماره حساب" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="field-label">شبا</label>
|
||||
<input className="input" value={shabaNumber} onChange={(e) => setShabaNumber(e.target.value)} placeholder="شبا" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user