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:
@@ -0,0 +1,77 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user