import React, { useMemo, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import Modal from '../components/ui/Modal'; import { formatDate, formatRial } from '../lib/utils'; import { usePermissions } from '../hooks/usePermissions'; import { useCreditLedger } from '../hooks/usePackages'; import type { CreditLedgerRow } from '../types'; const KIND_LABELS: Record = { purchase: 'خرید', consume: 'مصرف در نوبت', refund: 'بازگشت با لغو', adjustment: 'اصلاح دستی', expiry: 'انقضا', }; /** * دفتر اعتبار یک پکیج خریداری‌شده. * * ستون «مانده» در هر ردیف نشان می‌دهد عدد نهایی از کجا آمده — همان چیزی که یک * شمارندهٔ ذخیره‌شده هرگز نمی‌تواند نشان دهد. */ export default function PatientPackageLedgerPage() { const { patientPackageUuid } = useParams<{ patientPackageUuid: string }>(); const { ledger, loading, adjust, expire } = useCreditLedger(patientPackageUuid); const { can } = usePermissions(); const canCorrect = can('appointment_settings', 'update'); const [adjusting, setAdjusting] = useState(false); const [delta, setDelta] = useState(1); const [reason, setReason] = useState(''); /** * مانده تجمعی از روی همان ردیف‌هایی که نمایش داده می‌شوند. * * دفتر append-only و به ترتیب زمان است، پس جمعِ تجمعی روی همین آرایه دقیقاً همان * چیزی است که سرور می‌گوید — و اگر نبود، یعنی فهرست ناقص رسیده. */ const runningBalance = useMemo(() => { const out: Record = {}; let total = 0; for (const row of [...(ledger?.rows ?? [])].sort((a, b) => a.created_at - b.created_at)) { total += row.delta; out[row.uuid] = total; } return out; }, [ledger?.rows]); const columns: Column[] = [ { key: 'created_at', header: 'تاریخ', render: (r) => {formatDate(r.created_at)}, }, { key: 'kind', header: 'نوع', render: (r) => {KIND_LABELS[r.kind]}, }, { key: 'delta', header: 'تغییر', render: (r) => ( 0 ? 'var(--success)' : 'var(--danger)' }}> {r.delta > 0 ? `+${r.delta}` : r.delta} ), }, { key: 'running_balance', header: 'مانده', // ⚠️ در UI جمع می‌شود، نه از سرور خوانده. // // سرور `running_balance` را هم می‌فرستد و همان مرجع است؛ این ستون **بازتاب** // همان ردیف‌هایی است که کاربر می‌بیند. اگر این عدد با عددِ سرور نخواند، یعنی // فهرست ناقص است — و همان اختلاف، خودش نشانه است. render: (r) => {runningBalance[r.uuid] ?? r.running_balance}, }, { key: 'reason', header: 'دلیل', render: (r) => ( {r.reason ?? r.service_name ?? '—'} ), }, { // «چه کسی» و «کدام نوبت» از قبل در پاسخ بودند و نمایش داده نمی‌شدند. دفترِ // اصلاح‌پذیر بدون نامِ اصلاح‌کننده، نصف حسابرسی است. key: 'created_by', header: 'ثبت‌کننده', render: (r) => ( {r.created_by ?? 'سیستم'} ), }, { key: 'appointment_uuid', header: 'نوبت', render: (r) => r.appointment_uuid ? ( مشاهدهٔ نوبت ) : ( ), }, ]; return (
{ledger && (
مانده: {ledger.package.balance} از {ledger.package.session_count} جلسه پرداختی: {formatRial(ledger.package.price_paid_rials)} خرید: {formatDate(ledger.package.purchased_at)} {ledger.package.valid_to !== null && ` · اعتبار تا ${formatDate(ledger.package.valid_to)}`} {ledger.package.expired && منقضی} {canCorrect && (
)}
)}
setAdjusting(false)} footer={ <> } >
setDelta(Number(e.target.value))} />
setReason(e.target.value)} placeholder="مثلاً: جبران جلسهٔ لغوشده توسط کلینیک" /> دلیل در دفتر ثبت می‌شود و بعداً قابل ویرایش نیست.
); }