Files
clinicpro/.claude/prompt/session-payment-currency-and-service-archive.md

12 KiB

رفع باگ واحد پول پرداخت + اطلاعات پرداخت‌ها + منوی سرویس + آرشیو مراجعات

پروژه

clinicpro (backend Symfony + پنل ادمین React). تک-ریپو.

تست: پنل ادمین با 09390039833 / 09390039833. اجرا داخل ddev. قرارداد واحد پول پروژه (utils.ts): واحد ذخیره/API = ریال، واحد نمایش/ورودی UI = تومان. تبدیل با tomanToRial (×۱۰) و rialToToman (÷۱۰). نمایش با formatRial(rial) که خودش ÷۱۰ می‌کند و « تومان» می‌چسباند.

زمینه

صفحه پرداخت مراجعه (/admin/patients/{uuid}/session/{sessionUuid}/pay) و صفحه خدمات بیمار (/admin/patients/{uuid}?tab=services) چند مشکل/کمبود دارند: باگ واحد پول در ثبت پرداخت (تومان به‌عنوان ریال ذخیره می‌شود → یک صفر کم)، نمایش ناقص پرداخت‌های ثبت‌شده، نبود منوی عملیات روی هر مراجعه، و نبودِ قابلیت آرشیو مراجعات اشتباه.

فایل‌های مرتبط

فایل نقش
assets/admin/components/session/PaymentStep.tsx فرم پرداخت — باگ واحد پول (L83 discount fixed، L94 payment) + ردیف پرداخت‌ها (L252-260)
assets/admin/components/session/DetailsStep.tsx خلاصه مراجعه — ردیف پرداخت‌ها (L103-111)
assets/admin/lib/utils.ts tomanToRial/rialToToman (L4-11)، formatRial (L8)، formatDateTime (L38-46)
src/Patient/Entity/SessionPayment.php toArray() (L74-84) از قبل paid_at + created_by_name دارد — backend درست است
assets/admin/components/SessionServiceCard.tsx کارت مراجعه — آیکون «...» تزئینی (L81)، دکمه footer «مشاهده فاکتور»/«تکمیل پرداخت» (L99-119)، type SessionPaymentEntry (L5-11)
assets/admin/pages/PatientDetailPage.tsx تب services (L213-235)، fetch لیست (L140-150)، viewInvoice (L85-93)، InvoiceSummaryModal (L254)
src/Patient/Entity/PatientSession.php Entity مراجعه — ستون archived ندارد (باید افزوده شود)؛ toArray() (L221-265)
src/Patient/Repository/PatientSessionRepository.php findByRecord (L22-32) + countByRecord (L34-42) — بدون فیلتر archived
src/Patient/Controller/PatientController.php sessions GET (L912-934)، updateSession PATCH (L1036)، sessionWithBilling (L973-990)
docs/api/patient.md به‌روزرسانی مستندات (Standing Rule)

تسک ۱ — رفع باگ واحد پول در ثبت پرداخت و تخفیف ثابت

وضعیت فعلی (باگ — frontend خالص)

PaymentStep.tsx مبلغ تومانِ ورودی را بدون tomanToRial تحت کلید amount_rials می‌فرستد؛ backend همه‌جا ریال فرض می‌کند (getRemainingRials, wallet withdraw) و درست است. پس تومان خام به‌عنوان ریال ذخیره می‌شود → یک صفر کم (÷۱۰ در نمایش).

// L91-94 — payment
const submitPayment = (method: string) => {
  if (amount <= 0) return;
  payMut.mutate({ method, amount_rials: amount, paid_at: isoToUnix(paymentDate) }); // ← amount تومان است
};

// L80-83 — discount (فقط حالت fixed مبلغ است؛ percent درصد است)
const applyDiscount = () => {
  if (!discountType || discountValue <= 0) return;
  discountMut.mutate({ discount_type: discountType, discount_value: discountValue }); // ← fixed تومان است
};

utils.ts: tomanToRial = (t) => Math.round(t * 10).

وظایف

  1. در submitPayment، مبلغ را قبل از ارسال به ریال تبدیل کن:
payMut.mutate({ method, amount_rials: tomanToRial(amount), paid_at: isoToUnix(paymentDate) });
  1. در applyDiscount، فقط برای discount_type === 'fixed' مقدار را به ریال تبدیل کن (percent درصد است، تبدیل نشود):
const value = discountType === 'fixed' ? tomanToRial(discountValue) : discountValue;
discountMut.mutate({ discount_type: discountType, discount_value: value });
  1. tomanToRial را از ../../lib/utils import کن.
  2. backend را تغییر نده — تبدیل در backend باعث double-convert مسیر percent و سایر callerهای درست می‌شود (تخفیف دستی از قبل در PatientService::applyDiscount روی ریال کار می‌کند و از UI صفحه دیگر هم درست می‌آید؛ فقط این صفحه باگ دارد).

نکات

  • edge case: تخفیف بر اساس قانون (discount_rule_uuid) مبلغ را از backend می‌گیرد (نه UI) — دست نزن.
  • بعد از fix، یک پرداخت ۵۰۰٬۰۰۰ تومانی ثبت کن و تأیید کن در «پرداخت‌شده‌ها» و مانده، مبلغ درست (۵۰۰٬۰۰۰ تومان) نمایش داده می‌شود، نه ۵۰٬۰۰۰.

تسک ۲ — نمایش کامل پرداخت‌های ثبت‌شده (تاریخ/ساعت + ثبت‌کننده)

وضعیت فعلی

SessionPayment::toArray() از قبل paid_at (Unix) و created_by_name را می‌دهد و type frontend (SessionPaymentEntry) هم دارد. اما ردیف نمایش فقط روش + مبلغ را نشان می‌دهد:

// PaymentStep.tsx L252-260 و DetailsStep.tsx L103-111 (مشابه)
{payments.map((p) => (
  <div key={p.uuid} ...>
    <span>...<span>{METHOD_LABELS[p.method] ?? p.method}</span></span>
    <span>مبلغ : {formatRial(p.amount_rials)}</span>
  </div>
))}

وظایف

در هر دو PaymentStep.tsx و DetailsStep.tsx، ردیف پرداخت را کامل کن تا علاوه بر روش و مبلغ، این‌ها را هم نشان دهد:

  • تاریخ و ساعت پرداخت: formatDateTime(p.paid_at) (شمسی + HH:MM؛ از utils.ts import کن). اگر paid_at خالی بود، از p.created_at.
  • ثبت‌کننده: p.created_by_name (اگر موجود) — مثلاً «ثبت: {created_by_name}».

چیدمان تمیز بماند (مثلاً خط دوم کوچک‌تر و کم‌رنگ زیر روش/مبلغ).

نکات

  • formatDateTime ورودی Unix ثانیه می‌گیرد؛ paid_at/created_at هر دو Unix صحیح‌اند.
  • backend تغییر نمی‌کند — داده از قبل موجود است.

تسک ۳ — منوی «...» روی هر مراجعه: «مشاهده فاکتور» + «آرشیو»

وضعیت فعلی

در SessionServiceCard.tsx آیکون FilesServiceMore (L81) تزئینی است — بدون onClick/منو. «مشاهده فاکتور» فقط به‌صورت دکمه footer وقتی paid است وجود دارد؛ «آرشیو» اصلاً نیست.

وظایف

  1. آیکون «...» را به dropdown trigger تبدیل کن (منوی کوچک با کلیک، بسته‌شدن با کلیک بیرون). آیتم‌ها:
    • مشاهده فاکتور → همان onViewInvoice(session) که کارت از prop می‌گیرد (منطق viewInvoice در PatientDetailPage L85-93؛ اگر invoice_uuid نبود، ابتدا صادر و بعد باز می‌شود).
    • آرشیو (یا «خروج از آرشیو» اگر session.archived) → یک prop جدید onArchive(session, archived: boolean) که کارت صدا می‌زند؛ در PatientDetailPage به mutation آرشیو (تسک ۴) وصل شود.
  2. props جدید کارت: onArchive?: (session: SessionCardData, archived: boolean) => void. type SessionCardData را با archived?: boolean گسترش بده.
  3. از الگوی dropdown موجود پروژه استفاده کن (اگر کامپوننت منوی مشترک هست از آن؛ وگرنه یک منوی ساده با Portal/absolute + بستن با کلیک بیرون، هم‌راستا با بقیه).

نکات

  • «مشاهده فاکتور» در منو نباید دکمه footer را حذف کند مگر بخواهی یکدست کنی — کافی است در منو هم باشد.
  • برای مراجعه‌ی بدون فاکتور، «مشاهده فاکتور» همان مسیر صدور idempotent را طی می‌کند (رفتار فعلی viewInvoice).

تسک ۴ — آرشیو مراجعات (backend + UI فیلتر)

وضعیت فعلی

PatientSession هیچ ستون archived/status/deleted_at ندارد. findByRecord/countByRecord بدون فیلتر همه را برمی‌گردانند. sessions GET پارامتر فیلتر ندارد.

وظایف (Backend اول)

  1. Entity + migration: به PatientSession ستون archived (bool، default false) و archived_at (int nullable، Unix) اضافه کن؛ getter/setter (isArchived, setArchived(bool) که archived_at = archived ? time() : null را ست کند). در toArray() کلید archived را expose کن. make:migration + migrate.
  2. Repository: findByRecord/countByRecord یک پارامتر فیلتر بگیرند: all | active | archived (پیش‌فرض active). actives.archived = false، archiveds.archived = true، all → بدون شرط.
  3. sessions GET: پارامتر query filter (پیش‌فرض active) را بخوان و به repo بده. پس پیش‌فرض آرشیوها نمایش داده نشوند.
  4. endpoint آرشیو: در updateSession (PATCH /api/v1/session/{uuid}) پذیرش فیلد archived (bool) → session->setArchived((bool)$data['archived']). (یا اگر تمیزتر است یک route اختصاصی PATCH /api/v1/session/{uuid}/archive.) owner-scope مثل بقیه‌ی updateSession.

وظایف (Frontend)

  1. دکمه/فیلتر نمایش آرشیو: در تب services (PatientDetailPage.tsx L213-235) دکمه فیلتر تزئینی موجود (TurnsFilter) را فعال کن یا یک segmented/دکمه «نمایش آرشیو» اضافه کن؛ یک state filter: 'active' | 'all' | 'archived' (پیش‌فرض active). query key و URL شامل filter شود:
const [filter, setFilter] = useState<'active'|'all'|'archived'>('active');
const sessionsQ = useQuery({
  queryKey: ['patient-sessions', uuid, filter],
  queryFn: () => api.get(`/api/v1/patient/${uuid}/sessions?filter=${filter}`),
  enabled: !!uuid,
});
  1. اکشن آرشیو: onArchive (تسک ۳) به یک mutation وصل شود که PATCH /api/v1/session/{uuid} با { archived: true/false } می‌زند و ['patient-sessions', uuid] را invalidate می‌کند. toast مناسب («مراجعه آرشیو شد» / «از آرشیو خارج شد»).
  2. کارت آرشیوشده در حالت نمایش آرشیو یک نشانه‌ی بصری داشته باشد (مثلاً badge «آرشیو» یا کم‌رنگ).

نکات

  • تاریخ‌ها Unix صحیح؛ لیست‌های admin طبق قانون. تغییر Entity → migration.
  • سوابق حفظ شود: آرشیو فقط مخفی می‌کند (soft)، حذف نیست؛ فاکتور و پرداخت‌ها دست‌نخورده می‌مانند.
  • بعد از تغییر API، docs/api/patient.md را به‌روز کن (پارامتر filter روی sessions، فیلد archived روی updateSession/entity).
  • edge case: آرشیو کردن مراجعه‌ی تسویه‌شده مجاز است (فقط مخفی می‌شود)؛ گزارش‌های مالی نباید آرشیوها را از سابقه حذف کنند (فقط لیست پیش‌فرض این صفحه فیلتر شود).

قوانین عمومی

  • کنترلرها از BaseController؛ پاسخ‌ها $this->success()/$this->paginated()/$this->error().
  • تاریخ‌ها Unix؛ قیمت‌ها ریال (ذخیره/API)، تومان (UI) با tomanToRial/rialToToman.
  • TanStack Query + الگوهای موجود؛ selectها SearchableSelect.
  • هر تسک جدا تست و کامیت شود. Backend اول در تسک ۴. بعد از کد، graphify update . (بعد کامیت).