feat(admin): session edit form, payment edit/delete UI, and audit history modal
- SessionServiceCard menu gains «ویرایش» and «تاریخچه تغییرات» items.
- SessionAuditModal shows the session's change history (field, op badge,
old->new, actor, time) from /session/{uuid}/audit-log.
- PaymentStep payment rows get edit (modal) + delete (confirm) controls for
non-wallet payments, calling the new PATCH/DELETE payment endpoints.
- CreateStep accepts an editSession prop (prefill + PATCH); EditSessionPage
reuses it at /patients/:recordUuid/session/:sessionUuid/edit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,15 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../../lib/api';
|
||||
import type { ApiResponse } from '../../lib/api';
|
||||
import { formatRial, formatDateTime, tomanToRial } from '../../lib/utils';
|
||||
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { formatRial, formatDateTime, tomanToRial, rialToToman } from '../../lib/utils';
|
||||
import type { DiscountSuggestion } from '../../types';
|
||||
import type { SessionCardData } from '../SessionServiceCard';
|
||||
import SearchableSelect from '../ui/SearchableSelect';
|
||||
import PersianDateInput from '../ui/PersianDateInput';
|
||||
import PriceInput from '../ui/PriceInput';
|
||||
import Modal from '../ui/Modal';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import { Step2PaymentCard, FilesServiceBalanceWallet, TrashRed } from '../icons/FilesServiceIcons';
|
||||
|
||||
/** روشهای پرداخت — همان چهار گزینهی آکاردئون tauri Step2Payment. */
|
||||
@@ -72,6 +75,21 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
// ویرایش/حذف پرداخت
|
||||
const [editPay, setEditPay] = useState<{ uuid: string; method: string; amountToman: number } | null>(null);
|
||||
const [delPayUuid, setDelPayUuid] = useState<string | null>(null);
|
||||
|
||||
const editPayMut = useMutation({
|
||||
mutationFn: ({ uuid, body }: { uuid: string; body: object }) => api.patch(`/api/v1/session/${sessionUuid}/payments/${uuid}`, body),
|
||||
onSuccess: () => { invalidate(); setEditPay(null); toast.success('پرداخت ویرایش شد'); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
const delPayMut = useMutation({
|
||||
mutationFn: (uuid: string) => api.delete(`/api/v1/session/${sessionUuid}/payments/${uuid}`),
|
||||
onSuccess: () => { invalidate(); setDelPayUuid(null); toast.success('پرداخت حذف شد'); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const suggestionsQ = useQuery({
|
||||
queryKey: ['discount-suggestions', sessionUuid],
|
||||
queryFn: () => api.get<ApiResponse<DiscountSuggestion[]>>(`/api/v1/session/${sessionUuid}/discount-suggestions`),
|
||||
@@ -253,12 +271,24 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{payments.map((p) => (
|
||||
<div key={p.uuid} className="dark:border-[#404040]" style={{ padding: '6px 0', borderBottom: '1px solid #e0e0e0' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 32 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span className="dark:bg-[#6A6AD9]" style={{ width: 8, height: 8, borderRadius: '50%', background: '#636bd4' }} />
|
||||
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, color: '#111827' }}>{METHOD_LABELS[p.method] ?? p.method}</span>
|
||||
</span>
|
||||
<span className="dark:text-[#D7D8ED]" style={{ flex: 1, textAlign: 'left', fontSize: 14, color: '#111827' }}>مبلغ : {formatRial(p.amount_rials)}</span>
|
||||
{p.method !== 'wallet' && (
|
||||
<span style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
||||
<button type="button" aria-label="ویرایش پرداخت" onClick={() => setEditPay({ uuid: p.uuid, method: p.method, amountToman: rialToToman(p.amount_rials) })}
|
||||
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#5559ce', display: 'flex' }}>
|
||||
<PencilIcon style={{ width: 16 }} />
|
||||
</button>
|
||||
<button type="button" aria-label="حذف پرداخت" onClick={() => setDelPayUuid(p.uuid)}
|
||||
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#EF4444', display: 'flex' }}>
|
||||
<TrashIcon style={{ width: 16 }} />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="dark:text-[#A1A1A1]" style={{ display: 'flex', gap: 12, marginTop: 4, fontSize: 12, color: '#9CA3AF', flexWrap: 'wrap' }}>
|
||||
{(p.paid_at ?? p.created_at) && <span>{formatDateTime(p.paid_at ?? p.created_at!)}</span>}
|
||||
@@ -281,6 +311,44 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
|
||||
<button type="button" style={{ ...primaryBtn, flex: 1 }} onClick={onContinue}>ثبت و ادامه</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editPay && (
|
||||
<Modal open title="ویرایش پرداخت" size="sm" onClose={() => setEditPay(null)}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div>
|
||||
<label style={fieldLabel}>روش پرداخت</label>
|
||||
<SearchableSelect
|
||||
options={METHODS.filter((m) => m.key !== 'wallet').map((m) => ({ value: m.key, label: m.label }))}
|
||||
value={editPay.method}
|
||||
onChange={(v) => setEditPay((s) => s && { ...s, method: v ? String(v) : s.method })}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={fieldLabel}>مبلغ (تومان)</label>
|
||||
<PriceInput latin className="cp-input" style={{ width: '100%' }} value={editPay.amountToman} onChange={(v) => setEditPay((s) => s && { ...s, amountToman: v })} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button className="btn ghost sm" onClick={() => setEditPay(null)}>انصراف</button>
|
||||
<button className="btn primary sm" disabled={editPayMut.isPending || editPay.amountToman <= 0}
|
||||
onClick={() => editPay && editPayMut.mutate({ uuid: editPay.uuid, body: { method: editPay.method, amount_rials: tomanToRial(editPay.amountToman) } })}>
|
||||
{editPayMut.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!delPayUuid}
|
||||
title="حذف پرداخت"
|
||||
message="آیا از حذف این پرداخت مطمئن هستید؟ این عملیات در تاریخچه ثبت میشود."
|
||||
confirmLabel="حذف"
|
||||
danger
|
||||
loading={delPayMut.isPending}
|
||||
onConfirm={() => delPayUuid && delPayMut.mutate(delPayUuid)}
|
||||
onCancel={() => setDelPayUuid(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user