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:
hamed
2026-07-17 15:23:02 +03:30
co-authored by Claude Fable 5
parent 48579dc06f
commit f2ea2ff262
8 changed files with 287 additions and 12 deletions
@@ -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)}
/>
</>
);
}