diff --git a/.claude/prompt/session-edit-payment-crud-audit-log.md b/.claude/prompt/session-edit-payment-crud-audit-log.md new file mode 100644 index 00000000..a7e691f8 --- /dev/null +++ b/.claude/prompt/session-edit-payment-crud-audit-log.md @@ -0,0 +1,107 @@ +# ویرایش سرویسهای مراجعه + ویرایش/حذف پرداخت + Audit Log مالی جامع + +## پروژه + +`clinicpro` (backend Symfony + پنل ادمین React). تک-ریپو. + +> تست: پنل ادمین با `09390039833 / 09390039833`. اجرا داخل ddev. قرارداد پول: ذخیره/API ریال، UI تومان (`tomanToRial`/`rialToToman`). تاریخ Unix. + +## زمینه + +مراجعه (`PatientSession`) فقط **ایجاد** میشود؛ پس از ثبت، امکان ویرایش سرویسها/کالاها/قیمت ویزیت/بیمه وجود ندارد و پرداختهای ثبتشده نه ویرایش میشوند نه حذف. کاربر میخواهد بتواند همهی اینها را ویرایش کند، **اما** هر تغییر مالی/خدماتی باید در یک **Audit Log** ثبت و قابلمشاهده باشد (چه کسی، چه چیزی، کِی، مقدار قبل/بعد، نوع عملیات). + +## مشکل / هدف + +1. ویرایش کامل یک مراجعه پس از ثبت: سرویسها (افزودن/حذف/تعداد)، کالاهای مصرفی، قیمت ویزیت، بیمه/درصدها، یادداشت، تاریخ مراجعه — با محاسبهی مجدد `services_total_rials`/`final_price_rials`. +2. ویرایش و حذف پرداختهای ثبتشده، با نگهداشتن سازگاری `paid_total`/`remaining`/`payment_method`/`paid_at`. +3. **Audit Log جامع** برای همهی تغییرات مالی/خدماتی: مقدار قبل، مقدار بعد، کاربر، تاریخ/زمان، نوع عملیات (create/update/delete). نمایش تاریخچه در UI. + +## فایلهای مرتبط + +| فایل | نقش | +|------|-----| +| `src/Patient/Service/PatientService.php` | `createSession()` (create-only، L165-265)، `addSessionPayment()` (L338-386)، `calculateFinalPrice()` (L63-93) | +| `src/Patient/Controller/PatientController.php` | `updateSession` PATCH (L1037-1092، فیلدهای محدود)، `addSessionPayment` POST (L1100-1122)؛ **بدون** endpoint ویرایش/حذف payment و ویرایش services | +| `src/Patient/Entity/SessionService.php` | خط سرویس؛ **immutable** (بدون setter)؛ constructor snapshot قیمت | +| `src/Patient/Entity/SessionConsumable.php` | خط کالا؛ immutable | +| `src/Patient/Entity/SessionPayment.php` | پرداخت؛ فقط setter برای createdBy/Name؛ `METHODS` (L18)؛ toArray (L74-84) | +| `src/Patient/Repository/SessionServiceRepository.php` / `SessionConsumableRepository.php` / `SessionPaymentRepository.php` | فقط `save()` — **بدون `remove()`** | +| `src/Patient/Entity/PatientSession.php` | `getPaidTotalRials()` (L164-170)، `getRemainingRials()` (L173-176)؛ collections با `cascade:['remove']` | +| `src/Appointment/Entity/AppointmentEvent.php` + Repository + endpoint | **الگوی مرجع audit** (id, uuid, FK, type, title, actor_user_id, actor_name, reason, created_at؛ `findByAppointmentUuid`؛ `GET /appointment/{uuid}/events`) | +| `src/Settlement/Service/WalletService.php` | `resolveActorName(?User)` (L34-43) — نام نمایشی کاربر | +| `src/Shared/Constant/ErrorCodes.php` | `ERR_SESSION_NOT_FOUND`, `ERR_SESSION_PAYMENT_INVALID`, `ERR_SESSION_PAYMENT_EXCEEDS` (L64-66) | +| `assets/admin/components/SessionServiceCard.tsx` | dropdown «...» (L101-114) — محل افزودن «ویرایش» + «تاریخچه تغییرات» | +| `assets/admin/pages/PatientDetailPage.tsx` | تب services (L225-248)؛ کارتها؛ `sessionsQ` | +| `assets/admin/components/session/PaymentStep.tsx` | ردیف پرداختها (L250-270) — محل ویرایش/حذف + audit | +| `assets/admin/components/session/CreateStep.tsx` | فرم ثبت (submit body L233-245) — **الگوی فرم ویرایش** | +| `assets/admin/pages/NewSessionPage.tsx` | ویزارد ثبت — قابل بازاستفاده برای ویرایش | +| `docs/api/patient.md` | مستندات | + +## وضعیت فعلی + +`updateSession` فیلدهای محدود میپذیرد (notes/archived/discount/paid_at/payment_method) — نه services/consumables/visit_price: + +```php +// PatientController::updateSession (خلاصه) +if (isset($data['notes'])) { $session->setNotes($data['notes']); } +if (array_key_exists('archived', $data)) { $session->setArchived((bool)$data['archived']); } +// discount_rule_uuid / discount_type / paid_at / payment_method ... +// ← هیچ services / consumables / visit_price_rials +``` + +`createSession` تنها جایی است که `SessionService`/`SessionConsumable` ساخته و مجموع محاسبه میشود (create-only). پرداخت فقط `POST` دارد؛ `grep payments/{` صفر → نه PATCH نه DELETE. + +## وظایف + +### ۱. Entity + migration — `SessionAuditLog` (الگوی AppointmentEvent) + +`src/Patient/Entity/SessionAuditLog.php` (جدید): +- ستونها: `id`, `uuid`, `session` (ManyToOne `PatientSession`, `onDelete: CASCADE`), `field` string (مثل `visit_price_rials`, `services`, `consumables`, `payment`, `discount`), `operation` string (`create`|`update`|`delete`), `old_value` text nullable, `new_value` text nullable, `actor_user_id` int nullable, `actor_name` string nullable, `note` string nullable, `created_at` int. +- constructor `(PatientSession $session, string $field, string $operation)`؛ fluent `setActor(?int,?string)`, `setValues(?string $old, ?string $new)`, `setNote(?string)`. +- `toArray()`: `field`, `operation`, `old_value`, `new_value`, `actor_name`, `note`, `created_at`. +- Repository `SessionAuditLogRepository` با `save()` و `findBySessionUuid(string $uuid): array` (array hydration، مرتب بر `created_at DESC`). +- migration. + +> مقادیر قبل/بعد را بهصورت رشتهی خوانا ذخیره کن (مثلاً برای پول ریال عددی؛ برای لیست سرویسها یک خلاصه مثل «تزریق ژل ×۱، لیزر ×۲» یا JSON فشرده). ثبات مهمتر از فرمت است. + +### ۲. Service — ثبت audit + منطق ویرایش/حذف + +در `PatientService`: +- helper `logSessionChange(PatientSession $s, string $field, string $op, ?string $old, ?string $new, ?User $actor, ?string $note = null)` که `SessionAuditLog` میسازد و ذخیره میکند (`actor_name` با `walletService->resolveActorName`). +- **`updateSessionServices(PatientSession $s, array $data, User $actor)`**: سرویسها/کالاها/قیمت ویزیت/بیمه را جایگزین کند: + - snapshot مقادیر قبل (visit_price, services خلاصه, consumables خلاصه, services_total, final_price). + - سرویسهای قبلی را `remove` (به `SessionServiceRepository` متد `remove()` اضافه کن)، سپس از `$data['services']` دوباره بساز (مثل createSession). + - همین برای consumables (`SessionConsumableRepository::remove()`). + - `setVisitPriceRials`, بیمه/درصدها، `session_at`, notes را ست کن. + - با `calculateFinalPrice(...)` + مجموع کالاها، `services_total_rials`/`final_price_rials` را بازمحاسبه کن (دقیقاً مثل createSession L213-240). + - برای هر فیلدِ تغییرکرده یک `logSessionChange(... 'update' ...)` با old/new بزن. +- **`updatePayment(SessionPayment $p, array $data, User $actor)`**: `method`/`amount_rials`/`paid_at` را ویرایش کند (setterها را به `SessionPayment` اضافه کن). سقف: مجموع پرداختها نباید از `final - discount` بیشتر شود. audit با field=`payment`, op=`update`, old/new = مبلغ قبل/بعد. +- **`deletePayment(SessionPayment $p, User $actor)`**: پرداخت را `remove` (به `SessionPaymentRepository::remove()`). audit op=`delete`, old=مبلغ. +- **بازمحاسبهی فیلدهای کششده**: بعد از ویرایش/حذف پرداخت، اگر `getRemainingRials() > 0` بود `payment_method='pending'` و `paid_at=null`؛ اگر صفر شد `payment_method`/`paid_at` را ست کن. (چون `getPaidTotalRials()` از collection زنده جمع میزند ولی `payment_method`/`paid_at` کشاند.) +- **wallet edge**: اگر پرداخت `wallet` بود، ویرایش/حذف باید تراکنش کیف پول را جبران کند (بازگشت/کسر تفاوت). اگر جبران خارج از scope است، **حذف/ویرایش پرداخت wallet را مسدود کن** (خطای ۴۲۲ با پیام فارسی) تا مغایرت مالی ایجاد نشود — این سادهتر و امنتر است؛ در پرامپت این گزینه را انتخاب کن مگر بازگشت کیف پول ساده باشد. + +### ۳. Controller — endpointهای جدید + +در `PatientController` (extends BaseController، owner-scope مثل `updateSession`): +- **ویرایش services**: `updateSession` را گسترش بده تا اگر `services`/`consumables`/`visit_price_rials`/insurance آمد، `patientService->updateSessionServices()` صدا زده شود؛ یا یک route جداگانه `PATCH /api/v1/session/{uuid}/services`. (گسترش `updateSession` تمیزتر است.) +- **ویرایش پرداخت**: `PATCH /api/v1/session/{uuid}/payments/{paymentUuid}` → `updatePayment`. +- **حذف پرداخت**: `DELETE /api/v1/session/{uuid}/payments/{paymentUuid}` → `deletePayment`. +- **تاریخچه**: `GET /api/v1/session/{uuid}/audit-log` → `$this->success($auditRepo->findBySessionUuid($uuid))`. +- گارد: پرداخت باید متعلق به همان session باشد؛ session متعلق به owner (`ownsRecord`). + +### ۴. Frontend — فرم ویرایش + کنترل پرداخت + نمایش تاریخچه + +1. **منوی کارت** (`SessionServiceCard.tsx` dropdown L101-114): افزودن آیتمهای «ویرایش» و «تاریخچه تغییرات» (props جدید `onEdit(session)`, `onViewAudit(session)`). +2. **فرم ویرایش**: از `CreateStep` بازاستفاده کن (یا کامپوننت مشترک) در حالت edit؛ با دادهی فعلی session پر شود و به `PATCH /session/{uuid}` (با body مثل CreateStep L233-245) بفرستد. مسیر `session/{uuid}/edit` یا مودال. +3. **ردیف پرداخت** (`PaymentStep.tsx` L250-270): برای هر پرداخت آیکون ویرایش (مودال کوچک: مبلغ تومان + روش + تاریخ → `PATCH .../payments/{uuid}`) و حذف (`ConfirmDialog` → `DELETE`). بعد از هر عملیات `invalidate()`. +4. **تاریخچه تغییرات**: یک مودال/بخش که `GET /session/{uuid}/audit-log` را میخواند و هر رکورد را نشان میدهد: نوع عملیات (ایجاد/ویرایش/حذف — رنگبندی)، فیلد، مقدار قبل → بعد، کاربر، تاریخ/زمان شمسی (`formatDateTime`). مرتب نزولی. + +## نکات مهم + +- **همهی مسیرهای تغییر باید audit بزنند**: ویرایش سرویس، کالا، قیمت ویزیت، مبلغ سرویسها، ویرایش/حذف پرداخت، تخفیف. حتی `applyDiscount`/`applyDiscountRule` موجود را هم به `logSessionChange` مجهز کن (field=`discount`). +- تاریخها Unix؛ پول ریال (ذخیره) / تومان (UI). لیستهای admin array hydration. +- Entity جدید + ستونها → migration (diff سپس migrate؛ خطوط drift نامرتبط را از migration پاک کن). +- سازگاری مالی: بعد از هر ویرایش/حذف پرداخت، `paid_total`/`remaining`/`is_paid`/`paid_at` باید درست بمانند (بازمحاسبهی فیلدهای کششده). +- **wallet**: تصمیم امن = مسدودکردن ویرایش/حذف پرداخت `wallet` مگر جبران کیف پول پیاده شود. +- مستندات: `docs/api/patient.md` — endpointهای جدید (services edit، payment PATCH/DELETE، audit-log GET) با method/path/permission/body/response/errors. +- این فیچر بزرگ و حساس مالی است — هر وظیفه (۱..۴) جدا پیاده، تست (شامل مسیر خطا/مرزی) و کامیت شود. Backend اول. بعد از کد `graphify update .` (بعد کامیت). diff --git a/.claude/prompt/session-payment-currency-and-service-archive.md b/.claude/prompt/session-payment-currency-and-service-archive.md new file mode 100644 index 00000000..57b380f5 --- /dev/null +++ b/.claude/prompt/session-payment-currency-and-service-archive.md @@ -0,0 +1,167 @@ +# رفع باگ واحد پول پرداخت + اطلاعات پرداختها + منوی سرویس + آرشیو مراجعات + +## پروژه + +`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) => ( +