# رفع باگ واحد پول پرداخت + اطلاعات پرداخت‌ها + منوی سرویس + آرشیو مراجعات ## پروژه `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) و درست است. پس تومان خام به‌عنوان ریال ذخیره می‌شود → یک صفر کم (÷۱۰ در نمایش). ```tsx // 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`، مبلغ را قبل از ارسال به ریال تبدیل کن: ```tsx payMut.mutate({ method, amount_rials: tomanToRial(amount), paid_at: isoToUnix(paymentDate) }); ``` 2. در `applyDiscount`، فقط برای `discount_type === 'fixed'` مقدار را به ریال تبدیل کن (percent درصد است، تبدیل نشود): ```tsx const value = discountType === 'fixed' ? tomanToRial(discountValue) : discountValue; discountMut.mutate({ discount_type: discountType, discount_value: value }); ``` 3. `tomanToRial` را از `../../lib/utils` import کن. 4. **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`) هم دارد. اما ردیف نمایش فقط روش + مبلغ را نشان می‌دهد: ```tsx // PaymentStep.tsx L252-260 و DetailsStep.tsx L103-111 (مشابه) {payments.map((p) => (
...{METHOD_LABELS[p.method] ?? p.method} مبلغ : {formatRial(p.amount_rials)}
))} ``` ### وظایف در **هر دو** `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`). `active` → `s.archived = false`، `archived` → `s.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) 5. **دکمه/فیلتر نمایش آرشیو**: در تب services (`PatientDetailPage.tsx` L213-235) دکمه فیلتر تزئینی موجود (`TurnsFilter`) را فعال کن یا یک segmented/دکمه «نمایش آرشیو» اضافه کن؛ یک state `filter: 'active' | 'all' | 'archived'` (پیش‌فرض `active`). query key و URL شامل filter شود: ```tsx 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, }); ``` 6. **اکشن آرشیو**: `onArchive` (تسک ۳) به یک mutation وصل شود که `PATCH /api/v1/session/{uuid}` با `{ archived: true/false }` می‌زند و `['patient-sessions', uuid]` را invalidate می‌کند. toast مناسب («مراجعه آرشیو شد» / «از آرشیو خارج شد»). 7. کارت آرشیوشده در حالت نمایش آرشیو یک نشانه‌ی بصری داشته باشد (مثلاً 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 .` (بعد کامیت).