Files
clinicpro/assets/admin/components/session/SessionAuditModal.tsx
T
hamedandClaude Fable 5 f2ea2ff262 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>
2026-07-17 15:23:02 +03:30

78 lines
3.9 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { api } from '../../lib/api';
import type { ApiResponse } from '../../lib/api';
import type { SessionAuditEntry } from '../../types';
import { formatDateTime, formatRial } from '../../lib/utils';
import Modal from '../ui/Modal';
const FIELD_LABELS: Record<string, string> = {
visit_price_rials: 'قیمت ویزیت',
services: 'سرویس‌ها',
consumables: 'کالاهای مصرفی',
services_total_rials: 'مجموع سرویس‌ها',
final_price_rials: 'مبلغ نهایی',
payment: 'پرداخت',
discount: 'تخفیف',
};
const OP_LABELS: Record<string, { label: string; color: string }> = {
create: { label: 'ایجاد', color: '#3c9a4f' },
update: { label: 'ویرایش', color: '#5559ce' },
delete: { label: 'حذف', color: '#EF4444' },
};
// مقادیر پولی (ریال) را با فرمت تومان نشان بده؛ بقیه را خام.
const RIAL_FIELDS = new Set(['visit_price_rials', 'services_total_rials', 'final_price_rials', 'payment', 'discount']);
const fmtValue = (field: string, v: string | null): string => {
if (v === null || v === '') return '—';
if (RIAL_FIELDS.has(field) && /^\d+$/.test(v)) return formatRial(Number(v));
return v;
};
export default function SessionAuditModal({ sessionUuid, onClose }: { sessionUuid: string | null; onClose: () => void }) {
const { data, isLoading } = useQuery({
queryKey: ['session-audit-log', sessionUuid],
queryFn: () => api.get<ApiResponse<SessionAuditEntry[]>>(`/api/v1/session/${sessionUuid}/audit-log`),
enabled: !!sessionUuid,
});
const rows: SessionAuditEntry[] = (data?.data as any) ?? [];
if (!sessionUuid) return null;
return (
<Modal open title="تاریخچه تغییرات" size="lg" onClose={onClose}>
{isLoading ? (
<div style={{ padding: 16, color: 'var(--text-3)', fontSize: 13 }}>در حال بارگذاری...</div>
) : rows.length === 0 ? (
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)', fontSize: 14 }}>تغییری ثبت نشده است.</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{rows.map((r, i) => {
const op = OP_LABELS[r.operation] ?? { label: r.operation, color: 'var(--text-3)' };
return (
<div key={i} className="card" style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="badge" style={{ fontSize: 11, color: '#fff', background: op.color, borderRadius: 4, padding: '2px 8px' }}>{op.label}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)' }}>{FIELD_LABELS[r.field] ?? r.field}</span>
</span>
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>{formatDateTime(r.created_at)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, flexWrap: 'wrap' }}>
<span style={{ color: 'var(--text-3)' }}>{fmtValue(r.field, r.old_value)}</span>
<span style={{ color: 'var(--text-3)' }}></span>
<span style={{ color: 'var(--text)', fontWeight: 600 }}>{fmtValue(r.field, r.new_value)}</span>
</div>
<div style={{ display: 'flex', gap: 12, fontSize: 12, color: 'var(--text-3)' }}>
{r.actor_name && <span>توسط: {r.actor_name}</span>}
{r.note && <span>{r.note}</span>}
</div>
</div>
);
})}
</div>
)}
</Modal>
);
}