feat(docs): add laser treatment plan and related ADRs for multi-session treatments

This commit is contained in:
hamed
2026-08-06 15:23:06 +03:30
parent 1a07dad17c
commit 1d43475724
70 changed files with 15001 additions and 3234 deletions
+419
View File
@@ -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` باشد. `<select>` بومی ممنوع.
- هر بولین باید `components/ui/Switch` باشد. checkbox بومی ممنوع.
- صفحه جدید با تم و Layout و کامپوننت‌های موجود ساخته شود. طراحی جدید نکن.
- تاریخ‌ها شمسی. متن‌ها فارسی. RTL.
صفحه‌ها و تغییرها:
1. **فرم سرویس** — سوییچ «طول درمان». روشن که شد: جدول گام‌ها (شماره و فاصله از جلسه قبل)،
`SearchableSelect` چندتایی پرسنل مجاز، `SearchableSelect` پزشک ناظر.
2. **تنظیمات کلینیک**`SearchableSelect` حوزه فعالیت.
3. **صفحه نوع منابع** — ویرایشگر `fieldSchema`. افزودن و حذف فیلد، انتخاب نوع، گزینه‌ها.
4. **پنل ادمین پلتفرم** — CRUD حوزه‌های فعالیت با نشان «workflow دارد / ندارد».
5. **فهرست پرونده‌های درمان** — نام بیمار، سرویس، جلسه چندم از چند، وضعیت، سررسید بعدی، اپراتور.
6. **صفحه اجرای جلسه** — کارت هر ناحیه با دکمه شروع، تایمر زنده، فرم داینامیک از `fieldSchema`،
دکمه اتمام ناحیه، دکمه لغو ناحیه. پایین صفحه یادداشت کلی و دکمه اتمام جلسه با هشدار
نواحی ناتمام. اسکرین‌شات‌های مرجع در تیکت این کار هست.
7. **صف «جلسات بدون نوبت»** — جلساتی که `status = planned` و `due_at` گذشته یا نزدیک است.
از هر ردیف مستقیم به انتخاب اسلات پیشنهادی.
تایمر فقط نمایشی است. زمان معتبر همان `started_at` و `finished_at` سرور است.
---
## ترتیب اجرا
```
۱ → ۲ → ۳ → ۶ → ۷ → ۴ → ۵ → ۸ → ۹ → ۱۰
```
وظیفه ۵ (کلید اسلات) روی پرترافیک‌ترین جدول سیستم است. بعد از وظیفه ۴ انجامش بده و
قبل و بعدش تست رگرسیون رزرو را کامل اجرا کن.
## تست
- unit برای `CategoryClosureResolver` در حالت برگ‌یابی نواحی
- unit برای محاسبه `due_at` با گام‌های نامساوی (سناریوی بوتاکس: ۰، ۱۵، ۳۰، ۳۰)
- unit برای `FieldSchemaValidator` با کلید ناشناخته و مقدار خارج از `options`
- functional: پزشک ناظر مشترک بین دو دستگاه، دو رزرو هم‌ساعت، هر دو باید موفق شوند
- functional: اتاق با `capacity = 3`، سه رزرو هم‌ساعت، هر سه باید موفق شوند
- functional: چرخه کامل یک دوره سه‌جلسه‌ای شامل یک no-show
- رگرسیون: رزرو بدون منبع همچنان با کلید پزشک از دوبار رزرو جلوگیری کند
## خارج از دامنه فاز اول
- seeder هوشمند با AI برای پیشنهاد دسته‌بندی و منابع
- گزارش‌های تحلیلی روی `parameters` (کوئری JSON بدون ایندکس)
- حوزه فعالیت دوم غیر از زیبایی
- داشبورد اختصاصی به‌ازای هر تخصص
- قیمت پکیج و قفل قیمت
## مستندسازی
طبق قانون ثابت این پروژه، `docs/api/*` در همان session به‌روز شود.
اگر تصمیمی در حین اجرا عوض شد، ADR مربوطه به‌روز شود یا ADR جدید نوشته شود.
+69
View File
@@ -0,0 +1,69 @@
# ClinicPro Backend
The Symfony API and bundled React admin that owns all clinic data, scheduling and auth for the
ClinicPro product. This glossary records the language the backend uses for its own concepts.
## Language
### Clinic configuration
**Practice Domain**:
The single field of practice a clinic declares it operates in — beauty, dental, orthopaedics. It is a
configuration key: it selects which dashboard, forms and treatment workflows the clinic gets. A
clinic has exactly one.
_Avoid_: Specialty, clinic type, field, discipline
**Specialty**:
A medical specialty label attached to a Clinic or a Doctor, used by the public booking site for
listing and SEO. It is descriptive, not configuration — it never selects behaviour.
_Avoid_: Practice Domain, category
### Treatment
**Treatment Protocol**:
The template attached to a service that says a course of that service runs over several sessions, when
each one falls due, which doctor supervises the course, and which staff are allowed to perform it.
Defined once by the clinic manager, not per patient.
_Avoid_: Treatment plan, course template, session config
**Supervising Doctor**:
The doctor answerable for a Treatment Protocol and for every Treatment Case opened from it. They carry
clinical responsibility; they do not necessarily perform the treatment.
_Avoid_: Doctor, owner, responsible
**Operator**:
The staff member who actually performs a Treatment Session — runs the device, treats each area, and
records what was done. Chosen at booking time from the staff the Treatment Protocol allows.
_Avoid_: Technician, performer, staff, nurse
**Treatment Case**:
One patient's run of a Treatment Protocol, from the first booking until the course ends. It owns the
sessions and carries the state of the whole course.
_Avoid_: Treatment plan, patient file, dossier, episode
**Treatment Session**:
One numbered step of a Treatment Case — session 3 of 8. It exists whether or not it has been booked
yet, so its number survives cancellation and rescheduling. It holds the plan and the clinical record,
never money.
_Avoid_: Appointment, visit, session slot, patient session
**Visit Record**:
The billing side of one attended visit — services rendered, insurance shares, discounts, payments. It
comes into existence when an Appointment is confirmed, so a Treatment Session that has not been booked
yet has none.
_Avoid_: Session, invoice, patient session
**Appointment**:
A reserved slot in a calendar. It is the booking, not the treatment. A Treatment Session may point at
one, at a different one after rescheduling, or at none while still unbooked.
_Avoid_: Session, booking, reservation
**Treatment Area**:
A region of the body a Treatment Session is performed on — underarms, bikini line. A single session
covers several, each completed and recorded on its own.
_Avoid_: Zone, body part, region
**Session Area Record**:
What the operator actually did to one Treatment Area in one Treatment Session — the device used, the
parameters it was set to, how long it took, and any note.
_Avoid_: Treatment log, area result, shot record
@@ -0,0 +1,13 @@
# Treatment Sessions are separate from Appointments
Multi-session treatments (laser courses, botox top-ups) need a session that keeps its number and its
clinical record across cancellations and reschedules, and they must not lock eight months of calendar
slots the moment a course starts. So a Treatment Session is its own record owning the clinical data,
holding an optional reference to an Appointment; booking a session creates the Appointment, and
rescheduling swaps it without disturbing the session.
## Considered Options
Making the Appointment itself the session was rejected: session numbering would break on every
cancellation, and pre-creating every future session as a real Appointment would occupy slots months
ahead for patients who may never attend.
@@ -0,0 +1,11 @@
# A Treatment Case snapshots its areas
The areas a service covers are derived from the catalog category graph, which the clinic manager can
edit at any time. Because a session's area records are a medical record, a Treatment Case copies its
area list at creation and never re-derives it; editing the category graph afterwards affects only
cases opened from then on.
## Consequences
An in-flight case will not pick up a category change. If a clinic genuinely needs that, it has to be
an explicit re-sync action, not a silent side effect of editing settings.
@@ -0,0 +1,19 @@
# Resource-backed appointments are guarded by occupancy, not the doctor slot key
An appointment's `active_slot_key` is `doctor_id:slot_start` under a unique index, which assumes the
doctor is the thing being occupied. Once a doctor supervises several devices that assumption breaks:
the second booking in the same hour on a different device is rejected. Resource occupancy already
guards those bookings at the database level via `uniq_bucket_resource_seat (resource_id, bucket_at,
seat)`, so an appointment that carries a resource leaves `active_slot_key` null and lets occupancy be
the sole authority; only resourceless legacy bookings keep the doctor key.
## Considered Options
Rekeying on the resource (`r{resource_id}:{slot_start}`) was rejected because it silently defeats
`ClinicResource.capacity`: a room seating three would reject its second patient, and the unique index
knows nothing about seats, buffers, or setup and cleanup time.
## Consequences
Any booking path that omits the resource falls back to the doctor key. Those paths have to be found
and made resource-aware, or they end up with weaker protection than they have today.
@@ -0,0 +1,14 @@
# Session parameters are JSON, described by the resource type
What an operator records for a treated area splits in two: facts every specialty shares — which
resource, when it started and finished, the note — and readings that belong to one kind of device,
like a laser's energy, pulse and shot count. The shared facts are real columns on the session's area
record; the device-specific readings live in a `parameters` JSON column, and the field list that
drives the form and its validation is declared on the resource type. Adding a dentistry or RF device
is then a settings change, not a migration.
## Consequences
Aggregate reporting over a JSON key (average energy per area over a quarter) has no index behind it
and will be slow. If such a report becomes a first-class requirement, the specific keys get promoted
to real columns rather than the whole design being reversed.
@@ -0,0 +1,13 @@
# Treatment workflows are tagged services, not a configurable engine
Everything that differs between specialties as *data* — categories, protocol steps, resource types,
form fields — is already stored as data. What is left is behaviour: when a case opens, what happens
when a session completes. That is expressed as a `TreatmentWorkflow` interface with one
implementation per practice domain, resolved through a Symfony tagged-service registry, so booking
code never names a specialty and a new one is a new class rather than a core change.
## Considered Options
A data-driven state machine editable by clinic managers was rejected: no clinic manager will author a
state machine, so in practice only developers would use it — code with worse syntax, no type safety
and no debugger.
@@ -0,0 +1,14 @@
# The clinical record of a session is separate from its visit record
`PatientSession` already means "an attended visit that gets billed" — it is created when an
Appointment is confirmed and owns services, insurance shares, discounts and payments. A Treatment
Session, by contrast, exists from the moment a course is planned, long before any booking, and must
survive reschedules. So the two stay separate: a Treatment Session holds the plan and the clinical
record (its area records, devices and readings) and carries no money, while the Visit Record keeps
the financial side and is reached through the Appointment.
## Consequences
A session's full picture spans four records — case, session, appointment, visit — so queries are
longer. In exchange, neither side can silently become a second source of truth for the other. Any
money field appearing on a Treatment Session is a design error.
+66 -1
View File
@@ -1315,17 +1315,30 @@
"1313": "Community 1313",
"1314": "Community 1314",
"1315": "Community 1315",
"1316": "Community 1316",
"1317": "Community 1317",
"1318": "Community 1318",
"1319": "Community 1319",
"1320": "Community 1320",
"1321": "Community 1321",
"1322": "Community 1322",
"1323": "Community 1323",
"1324": "Community 1324",
"1325": "Community 1325",
"1326": "Community 1326",
"1327": "Community 1327",
"1328": "Community 1328",
"1329": "Community 1329",
"1330": "Community 1330",
"1331": "Community 1331",
"1332": "Community 1332",
"1333": "Community 1333",
"1334": "Community 1334",
"1335": "Community 1335",
"1336": "Community 1336",
"1337": "Community 1337",
"1338": "Community 1338",
"1339": "Community 1339",
"1340": "Community 1340",
"1341": "Community 1341",
"1342": "Community 1342",
@@ -1336,33 +1349,70 @@
"1347": "Community 1347",
"1348": "Community 1348",
"1349": "Community 1349",
"1350": "Community 1350",
"1351": "Community 1351",
"1352": "Community 1352",
"1353": "Community 1353",
"1354": "Community 1354",
"1355": "Community 1355",
"1356": "Community 1356",
"1357": "Community 1357",
"1358": "Community 1358",
"1359": "Community 1359",
"1360": "Community 1360",
"1361": "Community 1361",
"1362": "Community 1362",
"1363": "Community 1363",
"1364": "Community 1364",
"1365": "Community 1365",
"1366": "Community 1366",
"1367": "Community 1367",
"1368": "Community 1368",
"1369": "Community 1369",
"1370": "Community 1370",
"1371": "Community 1371",
"1372": "Community 1372",
"1373": "Community 1373",
"1374": "Community 1374",
"1375": "Community 1375",
"1376": "Community 1376",
"1377": "Community 1377",
"1378": "Community 1378",
"1379": "Community 1379",
"1380": "Community 1380",
"1381": "Community 1381",
"1382": "Community 1382",
"1383": "Community 1383",
"1384": "Community 1384",
"1385": "Community 1385",
"1386": "Community 1386",
"1387": "Community 1387",
"1388": "Community 1388",
"1389": "Community 1389",
"1390": "Community 1390",
"1391": "Community 1391",
"1392": "Community 1392",
"1393": "Community 1393",
"1394": "Community 1394",
"1395": "Community 1395",
"1396": "Community 1396",
"1397": "Community 1397",
"1398": "Community 1398",
"1399": "Community 1399",
"1400": "Community 1400",
"1401": "Community 1401",
"1402": "Community 1402",
"1403": "Community 1403",
"1404": "Community 1404",
"1405": "Community 1405",
"1406": "Community 1406",
"1407": "Community 1407",
"1408": "Community 1408",
"1409": "Community 1409",
"1410": "Community 1410",
"1411": "Community 1411",
"1412": "Community 1412",
"1413": "Community 1413",
"1414": "Community 1414",
"1415": "Community 1415",
"1416": "Community 1416",
@@ -1371,18 +1421,33 @@
"1419": "Community 1419",
"1420": "Community 1420",
"1421": "Community 1421",
"1422": "Community 1422",
"1423": "Community 1423",
"1424": "Community 1424",
"1425": "Community 1425",
"1426": "Community 1426",
"1427": "Community 1427",
"1428": "Community 1428",
"1429": "Community 1429",
"1430": "Community 1430",
"1431": "Community 1431",
"1432": "Community 1432",
"1433": "Community 1433",
"1434": "Community 1434",
"1435": "Community 1435",
"1436": "Community 1436",
"1437": "Community 1437",
"1438": "Community 1438",
"1439": "Community 1439",
"1440": "Community 1440",
"1441": "Community 1441",
"1442": "Community 1442",
"1443": "Community 1443",
"1444": "Community 1444",
"1445": "Community 1445",
"1447": "Community 1447",
"1452": "Community 1452",
"1453": "Community 1453",
"1457": "Community 1457",
"1462": "Community 1462",
"1475": "Community 1475"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0001_treatment_sessions_are_not_appointments_md", "label": "0001-treatment-sessions-are-not-appointments.md", "file_type": "document", "source_file": "docs/adr/0001-treatment-sessions-are-not-appointments.md", "source_location": "L1"}, {"id": "adr_0001_treatment_sessions_are_not_appointments_treatment_sessions_are_separate_from_appointments", "label": "Treatment Sessions are separate from Appointments", "file_type": "document", "source_file": "docs/adr/0001-treatment-sessions-are-not-appointments.md", "source_location": "L1"}, {"id": "adr_0001_treatment_sessions_are_not_appointments_considered_options", "label": "Considered Options", "file_type": "document", "source_file": "docs/adr/0001-treatment-sessions-are-not-appointments.md", "source_location": "L9"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0001_treatment_sessions_are_not_appointments_md", "target": "adr_0001_treatment_sessions_are_not_appointments_treatment_sessions_are_separate_from_appointments", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0001-treatment-sessions-are-not-appointments.md", "source_location": "L1", "weight": 1.0}, {"source": "adr_0001_treatment_sessions_are_not_appointments_treatment_sessions_are_separate_from_appointments", "target": "adr_0001_treatment_sessions_are_not_appointments_considered_options", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0001-treatment-sessions-are-not-appointments.md", "source_location": "L9", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_context_md", "label": "CONTEXT.md", "file_type": "document", "source_file": "CONTEXT.md", "source_location": "L1"}, {"id": "clinicpro_context_clinicpro_backend", "label": "ClinicPro Backend", "file_type": "document", "source_file": "CONTEXT.md", "source_location": "L1"}, {"id": "clinicpro_context_language", "label": "Language", "file_type": "document", "source_file": "CONTEXT.md", "source_location": "L6"}, {"id": "clinicpro_context_clinic_configuration", "label": "Clinic configuration", "file_type": "document", "source_file": "CONTEXT.md", "source_location": "L8"}, {"id": "clinicpro_context_treatment", "label": "Treatment", "file_type": "document", "source_file": "CONTEXT.md", "source_location": "L21"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_context_md", "target": "clinicpro_context_clinicpro_backend", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTEXT.md", "source_location": "L1", "weight": 1.0}, {"source": "clinicpro_context_clinicpro_backend", "target": "clinicpro_context_language", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTEXT.md", "source_location": "L6", "weight": 1.0}, {"source": "clinicpro_context_language", "target": "clinicpro_context_clinic_configuration", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTEXT.md", "source_location": "L8", "weight": 1.0}, {"source": "clinicpro_context_language", "target": "clinicpro_context_treatment", "relation": "contains", "confidence": "EXTRACTED", "source_file": "CONTEXT.md", "source_location": "L21", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0005_treatment_workflows_are_tagged_services_md", "label": "0005-treatment-workflows-are-tagged-services.md", "file_type": "document", "source_file": "docs/adr/0005-treatment-workflows-are-tagged-services.md", "source_location": "L1"}, {"id": "adr_0005_treatment_workflows_are_tagged_services_treatment_workflows_are_tagged_services_not_a_configurable_engine", "label": "Treatment workflows are tagged services, not a configurable engine", "file_type": "document", "source_file": "docs/adr/0005-treatment-workflows-are-tagged-services.md", "source_location": "L1"}, {"id": "adr_0005_treatment_workflows_are_tagged_services_considered_options", "label": "Considered Options", "file_type": "document", "source_file": "docs/adr/0005-treatment-workflows-are-tagged-services.md", "source_location": "L9"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0005_treatment_workflows_are_tagged_services_md", "target": "adr_0005_treatment_workflows_are_tagged_services_treatment_workflows_are_tagged_services_not_a_configurable_engine", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0005-treatment-workflows-are-tagged-services.md", "source_location": "L1", "weight": 1.0}, {"source": "adr_0005_treatment_workflows_are_tagged_services_treatment_workflows_are_tagged_services_not_a_configurable_engine", "target": "adr_0005_treatment_workflows_are_tagged_services_considered_options", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0005-treatment-workflows-are-tagged-services.md", "source_location": "L9", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0004_session_parameters_are_json_keyed_by_resource_type_md", "label": "0004-session-parameters-are-json-keyed-by-resource-type.md", "file_type": "document", "source_file": "docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md", "source_location": "L1"}, {"id": "adr_0004_session_parameters_are_json_keyed_by_resource_type_session_parameters_are_json_described_by_the_resource_type", "label": "Session parameters are JSON, described by the resource type", "file_type": "document", "source_file": "docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md", "source_location": "L1"}, {"id": "adr_0004_session_parameters_are_json_keyed_by_resource_type_consequences", "label": "Consequences", "file_type": "document", "source_file": "docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md", "source_location": "L10"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0004_session_parameters_are_json_keyed_by_resource_type_md", "target": "adr_0004_session_parameters_are_json_keyed_by_resource_type_session_parameters_are_json_described_by_the_resource_type", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md", "source_location": "L1", "weight": 1.0}, {"source": "adr_0004_session_parameters_are_json_keyed_by_resource_type_session_parameters_are_json_described_by_the_resource_type", "target": "adr_0004_session_parameters_are_json_keyed_by_resource_type_consequences", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0004-session-parameters-are-json-keyed-by-resource-type.md", "source_location": "L10", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082035_php", "label": "Version20260804082035.php", "file_type": "code", "source_file": "migrations/Version20260804082035.php", "source_location": "L1"}, {"id": "migrations_version20260804082035_version20260804082035", "label": "Version20260804082035", "file_type": "code", "source_file": "migrations/Version20260804082035.php", "source_location": "L17"}, {"id": "abstractmigration", "label": "AbstractMigration", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "migrations_version20260804082035_version20260804082035_getdescription", "label": ".getDescription()", "file_type": "code", "source_file": "migrations/Version20260804082035.php", "source_location": "L19"}, {"id": "migrations_version20260804082035_version20260804082035_up", "label": ".up()", "file_type": "code", "source_file": "migrations/Version20260804082035.php", "source_location": "L24"}, {"id": "schema", "label": "Schema", "file_type": "code", "source_file": "migrations/Version20260804082035.php", "source_location": "L24"}, {"id": "migrations_version20260804082035_version20260804082035_down", "label": ".down()", "file_type": "code", "source_file": "migrations/Version20260804082035.php", "source_location": "L43"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082035_php", "target": "schema", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082035_php", "target": "abstractmigration", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L8", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082035_php", "target": "migrations_version20260804082035_version20260804082035", "relation": "contains", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L17", "weight": 1.0}, {"source": "migrations_version20260804082035_version20260804082035", "target": "abstractmigration", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L17", "weight": 1.0}, {"source": "migrations_version20260804082035_version20260804082035", "target": "migrations_version20260804082035_version20260804082035_getdescription", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L19", "weight": 1.0}, {"source": "migrations_version20260804082035_version20260804082035", "target": "migrations_version20260804082035_version20260804082035_up", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L24", "weight": 1.0}, {"source": "migrations_version20260804082035_version20260804082035_up", "target": "schema", "relation": "references", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L24", "weight": 1.0, "context": "parameter_type"}, {"source": "migrations_version20260804082035_version20260804082035", "target": "migrations_version20260804082035_version20260804082035_down", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L43", "weight": 1.0}, {"source": "migrations_version20260804082035_version20260804082035_down", "target": "schema", "relation": "references", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082035.php", "source_location": "L43", "weight": 1.0, "context": "parameter_type"}], "raw_calls": [{"caller_nid": "migrations_version20260804082035_version20260804082035_up", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260804082035.php", "source_location": "L26", "receiver": null}, {"caller_nid": "migrations_version20260804082035_version20260804082035_down", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260804082035.php", "source_location": "L45", "receiver": null}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "label": "ErrorCodes.php", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L1"}, {"id": "constant_errorcodes_errorcodes", "label": "ErrorCodes", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5"}, {"id": "constant_errorcodes_errorcodes_message", "label": ".message()", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L136"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "target": "constant_errorcodes_errorcodes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5", "weight": 1.0}, {"source": "constant_errorcodes_errorcodes", "target": "constant_errorcodes_errorcodes_message", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L136", "weight": 1.0}], "raw_calls": []}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0002_treatment_areas_are_snapshotted_md", "label": "0002-treatment-areas-are-snapshotted.md", "file_type": "document", "source_file": "docs/adr/0002-treatment-areas-are-snapshotted.md", "source_location": "L1"}, {"id": "adr_0002_treatment_areas_are_snapshotted_a_treatment_case_snapshots_its_areas", "label": "A Treatment Case snapshots its areas", "file_type": "document", "source_file": "docs/adr/0002-treatment-areas-are-snapshotted.md", "source_location": "L1"}, {"id": "adr_0002_treatment_areas_are_snapshotted_consequences", "label": "Consequences", "file_type": "document", "source_file": "docs/adr/0002-treatment-areas-are-snapshotted.md", "source_location": "L8"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0002_treatment_areas_are_snapshotted_md", "target": "adr_0002_treatment_areas_are_snapshotted_a_treatment_case_snapshots_its_areas", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0002-treatment-areas-are-snapshotted.md", "source_location": "L1", "weight": 1.0}, {"source": "adr_0002_treatment_areas_are_snapshotted_a_treatment_case_snapshots_its_areas", "target": "adr_0002_treatment_areas_are_snapshotted_consequences", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0002-treatment-areas-are-snapshotted.md", "source_location": "L8", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082400_php", "label": "Version20260804082400.php", "file_type": "code", "source_file": "migrations/Version20260804082400.php", "source_location": "L1"}, {"id": "migrations_version20260804082400_version20260804082400", "label": "Version20260804082400", "file_type": "code", "source_file": "migrations/Version20260804082400.php", "source_location": "L17"}, {"id": "abstractmigration", "label": "AbstractMigration", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "migrations_version20260804082400_version20260804082400_getdescription", "label": ".getDescription()", "file_type": "code", "source_file": "migrations/Version20260804082400.php", "source_location": "L19"}, {"id": "migrations_version20260804082400_version20260804082400_up", "label": ".up()", "file_type": "code", "source_file": "migrations/Version20260804082400.php", "source_location": "L24"}, {"id": "schema", "label": "Schema", "file_type": "code", "source_file": "migrations/Version20260804082400.php", "source_location": "L24"}, {"id": "migrations_version20260804082400_version20260804082400_down", "label": ".down()", "file_type": "code", "source_file": "migrations/Version20260804082400.php", "source_location": "L29"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082400_php", "target": "schema", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082400_php", "target": "abstractmigration", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L8", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260804082400_php", "target": "migrations_version20260804082400_version20260804082400", "relation": "contains", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L17", "weight": 1.0}, {"source": "migrations_version20260804082400_version20260804082400", "target": "abstractmigration", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L17", "weight": 1.0}, {"source": "migrations_version20260804082400_version20260804082400", "target": "migrations_version20260804082400_version20260804082400_getdescription", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L19", "weight": 1.0}, {"source": "migrations_version20260804082400_version20260804082400", "target": "migrations_version20260804082400_version20260804082400_up", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L24", "weight": 1.0}, {"source": "migrations_version20260804082400_version20260804082400_up", "target": "schema", "relation": "references", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L24", "weight": 1.0, "context": "parameter_type"}, {"source": "migrations_version20260804082400_version20260804082400", "target": "migrations_version20260804082400_version20260804082400_down", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L29", "weight": 1.0}, {"source": "migrations_version20260804082400_version20260804082400_down", "target": "schema", "relation": "references", "confidence": "EXTRACTED", "source_file": "migrations/Version20260804082400.php", "source_location": "L29", "weight": 1.0, "context": "parameter_type"}], "raw_calls": [{"caller_nid": "migrations_version20260804082400_version20260804082400_up", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260804082400.php", "source_location": "L26", "receiver": null}, {"caller_nid": "migrations_version20260804082400_version20260804082400_down", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260804082400.php", "source_location": "L31", "receiver": null}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0006_clinical_record_is_separate_from_the_visit_record_md", "label": "0006-clinical-record-is-separate-from-the-visit-record.md", "file_type": "document", "source_file": "docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md", "source_location": "L1"}, {"id": "adr_0006_clinical_record_is_separate_from_the_visit_record_the_clinical_record_of_a_session_is_separate_from_its_visit_record", "label": "The clinical record of a session is separate from its visit record", "file_type": "document", "source_file": "docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md", "source_location": "L1"}, {"id": "adr_0006_clinical_record_is_separate_from_the_visit_record_consequences", "label": "Consequences", "file_type": "document", "source_file": "docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md", "source_location": "L10"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0006_clinical_record_is_separate_from_the_visit_record_md", "target": "adr_0006_clinical_record_is_separate_from_the_visit_record_the_clinical_record_of_a_session_is_separate_from_its_visit_record", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md", "source_location": "L1", "weight": 1.0}, {"source": "adr_0006_clinical_record_is_separate_from_the_visit_record_the_clinical_record_of_a_session_is_separate_from_its_visit_record", "target": "adr_0006_clinical_record_is_separate_from_the_visit_record_consequences", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0006-clinical-record-is-separate-from-the-visit-record.md", "source_location": "L10", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_md", "label": "0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "file_type": "document", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L1"}, {"id": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_resource_backed_appointments_are_guarded_by_occupancy_not_the_doctor_slot_key", "label": "Resource-backed appointments are guarded by occupancy, not the doctor slot key", "file_type": "document", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L1"}, {"id": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_considered_options", "label": "Considered Options", "file_type": "document", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L10"}, {"id": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_consequences", "label": "Consequences", "file_type": "document", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L16"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_md", "target": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_resource_backed_appointments_are_guarded_by_occupancy_not_the_doctor_slot_key", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L1", "weight": 1.0}, {"source": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_resource_backed_appointments_are_guarded_by_occupancy_not_the_doctor_slot_key", "target": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_considered_options", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L10", "weight": 1.0}, {"source": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_resource_backed_appointments_are_guarded_by_occupancy_not_the_doctor_slot_key", "target": "adr_0003_resource_backed_appointments_drop_the_doctor_slot_key_consequences", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/adr/0003-resource-backed-appointments-drop-the-doctor-slot-key.md", "source_location": "L16", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+13320 -2686
View File
File diff suppressed because it is too large Load Diff
+526 -336
View File
File diff suppressed because it is too large Load Diff