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
+34 -7
View File
@@ -55,6 +55,8 @@ interface Props {
profile?: PatientProfile | null;
onCreated: (sessionUuid: string) => void;
onCancel: () => void;
/** وقتی داده شود فرم در حالت ویرایش است و با PATCH به همان مراجعه ارسال می‌کند. */
editSession?: import('../SessionServiceCard').SessionCardData;
}
/**
@@ -62,8 +64,9 @@ interface Props {
* تاریخ/ساعت پذیرش، بخش/سرویس/پرسنل، کالای مصرفی با شمارنده، پکیج، و بلوک بیمه‌ی
* موجودِ NewSessionPage (نمایش شرطی: سرویسِ تحت پوشش بیمه یا بیمه در پروفایل بیمار).
*/
export default function CreateStep({ recordUuid, profile, onCreated, onCancel }: Props) {
export default function CreateStep({ recordUuid, profile, onCreated, onCancel, editSession }: Props) {
const userName = useAuthStore((s) => s.userName);
const isEdit = !!editSession;
// ── state گام ایجاد ──────────────────────────────────────────────────────
const [dateISO, setDateISO] = useState(todayISO());
@@ -111,9 +114,31 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel }:
const requireVisit = (pricingData as any)?.data?.require_visit_price ?? false;
useEffect(() => {
if (freeVisit > 0 && (!visitPrice || visitPrice === '0')) setVisitPrice(String(rialToToman(freeVisit)));
if (!isEdit && freeVisit > 0 && (!visitPrice || visitPrice === '0')) setVisitPrice(String(rialToToman(freeVisit)));
}, [freeVisit]); // eslint-disable-line react-hooks/exhaustive-deps
// پیش‌پرکردن فرم در حالت ویرایش (یک‌بار).
const [prefilled, setPrefilled] = useState(false);
useEffect(() => {
if (!editSession || prefilled) return;
setVisitPrice(String(rialToToman(editSession.visit_price_rials ?? 0)));
if (editSession.session_at) {
const d = new Date(editSession.session_at * 1000);
setDateISO(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
setTime(`${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`);
}
setNotes((editSession as any).notes ?? '');
if (editSession.insurance_base_id) { setBaseId(String(editSession.insurance_base_id)); setBasePercent(String(editSession.base_insurance_discount_percent ?? 0)); }
if (editSession.insurance_supplementary_id) { setSuppId(String(editSession.insurance_supplementary_id)); setSuppPercent(String(editSession.supplementary_discount_percent ?? 0)); }
setSelectedServices((editSession.services ?? []).map((s) => ({
uuid: s.service_item_uuid ?? '', name: s.service_name || s.name || '', price: s.price_rials ?? 0, qty: s.quantity ?? 1, insured: false,
})).filter((s) => s.uuid));
setSelectedConsumables((editSession.consumables ?? []).map((c) => ({
uuid: c.inventory_item_uuid ?? '', name: c.item_name ?? '', price: c.price_rials ?? 0, qty: c.quantity ?? 1,
})).filter((c) => c.uuid));
setPrefilled(true);
}, [editSession, prefilled]);
const contracts = (contractsData as any)?.data?.data as Contract[] | undefined ?? [];
const baseOpts = contracts.filter(c => c.insurance_kind === 'basic').map(c => ({ value: String(c.insurance_id), label: c.insurance_name ?? `#${c.insurance_id}` }));
const suppOpts = contracts.filter(c => c.insurance_kind === 'supplementary').map(c => ({ value: String(c.insurance_id), label: c.insurance_name ?? `#${c.insurance_id}` }));
@@ -216,10 +241,12 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel }:
// ── ثبت ──────────────────────────────────────────────────────────────────
const createMut = useMutation({
mutationFn: (body: object) => api.post(`/api/v1/patient/${recordUuid}/session`, body),
mutationFn: (body: object) => isEdit
? api.patch(`/api/v1/session/${editSession!.uuid}`, body)
: api.post(`/api/v1/patient/${recordUuid}/session`, body),
onSuccess: (res: any) => {
toast.success('مراجعه ثبت شد');
onCreated(res?.data?.uuid as string);
toast.success(isEdit ? 'مراجعه ویرایش شد' : 'مراجعه ثبت شد');
onCreated((isEdit ? editSession!.uuid : res?.data?.uuid) as string);
},
onError: (e: Error) => toast.error(e.message),
});
@@ -236,7 +263,7 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel }:
supplementary_discount_percent: showInsurance ? supp : 0,
...(showInsurance && baseId ? { insurance_base_id: Number(baseId) } : {}),
...(showInsurance && suppId ? { insurance_supplementary_id: Number(suppId) } : {}),
payment_method: 'pending',
...(isEdit ? {} : { payment_method: 'pending' }),
...(notes ? { notes } : {}),
session_at: toSessionAt(dateISO, time),
...(packageUuid ? { inventory_package_uuid: packageUuid } : {}),
@@ -436,7 +463,7 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel }:
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginTop: 16, width: '100%', maxWidth: 340, margin: '16px auto 0' }}>
<button type="button" style={{ ...ghostBtn, width: 164 }} onClick={onCancel}>انصراف</button>
<button type="button" style={{ ...primaryBtn, width: 164 }} onClick={submit} disabled={createMut.isPending}>
{createMut.isPending ? 'در حال ذخیره...' : 'ایجاد سرویس'}
{createMut.isPending ? 'در حال ذخیره...' : (isEdit ? 'ذخیره تغییرات' : 'ایجاد سرویس')}
</button>
</div>
</div>