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).
وظایف
- در
submitPayment، مبلغ را قبل از ارسال به ریال تبدیل کن:
payMut.mutate({ method, amount_rials: tomanToRial(amount), paid_at: isoToUnix(paymentDate) });
- در
applyDiscount، فقط برایdiscount_type === 'fixed'مقدار را به ریال تبدیل کن (percent درصد است، تبدیل نشود):
const value = discountType === 'fixed' ? tomanToRial(discountValue) : discountValue;
discountMut.mutate({ discount_type: discountType, discount_value: value });
tomanToRialرا از../../lib/utilsimport کن.- 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.tsimport کن). اگر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 است وجود دارد؛ «آرشیو» اصلاً نیست.
وظایف
- آیکون «...» را به dropdown trigger تبدیل کن (منوی کوچک با کلیک، بستهشدن با کلیک بیرون). آیتمها:
- مشاهده فاکتور → همان
onViewInvoice(session)که کارت از prop میگیرد (منطقviewInvoiceدرPatientDetailPageL85-93؛ اگرinvoice_uuidنبود، ابتدا صادر و بعد باز میشود). - آرشیو (یا «خروج از آرشیو» اگر
session.archived) → یک prop جدیدonArchive(session, archived: boolean)که کارت صدا میزند؛ درPatientDetailPageبه mutation آرشیو (تسک ۴) وصل شود.
- مشاهده فاکتور → همان
- props جدید کارت:
onArchive?: (session: SessionCardData, archived: boolean) => void. typeSessionCardDataرا باarchived?: booleanگسترش بده. - از الگوی dropdown موجود پروژه استفاده کن (اگر کامپوننت منوی مشترک هست از آن؛ وگرنه یک منوی ساده با
Portal/absolute + بستن با کلیک بیرون، همراستا با بقیه).
نکات
- «مشاهده فاکتور» در منو نباید دکمه footer را حذف کند مگر بخواهی یکدست کنی — کافی است در منو هم باشد.
- برای مراجعهی بدون فاکتور، «مشاهده فاکتور» همان مسیر صدور idempotent را طی میکند (رفتار فعلی
viewInvoice).
تسک ۴ — آرشیو مراجعات (backend + UI فیلتر)
وضعیت فعلی
PatientSession هیچ ستون archived/status/deleted_at ندارد. findByRecord/countByRecord بدون فیلتر همه را برمیگردانند. sessions GET پارامتر فیلتر ندارد.
وظایف (Backend اول)
- 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. - Repository:
findByRecord/countByRecordیک پارامتر فیلتر بگیرند:all|active|archived(پیشفرضactive).active→s.archived = false،archived→s.archived = true،all→ بدون شرط. sessionsGET: پارامتر queryfilter(پیشفرضactive) را بخوان و به repo بده. پس پیشفرض آرشیوها نمایش داده نشوند.- 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)
- دکمه/فیلتر نمایش آرشیو: در تب services (
PatientDetailPage.tsxL213-235) دکمه فیلتر تزئینی موجود (TurnsFilter) را فعال کن یا یک segmented/دکمه «نمایش آرشیو» اضافه کن؛ یک statefilter: '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,
});
- اکشن آرشیو:
onArchive(تسک ۳) به یک mutation وصل شود کهPATCH /api/v1/session/{uuid}با{ archived: true/false }میزند و['patient-sessions', uuid]را invalidate میکند. toast مناسب («مراجعه آرشیو شد» / «از آرشیو خارج شد»). - کارت آرشیوشده در حالت نمایش آرشیو یک نشانهی بصری داشته باشد (مثلاً 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 .(بعد کامیت).