feat(treatment): filter treatment cases by patient record
The list could be narrowed by status, search and open-date, but not by patient — so a patient's own file had no way to ask which courses belong to them. `?record=` adds that bound. patientRecord is joined once and shared with the search branch; joining it twice under the same alias is a DQL error, and search already needed it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
# تب «نوبتهای بعدی» در پروندهٔ بیمار + اصلاح واژهٔ پرسنل
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (backend + پنل ادمین). cross-repo نیست.
|
||||
|
||||
## زمینه
|
||||
|
||||
درمان چندجلسهای امروز فقط از یک صفحهٔ سراسری دیده میشود:
|
||||
`/admin/treatment-cases`. آنجا فهرست همهٔ پروندههای محیط است، نه پروندهٔ یک بیمار.
|
||||
|
||||
نتیجه: منشی که پروندهٔ یک بیمار را باز کرده (`/admin/patients/{uuid}`) هیچ راهی ندارد
|
||||
ببیند این بیمار چه دورههایی دارد، جلسهٔ بعدش کِی است، و جلسات قبلی چه چیزی ثبت کردهاند.
|
||||
|
||||
`TreatmentScheduler` هم فقط سررسید **جلسهٔ بعدی** را مینویسد و بقیه `null` میمانند
|
||||
(تصمیم عمدی؛ کامنت خودِ کلاس). پس هیچجا نمیشود کل تقویم یک دوره را دید.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
سه چیز:
|
||||
|
||||
1. **تب «نوبتهای بعدی» در پروندهٔ بیمار.** دورههای همان بیمار، و برای هر دوره کارتِ
|
||||
همهٔ جلسات با تاریخ و ساعتِ محاسبهشده. هر کارت دکمهٔ «ثبت نوبت» دارد که همان مودال
|
||||
`NewAppointmentModal` را با همان تاریخ/ساعت باز میکند و کاربر میتواند تاریخ و ساعت
|
||||
دیگری هم بگذارد.
|
||||
2. **دیدن جزئیات انجامشده.** برای جلسات تمامشده باید معلوم باشد چه کسی، روی چه
|
||||
ناحیهای، با چه دستگاهی و با چه خواندههایی (انرژی/پالس/شات) کار کرده.
|
||||
3. **واژهٔ «اپراتور» → «پرسنل»** در رشتههای کاربرپسند، و پیشفرض شدن پرسنلِ پروتکل در
|
||||
مودال ثبت نوبت.
|
||||
|
||||
---
|
||||
|
||||
## ⚠ تصمیم معماری که باید قبل از کد روشن باشد
|
||||
|
||||
خواستهٔ «همهٔ نوبتها را بر اساس طول درمان محاسبه کن» با یک تصمیم ثبتشدهٔ پروژه در
|
||||
تضاد ظاهری است. متن خودِ `TreatmentScheduler`:
|
||||
|
||||
> فقط جلسهٔ **بعدی** بازمحاسبه میشود. جلسات دورتر حدسِ قبلیشان را نگه میدارند تا
|
||||
> نوبتشان برسد — عددی که هنوز به هیچ واقعیتی گره نخورده، بازمحاسبهاش دقیقترش نمیکند.
|
||||
|
||||
**راهحل انتخابی: محاسبه بشود ولی ذخیره نشود.**
|
||||
|
||||
- یک سرویس **فقطخواندنی** تقویم کل دوره را از روی آخرین لنگرِ واقعی میسازد.
|
||||
- `TreatmentSession.due_at` دستنخورده میماند و همچنان فقط جلسهٔ بعدی را نگه میدارد.
|
||||
- در UI، جلسهای که `due_at` واقعی دارد با جلسهای که فقط تخمین است **متفاوت** نشان
|
||||
داده میشود.
|
||||
|
||||
**چرا این و نه ذخیره کردن همه:** اگر همهٔ سررسیدها ذخیره شوند، هر بار که بیمار دیر
|
||||
میآید باید کل زنجیره بازنویسی شود و هر نسخهٔ ذخیرهشده یک ادعای غلط دربارهٔ آینده است.
|
||||
تخمینِ محاسبهشده در لحظهٔ نمایش، همیشه با آخرین واقعیت همخوان است و چیزی برای
|
||||
نگهداشتن ندارد.
|
||||
|
||||
**ریسک این انتخاب:** تاریخهای کارت با هر بار باز کردن صفحه ممکن است عوض شوند (اگر
|
||||
جلسهای بین دو بازدید تمام شده باشد). این پذیرفته است و باید در UI با برچسب «تخمینی»
|
||||
اعلام شود.
|
||||
|
||||
**گزینهٔ جایگزینی که رد شد:** ساختن نوبتهای `pending` برای کل دوره. رد شد چون جای
|
||||
دستگاه را میگیرد و بیمارِ نیامده وقت منبع را میسوزاند — همان دلیلی که «رزرو خودکار»
|
||||
قبلاً رد شده بود.
|
||||
|
||||
---
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ موفق: `GET /api/v1/treatment-cases?record=<record_uuid>` با توکن منشی → ۲۰۰ و فقط
|
||||
پروندههای همان بیمار. `GET /api/v1/treatment-case/{uuid}/plan` → ۲۰۰ با آرایهٔ
|
||||
جلسات که هر کدام `planned_at` و `is_estimate` دارند.
|
||||
- ✅ موفق: در `/admin/patients/{uuid}?tab=treatment` کارت هر جلسه تاریخ و ساعت شمسی
|
||||
نشان میدهد و دکمهٔ «ثبت نوبت» مودال `NewAppointmentModal` را با همان تاریخ/ساعت
|
||||
و همان بیمار و سرویس باز میکند.
|
||||
- ✅ موفق: در مودال ثبت نوبت، وقتی سرویسی با پروتکل فعال انتخاب میشود، فیلد «پرسنل»
|
||||
از `TreatmentProtocol.staff` پیشفرض پر میشود.
|
||||
- ❌ خطا: `GET /api/v1/treatment-case/{uuid}/plan` برای پروندهٔ محیط دیگر → ۴۰۴ با
|
||||
envelope خطا. `?record=` با uuid ناموجود → آرایهٔ خالی، نه ۵۰۰.
|
||||
- ⚠️ مرزی: بیمارِ بدون هیچ دوره → تب «نوبتهای بعدی» حالت خالیِ طراحیشده دارد، نه
|
||||
اسکلتون بیپایان.
|
||||
- ⚠️ مرزی: دورهای که همهٔ جلساتش تمام شده → کارتها همه «انجامشده» با جزئیات، و هیچ
|
||||
دکمهٔ «ثبت نوبت» فعالی ندارند.
|
||||
- ⚠️ مرزی: جلسهای که نوبت دارد → دکمهاش «ثبت نوبت» نیست؛ نوبت موجود نشان داده میشود.
|
||||
- ⚠️ مرزی: پروتکلی که هیچ پرسنلی ندارد → فیلد «پرسنل» خالی میماند و فرم قفل نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Treatment/Controller/TreatmentCaseController.php` | فهرست و جزئیات پرونده؛ اندپوینتهای جدید اینجا |
|
||||
| `src/Treatment/Repository/TreatmentCaseRepository.php` | `findForTenant` — فیلتر `record` اینجا اضافه میشود |
|
||||
| `src/Treatment/Service/TreatmentScheduler.php` | محاسبهٔ سررسید؛ **دستنخورده میماند** |
|
||||
| `src/Treatment/Entity/TreatmentCase.php` | `toArray(withSessions)` — امروز نواحی را نمیفرستد |
|
||||
| `src/Treatment/Entity/TreatmentSession.php` | `toArray(withAreas)` |
|
||||
| `src/Treatment/Entity/SessionAreaRecord.php` | خواندههای دستگاه — منبع «ورکفلوی انجامشده» |
|
||||
| `assets/admin/pages/PatientDetailPage.tsx` | تبهای پروندهٔ بیمار |
|
||||
| `assets/admin/components/appointments/NewAppointmentModal.tsx` | مودال ثبت نوبت |
|
||||
| `assets/admin/components/TreatmentCaseEditModal.tsx` | لیبل «اپراتور» |
|
||||
| `assets/admin/pages/TreatmentCasesPage.tsx` | رشتهٔ «اپراتور:» |
|
||||
| `assets/admin/pages/StaffSessionDetailPage.tsx` | رشتهٔ «اپراتور:» |
|
||||
| `docs/api/treatment.md` | سند اندپوینتها |
|
||||
|
||||
---
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
### فیلتر پرونده بر اساس بیمار وجود ندارد
|
||||
|
||||
`TreatmentCaseRepository::findForTenant` فقط محیط، وضعیت، جستجو و بازهٔ تاریخ دارد:
|
||||
|
||||
```php
|
||||
public function findForTenant(
|
||||
string $entityType,
|
||||
int $entityId,
|
||||
?string $status = null,
|
||||
?string $q = null,
|
||||
?int $openedFrom = null,
|
||||
?int $openedTo = null,
|
||||
): array {
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
->where('c.entityType = :type')
|
||||
->andWhere('c.entityId = :id')
|
||||
// ...
|
||||
```
|
||||
|
||||
### جزئیات پرونده نواحی جلسات را نمیفرستد
|
||||
|
||||
`TreatmentCase::toArray()`:
|
||||
|
||||
```php
|
||||
if ($withSessions) {
|
||||
$data['sessions'] = array_map(
|
||||
static fn (TreatmentSession $s): array => $s->toArray(),
|
||||
$this->sessions->toArray(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`TreatmentSession::toArray(bool $withAreas = false)` نواحی را فقط با آرگومان میدهد و
|
||||
اینجا فرستاده نمیشود. پس «چه کاری روی چه ناحیهای انجام شد» در پاسخ نیست.
|
||||
|
||||
### تبهای پروندهٔ بیمار
|
||||
|
||||
```tsx
|
||||
type TabKey = 'services' | 'info' | 'appointments' | 'payments' | 'wallet' | 'notes' | 'callcenter' | 'attach' | 'records';
|
||||
|
||||
const TABS: { key: TabKey; label: string; icon: (c: string) => React.ReactNode }[] = [
|
||||
{ key: 'services', label: 'سرویسها', icon: (c) => <TabServices color={c} /> },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
### فیلد پرسنل در مودال ثبت نوبت — بدون پیشفرض
|
||||
|
||||
```tsx
|
||||
// اپراتور اختیاری است: خالی گذاشتنش جلسه را در صفِ مشترکِ پرسنلِ مجاز میگذارد،
|
||||
// پر کردنش آن را از قبل به یک نفر میدهد.
|
||||
const [staffUuid, setStaffUuid] = useState('');
|
||||
```
|
||||
|
||||
```tsx
|
||||
<label htmlFor="appt-operator">اپراتور <span className="opt">(اختیاری)</span></label>
|
||||
```
|
||||
|
||||
### پروتکل، پرسنل مجاز را از قبل میفرستد
|
||||
|
||||
`TreatmentProtocol::toArray()`:
|
||||
|
||||
```php
|
||||
'staff' => array_map(
|
||||
static fn (TreatmentProtocolStaff $m): array => $m->toArray(),
|
||||
$this->allowedStaff->toArray(),
|
||||
),
|
||||
```
|
||||
|
||||
یعنی دادهٔ لازم برای پیشفرض هست؛ فقط مصرف نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. فیلتر پرونده بر اساس بیمار
|
||||
|
||||
به `findForTenant` پارامتر `?string $recordUuid = null` اضافه کن و در کنترلر از
|
||||
`?record=` بخوان.
|
||||
|
||||
```php
|
||||
if ($recordUuid !== null && $recordUuid !== '') {
|
||||
$qb->join('c.patientRecord', 'prf')
|
||||
->andWhere('prf.uuid = :record')
|
||||
->setParameter('record', $recordUuid);
|
||||
}
|
||||
```
|
||||
|
||||
> اگر `q` هم فرستاده شده باشد، `patientRecord` قبلاً join شده — از alias تکراری
|
||||
> پرهیز کن (یک join مشترک، نه دو تا).
|
||||
|
||||
**نحوه تست:**
|
||||
```bash
|
||||
TOKEN=... # 09390039833 / QaTest@1234
|
||||
curl -sk "https://clinic-pro.ddev.site/api/v1/treatment-cases?record=<record_uuid>" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
باید فقط پروندههای همان بیمار برگردد. با `record=` نامعتبر → آرایهٔ خالی.
|
||||
تست PHPUnit در `tests/Treatment/TreatmentCaseEditTest.php` کنار تستهای `findForTenant`.
|
||||
|
||||
---
|
||||
|
||||
### ۲. سرویس تقویم دوره (فقطخواندنی)
|
||||
|
||||
`src/Treatment/Service/TreatmentPlanProjector.php`
|
||||
|
||||
مسئولیت واحد: از روی جلسات یک پرونده، تاریخ **همهٔ** جلسات را بساز.
|
||||
|
||||
قاعده:
|
||||
- جلسهٔ انجامشده → `planned_at = finished_at`، `is_estimate = false`
|
||||
- جلسهای که نوبت دارد → `planned_at = appointment.slot_start`، `is_estimate = false`
|
||||
- جلسهای که `due_at` دارد → `planned_at = due_at`، `is_estimate = false`
|
||||
- بقیه → از آخرین لنگر به بعد، با جمع زدن `offset_days` گامها، `is_estimate = true`
|
||||
|
||||
```php
|
||||
final class TreatmentPlanProjector
|
||||
{
|
||||
private const DAY = 86400;
|
||||
|
||||
/** @return list<array{session: TreatmentSession, planned_at: ?int, is_estimate: bool}> */
|
||||
public function project(TreatmentCase $case): array
|
||||
{
|
||||
// جلسات به ترتیب شماره؛ لنگر = آخرین زمان قطعیِ دیدهشده
|
||||
// برای هر جلسهٔ بیلنگر: anchor += offsetDays(stepNumber) * DAY
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**چرا سرویس جدا و نه متد روی `TreatmentScheduler`:** آن کلاس *مینویسد* و این فقط
|
||||
*میخواند*. قاطی کردنشان یعنی یک کلاس با دو مسئولیت و ریسک اینکه تخمین اشتباهی
|
||||
ذخیره شود (guidelines §۵، اصل S).
|
||||
|
||||
**نحوه تست:** تست واحد در `tests/Treatment/` — پروندهای با ۴ جلسه بساز، جلسهٔ ۱ را
|
||||
تمام کن، و بررسی کن جلسهٔ ۲ `is_estimate=false` (چون `due_at` گرفته) و جلسات ۳ و ۴
|
||||
`is_estimate=true` با فاصلهٔ درستِ گامهایشان باشند. حالت مرزی: پروندهای که هیچ جلسهٔ
|
||||
تمامشدهای ندارد و نوبت هم ندارد → همه `is_estimate=true` با لنگرِ `opened_at`.
|
||||
|
||||
---
|
||||
|
||||
### ۳. اندپوینت تقویم + جزئیات انجامشده
|
||||
|
||||
`GET /api/v1/treatment-case/{uuid}/plan`
|
||||
|
||||
پاسخ برای هر جلسه: `uuid`، `session_number`، `status`، `planned_at`، `is_estimate`،
|
||||
`appointment` (اگر دارد)، `performed_by`، و **`areas`** با `parameters`، `resource`،
|
||||
`note`، `started_at`، `finished_at`.
|
||||
|
||||
نواحی همان چیزی است که کاربر «ورکفلوی انجامشده» مینامد. `SessionAreaRecord::toArray()`
|
||||
از قبل همه را دارد؛ فقط باید `withAreas: true` فرستاده شود.
|
||||
|
||||
**تصمیم:** اندپوینت جدا از `GET /treatment-case/{uuid}` باشد، نه اضافه کردن به آن.
|
||||
دلیل: پاسخ فعلی مصرفکنندهٔ دیگری دارد (مودال ویرایش) که نواحی را لازم ندارد و
|
||||
بزرگترش کردن یعنی هزینهٔ بیمصرف روی همان مسیر.
|
||||
|
||||
**نحوه تست:**
|
||||
```bash
|
||||
curl -sk "https://clinic-pro.ddev.site/api/v1/treatment-case/<uuid>/plan" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
باید همهٔ جلسات با `planned_at` بیایند و جلسهٔ انجامشده `areas[].parameters` داشته باشد.
|
||||
پروندهٔ محیط دیگر → ۴۰۴.
|
||||
|
||||
---
|
||||
|
||||
### ۴. تب «نوبتهای بعدی» در پروندهٔ بیمار
|
||||
|
||||
`TabKey` را با `'treatment'` گسترش بده و به `TABS` اضافه کن.
|
||||
|
||||
کامپوننت جدید `assets/admin/components/patient/PatientTreatmentTab.tsx`:
|
||||
|
||||
- `GET /api/v1/treatment-cases?record={uuid}` → فهرست دورهها
|
||||
- برای دورهٔ باز، `GET /api/v1/treatment-case/{uuid}/plan`
|
||||
- هر جلسه یک کارت: شمارهٔ جلسه، تاریخ و ساعت شمسی (`formatDateTime`)، وضعیت با
|
||||
`StatusBadge type="treatment-session"`، و برچسب «تخمینی» وقتی `is_estimate`
|
||||
- جلسهٔ انجامشده: پرسنل + نواحی + خواندههای دستگاه
|
||||
- جلسهٔ بدون نوبت: دکمهٔ «ثبت نوبت»
|
||||
|
||||
**دکمهٔ «ثبت نوبت» باید همان `NewAppointmentModal` را باز کند** — نه صفحهٔ جدید و نه
|
||||
مودال تازه. همان مودالی که در `/admin/appointments?resource=…` استفاده میشود.
|
||||
|
||||
مودال این propها را میگیرد: `slot`، `services`، `date`، `clinicUuid`، `resource`.
|
||||
برای اینکه تاریخ و ساعتِ کارت پیشفرض شود، `date` را از `planned_at` بده.
|
||||
|
||||
> **این را قبل از کد بررسی کن:** مودال امروز `treatment_session_uuid` را نمیفرستد
|
||||
> (فقط صفحهٔ `AppointmentCreatePage` میفرستد). برای اینکه نوبت به همان جلسه بچسبد،
|
||||
> باید prop تازهای مثل `treatmentSessionUuid` به مودال اضافه شود و در payload برود.
|
||||
> بدون آن، اتصال دوباره به حدسِ سرویس میافتد — همان چیزی که در
|
||||
> `SessionBookingLink` صریح شد.
|
||||
|
||||
**نحوه تست UI:**
|
||||
```bash
|
||||
node .claude/skills/redesign-page/driver.mjs shot \
|
||||
"https://clinic-pro.ddev.site/admin/patients/<uuid>?tab=treatment" \
|
||||
--out /tmp/plan.png --full --wait 5000
|
||||
```
|
||||
سناریو: بیماری با دورهٔ باز → کارتها دیده شوند؛ کلیک روی «ثبت نوبت» → مودال با تاریخ
|
||||
همان کارت باز شود؛ ثبت → جلسه `booked` شود و کارت بهروز شود.
|
||||
|
||||
---
|
||||
|
||||
### ۵. پیشفرض شدن پرسنلِ پروتکل در مودال ثبت نوبت
|
||||
|
||||
در `NewAppointmentModal`، وقتی `pick.serviceUuids` تغییر میکند، پروتکل سرویس اول را
|
||||
بخوان و اگر `staff` دارد و کاربر هنوز دستی چیزی انتخاب نکرده، اولین پرسنل را بگذار.
|
||||
|
||||
```tsx
|
||||
const protocolQ = useQuery({
|
||||
queryKey: ['service-protocol', pick.serviceUuids[0]],
|
||||
queryFn: () => api.get(`/api/v1/service-item/${pick.serviceUuids[0]}/treatment-protocol`),
|
||||
enabled: pick.serviceUuids.length > 0,
|
||||
});
|
||||
```
|
||||
|
||||
**قاعده:** فقط وقتی پیشفرض بگذار که کاربر دست نزده باشد (یک فلگ `staffTouched`).
|
||||
وگرنه انتخاب دستیِ منشی با هر تغییر سرویس پاک میشود.
|
||||
|
||||
**نحوه تست:** vitest در `assets/admin/components/appointments/ResourceBookingModal.test.tsx` —
|
||||
mock کردن پاسخ پروتکل با یک پرسنل، انتخاب سرویس، و بررسی اینکه `staff_uuid` در payload
|
||||
همان است. حالت مرزی: پروتکل بدون پرسنل → فیلد خالی و ثبت همچنان ممکن.
|
||||
|
||||
---
|
||||
|
||||
### ۶. «اپراتور» → «پرسنل»
|
||||
|
||||
فقط رشتههای کاربرپسند. چهار مورد:
|
||||
|
||||
| فایل | خط | رشته |
|
||||
|------|----|------|
|
||||
| `assets/admin/components/appointments/NewAppointmentModal.tsx` | ۲۸۴ | `اپراتور (اختیاری)` |
|
||||
| `assets/admin/components/TreatmentCaseEditModal.tsx` | ۲۱۲ | `اپراتور` |
|
||||
| `assets/admin/pages/TreatmentCasesPage.tsx` | ۳۵۷ | `اپراتور: …` |
|
||||
| `assets/admin/pages/StaffSessionDetailPage.tsx` | ۱۱۶ | `اپراتور: …` |
|
||||
|
||||
**دست نزن به** `assets/admin/pages/ResourcesPage.tsx:107`:
|
||||
|
||||
```tsx
|
||||
description="هر چیزی که ممکن است اشغال باشد: پزشک، اپراتور، اتاق، دستگاه. ظرفیت یعنی تعداد بیمار همزمان."
|
||||
```
|
||||
|
||||
آنجا «اپراتور» یک **نوع منبع** است، نه رکورد `ClinicStaff`. عوض کردنش معنی جمله را
|
||||
خراب میکند. اگر مطمئن نیستی، از کاربر بپرس.
|
||||
|
||||
کامنتهای کد را هم میتوانی هماهنگ کنی ولی اولویت ندارد.
|
||||
|
||||
**نحوه تست:** `npx vitest run` — تستی که به متن «اپراتور» تکیه کرده باشد باید بهروز
|
||||
شود؛ بعد grep بزن که هیچ رشتهٔ کاربرپسندِ «اپراتور» جز مورد `ResourcesPage` نمانده باشد.
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **`TreatmentScheduler` تغییر نمیکند.** تقویم تخمینی فقط خوانده میشود و `due_at`
|
||||
هیچوقت از این مسیر نوشته نمیشود. اگر خودت را در حال `setDueAt` دیدی، از مسیر
|
||||
خارج شدهای.
|
||||
- الگوی بهکاررفته: **Projection/Read Model** — یک سرویس فقطخواندنی که از دادههای
|
||||
موجود یک نمای مشتق میسازد. دلیل انتخاب: نما با هر تغییرِ واقعیت خودش بهروز است و
|
||||
هیچ دادهای برای همگامسازی ندارد.
|
||||
- سه حالت داده (Loading / Empty / Error) برای تب جدید اجباری است — الگوی موجود در
|
||||
`TreatmentCasesPage.tsx` را ببین: خطای سرور نباید «دورهای ندارد» خوانده شود.
|
||||
- ارقام با `formatNumber` و تاریخها با `formatDateTime` (شمسی). ارقام لاتین وسط متن
|
||||
فارسی، نقصِ تکرارشوندهٔ این صفحهها بوده.
|
||||
- وضعیت تب در URL است (`?tab=treatment`) — `PatientDetailPage` از قبل `searchParams`
|
||||
را میخواند.
|
||||
- بعد از هر تغییر API، `docs/api/treatment.md` در همان جلسه بهروز شود.
|
||||
- تنانسی: هر دو اندپوینت جدید باید از `requireCase`/`branches->pair($user)` عبور کنند؛
|
||||
پروندهٔ محیط دیگر ۴۰۴ میگیرد نه ۴۰۳.
|
||||
- migration لازم **نیست** — هیچ Entity تغییر نمیکند.
|
||||
Reference in New Issue
Block a user