From 1d434757246b71c45845553c6735b294142c1b88 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 6 Aug 2026 15:23:06 +0330 Subject: [PATCH] feat(docs): add laser treatment plan and related ADRs for multi-session treatments --- .claude/prompt/laser-treatment-plan.md | 419 + CONTEXT.md | 69 + ...treatment-sessions-are-not-appointments.md | 13 + .../0002-treatment-areas-are-snapshotted.md | 11 + ...d-appointments-drop-the-doctor-slot-key.md | 19 + ...ameters-are-json-keyed-by-resource-type.md | 14 + ...treatment-workflows-are-tagged-services.md | 13 + ...ecord-is-separate-from-the-visit-record.md | 14 + graphify-out/.graphify_labels.json | 67 +- graphify-out/GRAPH_REPORT.md | 669 +- ...a33181d7a472a3b45421089180aae50bb7479.json | 1 + ...7924b71b0875c9f790e7af75a0332c5597f2a.json | 1 + ...bf7fd6bcd4d10f2b4b1807f0285b598619119.json | 1 + ...020d6bb10fda30a2df2524ab5970b8bb25495.json | 1 + ...9d136562c1d65aeb1b0d7739b1267b903a329.json | 1 + ...7a221b5af939978a1af55b4b486dcb2100fa1.json | 1 + ...601df655337e8677b6096d872c2332506652b.json | 1 + ...e6e556db147ca6639e20f8d162cab62dd8efe.json | 1 + ...b8ac3d279322a4520d80e0e6ec6fe1e1535db.json | 1 + ...1d0d40d3a91078a5f02a49baf42a1d6a859b9.json | 1 + ...ec6d65f20ab9f796a08008ff56132a9a6bb50.json | 1 + ...c0cc9084bbe4a14ff3ae46c1f4b8aa4cbd2e2.json | 1 + ...c3c86d5ebb3d18516a875ae3204c571fbcb8e.json | 1 + ...219d6a5d2e154a06d3bd9ad2aa546828eb19c.json | 1 + ...7042adcc7dacff8509cdb8259024c22aa6581.json | 1 + ...b7c31ebfd8f321a2871c59c170003fd6dc0e2.json | 1 + ...89a34e2a2af10ddf32e8379274d9315d381b7.json | 1 + ...6c8f8293252abdab003a7c7c1e1823f768bb9.json | 1 + ...27d3f48cbbd8281afb1dedea41f761c4a061e.json | 1 + ...89223e3bb3f5d9977f256051518ba7b842453.json | 1 + ...afb00dbf56209a1e69092049aa5d43d632447.json | 1 + ...4767a200265283df2c248241052d68e261d7c.json | 1 + ...d7f81b088014923b72196590fa78e6390388b.json | 1 + ...2d56a59137ecd28deda0c1f4eca408a0b65bf.json | 1 + ...07b3389d4fcd9678576397378f605e713d270.json | 1 + ...c0cd85eccce0da2b5cc41123853e1307ecc39.json | 1 + ...fe880205c7b39491e020323b43182b1927fe3.json | 1 + ...fc666b9e8e10ff293d484ccb0ec7ddebcdc8f.json | 1 + ...c3ef8eea85e5837c04b3e4911d94f6bc5fc8d.json | 1 + ...fbfc2c7f5fabf96fc120b9161d394154583f9.json | 1 + ...4e90f01b84cf948453627d3050ee863f97d76.json | 1 + ...7adf964c6f45bf13ad773944c84f80f713efb.json | 1 + ...b907438424cd01701ea1905302918f615b5da.json | 1 + ...521a39528cfe4447594b572425b2e01997817.json | 1 + ...64d26f3e1e2fadbe38280935a250224102d2d.json | 1 + ...e5048e3b70f9325ee78b141f5fba300848d77.json | 1 + ...af642aa4984311671414e6af36b8982bd8b38.json | 1 + ...9773785fe3cab375fb5c0437e1bd7a680d238.json | 1 + ...a29296b1fbb484e5f96932e1d9c97b7e1faf4.json | 1 + ...fac999556f4af33c55ad76721db72604057b4.json | 1 + ...460197a650d69361468ef273de4c1b3b7d8d4.json | 1 + ...e3dc06affa056d087d047f6be5c3223ab2074.json | 1 + ...612d00ecffcd6bc09da9cf3e07f2ab7da0a4a.json | 1 + ...ca6561a8a52a18840d7844ccda0fad1991205.json | 1 + ...62af99b11a003dee631de96736cc93903b06e.json | 1 + ...6fecb923f7d03faaa7ae7b3fe6133c5c7199d.json | 1 + ...1c5c7038c93d6b9e2fcdba7247c8b1356a0c7.json | 1 + ...8f6483281b02fd164ff6c148a7e3b81c30544.json | 1 + ...bb5704950b3341d711de668a29413d0582193.json | 1 + ...29852fd6bfcbb5884dfd8e64b38a85668c82f.json | 1 + ...a7c32c4b5ebf3b7778d4348bc68b0ea0ce897.json | 1 + ...c0255a8ff4eb13215afe7d5ba0058dfad8c32.json | 1 + ...3be96fd2e0e8938491390ec024f220162b937.json | 1 + ...25305b606546d570a0948e7f8768cccfc4e42.json | 1 + ...6d2f052cb4ade8b7df13cc0f44305ba2d0f66.json | 1 + ...2f6535506d5e1680e3b271813e624be39b338.json | 1 + ...6941abe1b8ee47dcea18ffdaab08be14f2d84.json | 1 + graphify-out/cache/stat-index.json | 2 +- graphify-out/graph.json | 16006 +++++++++++++--- graphify-out/manifest.json | 862 +- 70 files changed, 15001 insertions(+), 3234 deletions(-) create mode 100644 .claude/prompt/laser-treatment-plan.md create mode 100644 CONTEXT.md create mode 100644 docs/adr/0001-treatment-sessions-are-not-appointments.md create mode 100644 docs/adr/0002-treatment-areas-are-snapshotted.md create mode 100644 docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md create mode 100644 docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md create mode 100644 docs/adr/0005-treatment-workflows-are-tagged-services.md create mode 100644 docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md create mode 100644 graphify-out/cache/ast/v0.8.44/072154a8b4bc4a968e659476940a33181d7a472a3b45421089180aae50bb7479.json create mode 100644 graphify-out/cache/ast/v0.8.44/0bebfa0dce3d5fdfa58447295507924b71b0875c9f790e7af75a0332c5597f2a.json create mode 100644 graphify-out/cache/ast/v0.8.44/0dd012ebdbe31aa55aa576bca27bf7fd6bcd4d10f2b4b1807f0285b598619119.json create mode 100644 graphify-out/cache/ast/v0.8.44/1c46cb110a9b2c8ae17db70da9f020d6bb10fda30a2df2524ab5970b8bb25495.json create mode 100644 graphify-out/cache/ast/v0.8.44/1c4ea68ad641c6e0e9374579c819d136562c1d65aeb1b0d7739b1267b903a329.json create mode 100644 graphify-out/cache/ast/v0.8.44/1d0433d8ee8a586d37b5d7a03817a221b5af939978a1af55b4b486dcb2100fa1.json create mode 100644 graphify-out/cache/ast/v0.8.44/1d6fb0164c52de51c38f6f9189e601df655337e8677b6096d872c2332506652b.json create mode 100644 graphify-out/cache/ast/v0.8.44/205a0404e878c6605ef02a79c7be6e556db147ca6639e20f8d162cab62dd8efe.json create mode 100644 graphify-out/cache/ast/v0.8.44/25641224fa2513a1b7e1d66f460b8ac3d279322a4520d80e0e6ec6fe1e1535db.json create mode 100644 graphify-out/cache/ast/v0.8.44/282420f28443c29d777191f6cb21d0d40d3a91078a5f02a49baf42a1d6a859b9.json create mode 100644 graphify-out/cache/ast/v0.8.44/2a2d6aa142a13fc6868c5501e8dec6d65f20ab9f796a08008ff56132a9a6bb50.json create mode 100644 graphify-out/cache/ast/v0.8.44/2f215af3e38682233f73a5c578dc0cc9084bbe4a14ff3ae46c1f4b8aa4cbd2e2.json create mode 100644 graphify-out/cache/ast/v0.8.44/38c577432061de325941f2a2e67c3c86d5ebb3d18516a875ae3204c571fbcb8e.json create mode 100644 graphify-out/cache/ast/v0.8.44/3bdfa81cc0a4bec0643811f2330219d6a5d2e154a06d3bd9ad2aa546828eb19c.json create mode 100644 graphify-out/cache/ast/v0.8.44/3bef39dcbe12e69cfadd5b1996d7042adcc7dacff8509cdb8259024c22aa6581.json create mode 100644 graphify-out/cache/ast/v0.8.44/3e0c06ccca281c5b076a08f365cb7c31ebfd8f321a2871c59c170003fd6dc0e2.json create mode 100644 graphify-out/cache/ast/v0.8.44/419f08e26fba11c4270d1cb66ad89a34e2a2af10ddf32e8379274d9315d381b7.json create mode 100644 graphify-out/cache/ast/v0.8.44/4286ad4c2fe1aeb1844dd41b0746c8f8293252abdab003a7c7c1e1823f768bb9.json create mode 100644 graphify-out/cache/ast/v0.8.44/43445dfea43882e0d01e82bdc2a27d3f48cbbd8281afb1dedea41f761c4a061e.json create mode 100644 graphify-out/cache/ast/v0.8.44/46ef48d771de1bda3036463408989223e3bb3f5d9977f256051518ba7b842453.json create mode 100644 graphify-out/cache/ast/v0.8.44/532f0fcbb18a234f1c7c32da700afb00dbf56209a1e69092049aa5d43d632447.json create mode 100644 graphify-out/cache/ast/v0.8.44/53d246eeba97e4142c1aa77982f4767a200265283df2c248241052d68e261d7c.json create mode 100644 graphify-out/cache/ast/v0.8.44/5e992df0892069586fe4c1f469ed7f81b088014923b72196590fa78e6390388b.json create mode 100644 graphify-out/cache/ast/v0.8.44/61f4c7f58b85061746c135102be2d56a59137ecd28deda0c1f4eca408a0b65bf.json create mode 100644 graphify-out/cache/ast/v0.8.44/6229d20d70b20bb4599f759d23807b3389d4fcd9678576397378f605e713d270.json create mode 100644 graphify-out/cache/ast/v0.8.44/65cbc83a36d24af2358a0055c93c0cd85eccce0da2b5cc41123853e1307ecc39.json create mode 100644 graphify-out/cache/ast/v0.8.44/6bc8654e16dbd218cf1eeb6d8dcfe880205c7b39491e020323b43182b1927fe3.json create mode 100644 graphify-out/cache/ast/v0.8.44/6eff7a5ae4faa1a7abc126d126efc666b9e8e10ff293d484ccb0ec7ddebcdc8f.json create mode 100644 graphify-out/cache/ast/v0.8.44/6f4f5ec7bdf3cc6591a29831bd9c3ef8eea85e5837c04b3e4911d94f6bc5fc8d.json create mode 100644 graphify-out/cache/ast/v0.8.44/6fae579b98a64e5f987fc6c67a3fbfc2c7f5fabf96fc120b9161d394154583f9.json create mode 100644 graphify-out/cache/ast/v0.8.44/700da6d0e219ac1e85e86b6720b4e90f01b84cf948453627d3050ee863f97d76.json create mode 100644 graphify-out/cache/ast/v0.8.44/803cf63113dc5e82f6ddb30d7fb7adf964c6f45bf13ad773944c84f80f713efb.json create mode 100644 graphify-out/cache/ast/v0.8.44/822735aad0a3d8ea927b5c50c65b907438424cd01701ea1905302918f615b5da.json create mode 100644 graphify-out/cache/ast/v0.8.44/8601b5648a45bdd5135a49e0e74521a39528cfe4447594b572425b2e01997817.json create mode 100644 graphify-out/cache/ast/v0.8.44/882b8ccb807816d18368c4db63664d26f3e1e2fadbe38280935a250224102d2d.json create mode 100644 graphify-out/cache/ast/v0.8.44/8bd30ded11b61493e69f2aadbabe5048e3b70f9325ee78b141f5fba300848d77.json create mode 100644 graphify-out/cache/ast/v0.8.44/961e3ff480aa6c2c6ee75a4f1d4af642aa4984311671414e6af36b8982bd8b38.json create mode 100644 graphify-out/cache/ast/v0.8.44/a051613355f86a776d0a1e44c959773785fe3cab375fb5c0437e1bd7a680d238.json create mode 100644 graphify-out/cache/ast/v0.8.44/a3a35af84b0201b6af00659d9e1a29296b1fbb484e5f96932e1d9c97b7e1faf4.json create mode 100644 graphify-out/cache/ast/v0.8.44/a5d6ff0a578c4815fee3fea7d40fac999556f4af33c55ad76721db72604057b4.json create mode 100644 graphify-out/cache/ast/v0.8.44/a5fa078fe6602dcbae9d0a5bfcd460197a650d69361468ef273de4c1b3b7d8d4.json create mode 100644 graphify-out/cache/ast/v0.8.44/a831c4a57bebbd5d786265d9d33e3dc06affa056d087d047f6be5c3223ab2074.json create mode 100644 graphify-out/cache/ast/v0.8.44/b2109f4d7f5208f8eecaea0d855612d00ecffcd6bc09da9cf3e07f2ab7da0a4a.json create mode 100644 graphify-out/cache/ast/v0.8.44/b51569102387519b7b97e24f18dca6561a8a52a18840d7844ccda0fad1991205.json create mode 100644 graphify-out/cache/ast/v0.8.44/bc68f7f551455645c2cc2b38b0362af99b11a003dee631de96736cc93903b06e.json create mode 100644 graphify-out/cache/ast/v0.8.44/c7deeb227b2874bba738abb420a6fecb923f7d03faaa7ae7b3fe6133c5c7199d.json create mode 100644 graphify-out/cache/ast/v0.8.44/d665b34e87d79a777a6c9347a371c5c7038c93d6b9e2fcdba7247c8b1356a0c7.json create mode 100644 graphify-out/cache/ast/v0.8.44/de18bf358b9bffd98359fa191158f6483281b02fd164ff6c148a7e3b81c30544.json create mode 100644 graphify-out/cache/ast/v0.8.44/dfa42032860a462fecf10558c33bb5704950b3341d711de668a29413d0582193.json create mode 100644 graphify-out/cache/ast/v0.8.44/e18d78ed4ed0f8d4aeabee313ff29852fd6bfcbb5884dfd8e64b38a85668c82f.json create mode 100644 graphify-out/cache/ast/v0.8.44/e2984972bf7abf11ad01f22e847a7c32c4b5ebf3b7778d4348bc68b0ea0ce897.json create mode 100644 graphify-out/cache/ast/v0.8.44/edc1fdefda62f3add53ef1dc1bdc0255a8ff4eb13215afe7d5ba0058dfad8c32.json create mode 100644 graphify-out/cache/ast/v0.8.44/f587200f8179463f558961ac5b53be96fd2e0e8938491390ec024f220162b937.json create mode 100644 graphify-out/cache/ast/v0.8.44/f5dd65b244278752a9a1f3bc98d25305b606546d570a0948e7f8768cccfc4e42.json create mode 100644 graphify-out/cache/ast/v0.8.44/f7076795ce0cc660c7537ab63106d2f052cb4ade8b7df13cc0f44305ba2d0f66.json create mode 100644 graphify-out/cache/ast/v0.8.44/ff3d832d5b25683f4f1de4a74f12f6535506d5e1680e3b271813e624be39b338.json create mode 100644 graphify-out/cache/ast/v0.8.44/ffbffe5efc33664cc36ff6db6376941abe1b8ee47dcea18ffdaab08be14f2d84.json diff --git a/.claude/prompt/laser-treatment-plan.md b/.claude/prompt/laser-treatment-plan.md new file mode 100644 index 00000000..c68f26cb --- /dev/null +++ b/.claude/prompt/laser-treatment-plan.md @@ -0,0 +1,419 @@ +# طول درمان و پرونده درمان چندجلسه‌ای (فاز اول: لیزر) + +## پروژه + +`clinicpro` (backend + پنل ادمین). هیچ تغییری در `nobat724_front` و `clinic-pro-tauri` لازم نیست. + +## زمینه + +این سند خروجی یک جلسه grilling است. ۲۳ تصمیم قفل شد، شش ADR و یک glossary نوشته شد. +پیش از شروع این‌ها را بخوان: + +- `clinicpro/CONTEXT.md` — واژگان رسمی این دامنه +- `clinicpro/docs/adr/0001-treatment-sessions-are-not-appointments.md` +- `clinicpro/docs/adr/0002-treatment-areas-are-snapshotted.md` +- `clinicpro/docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md` +- `clinicpro/docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md` +- `clinicpro/docs/adr/0005-treatment-workflows-are-tagged-services.md` +- `clinicpro/docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md` +- `clinicpro/docs/architecture/resource-first-model.md` + +از واژگان `CONTEXT.md` استفاده کن. مترادف نساز. + +## هدف + +کلینیک بتواند سرویسی تعریف کند که درمانش چند جلسه طول می‌کشد، و سیستم برای هر بیمار +پرونده درمان بسازد، جلسات را بشمارد، سررسید جلسه بعد را حساب کند، و اپراتور بتواند +برای هر ناحیه بدن، دستگاه و پارامترهایش را ثبت کند. + +فاز اول فقط لیزر. ولی هیچ‌جای کد نباید کلمه «لیزر» را بداند، جز پیاده‌سازی workflow. + +## آنچه از قبل هست و باید استفاده شود + +| چیز | کجا | نکته | +|---|---|---| +| دسته‌بندی گرافی خدمات | `CatalogCategory` + `CatalogCategoryInclude` | نواحی بدن همین‌جا تعریف می‌شوند | +| بستار گذرا و تشخیص تعارض | `CategoryClosureResolver` | `descendants()` و `overlaps()` | +| سرویس | `ServiceItem` با `catalogCategory` | قیمت و مدت اینجاست | +| نوع منبع داینامیک | `ResourceType` | کدهای سیستمی `doctor` / `staff` / `room` | +| منبع با پزشک ناظر | `ClinicResource.supervisor` | تعریف شده ولی در مسیر رزرو خوانده نمی‌شود | +| اشغال منبع در سطح دیتابیس | `OccupancyBucket` | `uniq_bucket_resource_seat(resource_id, bucket_at, seat)` | +| پرسنل و نقش | `ClinicStaff` + `ROLE_STAFF` | `ClinicStaff.user` اختیاری است | +| داشبورد پرسنل | `GET /api/v1/dashboard/staff` | فیلتر روی `a.staff` | +| صفحه پرسنل | `assets/admin/pages/StaffMyServicesPage.tsx` | فقط سرویس‌های تخصیص‌یافته | +| مراجعه مالی | `PatientSession` | در `AppointmentConfirmationService::onConfirmed` ساخته می‌شود | + +## آنچه نیست + +هیچ موجودیتی برای پرونده درمان، جلسه درمان، ثبت ناحیه، پروتکل، یا حوزه فعالیت. +`ServiceItem.sessionCount` هست ولی هیچ منطقی از آن استفاده نمی‌کند — عملاً مرده. + +--- + +## تصمیم‌های قفل‌شده + +این‌ها در جلسه grilling تصمیم‌گیری شده‌اند. اجرایشان کن، دوباره طراحی نکن. +اگر جایی از کد با یکی از این‌ها تناقض داشت، **متوقف شو و بپرس**؛ خودت تصمیم را عوض نکن. + +1. **حوزه فعالیت از `Specialty` جداست.** `Specialty` قرارداد سایت عمومی است و دست نمی‌خورد. +2. **هر کلینیک یک حوزه فعالیت دارد.** نال یعنی تنظیم‌نشده و رفتار امروز. +3. **جلسه درمان موجودیت مستقل است**، نه `Appointment`. +4. **زمان‌بندی با لیست صریح گام‌ها.** فاصله ثابت نداریم. +5. **دوره پایان مشخص دارد.** دوره بی‌پایان وجود ندارد. +6. **همه جلسات از ابتدا ساخته می‌شوند**، ولی فقط جلسه بعدی نوبت می‌گیرد. +7. **ناحیه درمان همان `CatalogCategory` است.** هیچ entity جدیدی برای ناحیه ساخته نشود. +8. **نواحی از دسته سرویس مشتق می‌شوند**، هنگام رزرو انتخاب نمی‌شوند. +9. **فهرست نواحی هنگام ساخت پرونده قفل می‌شود** (snapshot). +10. **پزشک ناظرِ منبع روی نوبت می‌نشیند.** پرسنل انجام می‌دهد. +11. **نوبتی که منبع دارد `active_slot_key` ندارد.** حفاظت فقط با `OccupancyBucket`. +12. **`TreatmentProtocol` موجودیت جداست**، یک‌به‌یک با `ServiceItem`. +13. **پارامترهای دستگاه در JSON**، تعریفشان روی `ResourceType`. +14. **Workflow با tagged service.** موتور داده‌محور نداریم. +15. **حوزه فعالیت را فقط ادمین پلتفرم می‌سازد.** +16. **جلسه وضعیت مستقل دارد** و نوبت را هم‌گام می‌کند. بستن جلسه با ناحیه ناتمام مجاز است. +17. **`TreatmentSession` بالینی است، `PatientSession` مالی.** هیچ فیلد پولی روی جلسه درمان. +18. **قیمت هر جلسه با قیمت روز.** پکیج قیمت نداریم. قیمت هیچ‌وقت قفل نمی‌شود. +19. **جلسه بعد پیشنهاد می‌شود، منشی تأیید می‌کند.** رزرو خودکار بدون انسان نداریم. +20. **حوزه فعالیت فقط workflow را انتخاب می‌کند.** داشبورد اختصاصی به‌ازای تخصص نداریم. +21. **سررسید هر جلسه نسبی به تاریخ واقعی جلسه قبل است.** +22. **no-show جلسه را نمی‌سوزاند.** تعداد جلسات ثابت می‌ماند. +23. **زمان واقعی جدا ثبت می‌شود.** `slotStart` و `slotEnd` هرگز بازنویسی نمی‌شوند. + +--- + +## وظایف + +هر وظیفه را جداگانه پیاده کن، تست بنویس، و بعد سراغ بعدی برو. + +### ۱. حوزه فعالیت کلینیک + +موجودیت جدید در `src/PracticeDomain/Entity/PracticeDomain.php`: + +```php +// جدول سراسری (global) — نه per-tenant. در GlobalTables ثبت شود. +id, uuid, code (unique), name, sort_order, active, created_at, updated_at +``` + +`code` پایدار است و workflow به آن bind می‌شود. بعد از ساخت قابل ویرایش نیست. + +روی `Clinic` یک `ManyToOne` نال‌پذیر اضافه کن: + +```php +#[ORM\ManyToOne(targetEntity: PracticeDomain::class)] +#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] +private ?PracticeDomain $practiceDomain = null; +``` + +migration نباید مقدار پیش‌فرض برای کلینیک‌های موجود بگذارد. نال بماند. + +اندپوینت‌ها: + +- `GET /api/v1/practice-domains` — فهرست فعال‌ها. برای همه نقش‌های پنلی. +- `POST /api/v1/admin/practice-domains` — فقط `ROLE_ADMIN`. +- `PATCH /api/v1/admin/practice-domains/{uuid}` — فقط `name` و `active` و `sort_order`. +- `PATCH /api/v1/clinic/{uuid}/practice-domain` — مدیر کلینیک انتخاب می‌کند. + +در پاسخ هر حوزه یک فیلد `has_workflow` بگذار که از `TreatmentWorkflowRegistry` می‌آید. +پنل ادمین پلتفرم باید بتواند نشان دهد کدام حوزه هنوز workflow ندارد. + +seed اولیه با migration: `beauty` = «کلینیک زیبایی». + +### ۲. پروتکل درمان + +سه موجودیت در `src/Treatment/Entity/`: + +```php +// TreatmentProtocol — وجود این ردیف یعنی سوییچ «طول درمان» روشن است +id, uuid, service_item_id (unique, ON DELETE CASCADE), +supervisor_doctor_id (nullable), active, created_at, updated_at + +// TreatmentProtocolStep — گام‌های دوره +id, protocol_id, step_number, offset_days, created_at +// UNIQUE(protocol_id, step_number) +// offset_days یعنی «فاصله از جلسهٔ قبل»، نه از شروع دوره. گام ۱ همیشه offset_days = 0. + +// TreatmentProtocolStaff — پرسنل مجاز به انجام +id, protocol_id, staff_id +// UNIQUE(protocol_id, staff_id) +``` + +قواعد اعتبارسنجی: + +- حداقل دو گام. پروتکل یک‌جلسه‌ای معنی ندارد؛ آن یعنی سوییچ خاموش. +- `step_number` پیوسته از ۱. +- گام اول `offset_days = 0`. بقیه بزرگ‌تر از صفر. +- حداقل یک پرسنل مجاز. +- `supervisor_doctor_id` باید پزشکِ همان محیط باشد. + +`ServiceItem::$sessionCount` را `@deprecated` علامت بزن، از `toArray()` بیرون **نبر** +(پنل و تایپ TS به آن وابسته‌اند) ولی هیچ منطق جدیدی از آن نخوان. تعداد جلسات +همیشه `count(protocol.steps)` است. + +اندپوینت‌ها زیر `/api/v1/clinic-services/{serviceUuid}/treatment-protocol`: +`GET`، `PUT` (کل پروتکل با گام‌ها و پرسنل یکجا)، `DELETE` (خاموش کردن سوییچ). + +### ۳. پرونده و جلسه درمان + +```php +// TreatmentCase +id, uuid, entity_type, entity_id, // tenant +patient_record_id, service_item_id, protocol_id, +supervisor_doctor_id (nullable), +status, // active | completed | abandoned +total_sessions, // snapshot از تعداد گام‌ها +opened_at, closed_at (nullable), +created_at, updated_at + +// TreatmentCaseArea — snapshot نواحی (تصمیم ۹) +id, case_id, catalog_category_id, name_snapshot, sort_order +// UNIQUE(case_id, catalog_category_id) + +// TreatmentSession +id, uuid, case_id, session_number, +appointment_id (nullable, ON DELETE SET NULL), +performed_by_staff_id (nullable), // تصمیم ۲۳ +status, // planned | booked | in_progress | done | cancelled | no_show +due_at (nullable), // تخمینی، بعد از هر جلسه بازمحاسبه می‌شود +started_at (nullable), finished_at (nullable), +note (nullable), +created_at, updated_at +// UNIQUE(case_id, session_number) + +// SessionAreaRecord +id, uuid, session_id, case_area_id, +resource_id (nullable), // دستگاه — در سطح ناحیه +status, // pending | in_progress | completed | skipped +parameters JSON (nullable), // { "energy": 18, "pulse": 3, "shots": 212 } +started_at (nullable), finished_at (nullable), +note (nullable), +created_at, updated_at +// UNIQUE(session_id, case_area_id) +``` + +**هیچ ستون پولی روی این جدول‌ها نگذار.** ADR-0006. + +`name_snapshot` روی `TreatmentCaseArea` عمدی است: اگر مدیر بعداً اسم دسته را عوض کند، +سابقه درمان نباید تغییر کند. + +نواحی هنگام ساخت پرونده اینطور حساب می‌شوند: + +``` +leaves = برگ‌های CategoryClosureResolver::descendants(service.catalogCategory) +اگر descendants خالی بود → خودِ service.catalogCategory تنها ناحیه است +``` + +«برگ» یعنی دسته‌ای که خودش `descendants` ندارد. دسته‌های میانی فقط گروه‌بندی‌اند و +ناحیه درمان نیستند. + +### ۴. اتصال پزشک ناظر به مسیر رزرو + +در `AppointmentController` بلوکی که پزشک را از منبع استنتاج می‌کند (حدود خط ۴۷۱) +یک fallback اضافه کن: + +```php +if ($doctorUuid === '' && $resource->subject() instanceof Doctor) { + $doctorUuid = $resource->subject()->getUuid(); +} +// جدید: +if ($doctorUuid === '' && $resource->getSupervisor() !== null) { + $doctorUuid = $resource->getSupervisor()->getUuid(); +} +``` + +اگر منبعی نه پزشک است نه ناظر دارد، خطای واضح بده: +«این منبع پزشک ناظر ندارد؛ ابتدا در تنظیمات منابع پزشک ناظر را مشخص کنید». +پیام فعلی (`doctor_uuid یا resource_uuid ...`) گمراه‌کننده است. + +### ۵. آزادسازی کلید اسلات برای نوبت‌های منبع‌دار + +در `Appointment::refreshActiveSlotKey()`: + +```php +$this->activeSlotKey = (!$this->isReserve + && $this->resource === null // ← شرط جدید + && in_array($this->status, self::SLOT_OCCUPYING_STATUSES, true)) + ? sprintf('%d:%d', $this->doctor->getId(), $this->slotStart) + : null; +``` + +`setResource()` باید `refreshActiveSlotKey()` را صدا بزند، وگرنه نوبتی که اول ساخته +و بعد منبعش ست می‌شود کلیدش باقی می‌ماند. + +**قبل از این تغییر:** همه مسیرهای ساخت نوبت را فهرست کن و مشخص کن کدام‌ها `resource` +ست نمی‌کنند. آن‌ها بعد از این تغییر همچنان با کلید پزشک محافظت می‌شوند — این درست است، +ولی باید مستند شود که کدام‌ها هستند. ADR-0003 روی همین هشدار داده. + +migration لازم نیست؛ ستون بدون تغییر می‌ماند و فقط منطق پرشدنش عوض می‌شود. + +### ۶. تعریف فیلد روی نوع منبع + +ستون JSON روی `ResourceType`: + +```php +#[ORM\Column(name: 'field_schema', type: 'json', nullable: true)] +private ?array $fieldSchema = null; +``` + +قالب هر فیلد: + +```json +{ "key": "energy", "label": "انرژی", "type": "select", + "options": [7, 8, 9, 10, 12, 14, 16, 18], "required": true, "sort_order": 1 } +``` + +`type` مجاز: `select` و `number` و `text`. همین سه تا، نه بیشتر. + +اعتبارسنجی مقادیر `SessionAreaRecord.parameters` از همین schema می‌آید. یک سرویس +`FieldSchemaValidator` بنویس که هم در ذخیره اندپوینت استفاده شود هم در تست. + +کلیدهای ناشناخته که در schema نیستند رد شوند، نه اینکه بی‌صدا ذخیره شوند. + +seed اولیه: یک `ResourceType` با کد `laser_device` و نام «دستگاه لیزر» و همان سه فیلد +بالا به‌علاوه `shots` از نوع `number`. `is_system = false` تا مدیر بتواند ویرایشش کند. + +### ۷. Workflow قابل توسعه + +```php +// src/Treatment/Workflow/TreatmentWorkflow.php +#[AutoconfigureTag('app.treatment_workflow')] +interface TreatmentWorkflow +{ + public function supports(?string $practiceDomainCode): bool; + + /** بعد از تأیید اولین نوبتِ یک سرویسِ پروتکل‌دار */ + public function openCase(Appointment $appointment, TreatmentProtocol $protocol): TreatmentCase; + + /** بعد از بسته شدن یک جلسه — سررسید جلسه بعد را حساب می‌کند */ + public function onSessionFinished(TreatmentSession $session): void; +} +``` + +`TreatmentWorkflowRegistry` با `#[TaggedIterator('app.treatment_workflow')]` ساخته شود و +اولین workflow ای که `supports()` بدهد را برگرداند. اگر هیچ‌کدام، `DefaultTreatmentWorkflow` +که رفتار عمومی دارد. + +**هسته نباید بداند لیزر چیست.** `AppointmentConfirmationService` فقط این را می‌کند: + +``` +اگر سرویسِ نوبت پروتکل فعال دارد و بیمار پروندهٔ باز برای همان سرویس ندارد + → registry->for(clinic.practiceDomain?.code)->openCase(...) +``` + +`LaserTreatmentWorkflow` در فاز اول تقریباً همان `DefaultTreatmentWorkflow` است. +جدا نگهش دار حتی اگر خالی باشد — نقطه اتصال آینده است. + +### ۸. سررسید و جلسه بعد + +قاعده محاسبه (تصمیم ۲۱): + +``` +due_at(session n) = finished_at(session n-1) + protocol.step[n].offset_days +جلسه ۱ سررسید ندارد؛ تاریخش همان نوبت اول است. +``` + +بعد از `finished_at` شدن هر جلسه، فقط `due_at` **جلسه بعدی** بازمحاسبه شود، نه کل دوره. +جلسات دورتر تخمین قبلی‌شان را نگه می‌دارند تا نوبتشان برسد. + +no-show (تصمیم ۲۲): جلسه به `no_show` می‌رود، `session_number` عوض نمی‌شود، +`total_sessions` عوض نمی‌شود. یک جلسه جایگزین با همان شماره **ساخته نمی‌شود**؛ همان جلسه +دوباره به `planned` برمی‌گردد و `due_at` از تاریخ جلسه قبلِ **انجام‌شده** حساب می‌شود. + +رزرو جلسه بعد (تصمیم ۱۹) **خودکار نیست**. اندپوینت پیشنهاد بده: + +``` +GET /api/v1/treatment-sessions/{uuid}/slot-suggestions +→ اسلات‌های آزاد منبع، از due_at به بعد، با استفاده از ResourceFreeTimeCalculator +``` + +منشی یکی را انتخاب می‌کند و مسیر عادی ساخت نوبت اجرا می‌شود، سپس نوبت به جلسه وصل می‌شود. + +### ۹. اندپوینت‌های اجرای جلسه + +همه زیر `ROLE_STAFF` یا بالاتر. علاوه بر نقش، بررسی کن پرسنل واقعاً به این جلسه دسترسی +دارد — الگویش در `DashboardController::staff` هست (`findActiveByUserAndEntity`). + +``` +GET /api/v1/dashboard/staff/treatment-sessions جلسات امروز پرسنل +GET /api/v1/treatment-sessions/{uuid} جزئیات + نواحی + فیلدهای دستگاه +POST /api/v1/treatment-sessions/{uuid}/start → in_progress، started_at +POST /api/v1/treatment-sessions/{uuid}/finish → done، finished_at، note +POST /api/v1/session-areas/{uuid}/start → in_progress، started_at +POST /api/v1/session-areas/{uuid}/complete → completed، parameters، finished_at +POST /api/v1/session-areas/{uuid}/skip → skipped +GET /api/v1/treatment-cases فهرست پرونده‌ها با فیلتر +GET /api/v1/treatment-cases/{uuid} پرونده + همه جلسات +``` + +هم‌گام‌سازی وضعیت نوبت (تصمیم ۱۶): + +``` +session start → Appointment::STATUS_SALON +session finish → Appointment::STATUS_COMPLETED +``` + +از `ALLOWED_TRANSITIONS` عبور کن، مستقیم `setStatus` نزن. اگر گذار مجاز نبود، جلسه را +ببند ولی نوبت را دست نزن و لاگ بگذار — خطای ۵۰۰ نده. + +بستن جلسه با ناحیه ناتمام **مجاز است** (تصمیم ۱۶). فقط در پاسخ تعداد ناتمام‌ها را برگردان +تا پنل هشدار نشان دهد. + +`started_at` و `finished_at` هرگز روی `Appointment` نوشته نشوند (تصمیم ۲۳). + +### ۱۰. پنل ادمین + +قواعد اجباری این پروژه: + +- هر `select` باید `components/ui/SearchableSelect` باشد. `