feat(representation): expose doctor representation details and add counts in admin views
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# نمایش تعلق دکتر به نماینده + صفحهی نماینده با تعداد دکتر و نوبت
|
||||
|
||||
## زمینه
|
||||
|
||||
هر دکتر از طریق ستون `doctors.representation_id` (FK به `representations.id`) به یک نماینده وصل میشود؛ این ستون **از قبل وجود دارد** و هنگام ثبتِ دکتر توسط نماینده مقداردهی میشود (`RepresentationActionController::registerDoctor` → `setRepresentationId`). اما این تعلق **هیچجا در پنل ادمین دیده نمیشود**: نه در لیست دکترها، نه در پروفایل دکتر، و صفحهی نمایندهها تعداد دکتر و نوبت نماینده را نشان نمیدهد. هدف: مرئیکردن این رابطه در ادمین. **نیازی به migration نیست** — فقط expose و UI.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
1. در لیست و پروفایل دکترِ ادمین، مشخص باشد دکتر به کدام نماینده تعلق دارد (نام + لینک به نماینده).
|
||||
2. در صفحهی نمایندهها (`RepresentationsPage`) هر نماینده ستون «تعداد پزشکان» و «تعداد نوبت» داشته باشد.
|
||||
3. در جزئیات نماینده (`RepresentationDetailPage`) بشود پزشکان زیرمجموعه و نوبتهای آن نماینده را دید.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `src/Doctor/Entity/Doctor.php` | `representationId` (ستون L76-77، getter L217)؛ `toDetailArray()` L530 — الان نماینده را برنمیگرداند |
|
||||
| `src/Doctor/Controller/DoctorController.php` | `show()` L149-173 — `GET /api/v1/doctor/{uuid}`؛ پنل ادمینِ پروفایل دکتر از همین میخواند (`DoctorDetailPage.tsx:2363`) |
|
||||
| `src/Admin/Controller/AdminApiController.php` | `doctorsList` (SELECT L350-359)؛ `representations` (L1016-1082) |
|
||||
| `src/Representation/Repository/RepresentationRepository.php` | برای resolve نامِ نماینده از روی id |
|
||||
| `assets/admin/pages/DoctorsPage.tsx` | لیست دکتر ادمین (fetch L144) |
|
||||
| `assets/admin/pages/DoctorDetailPage.tsx` | پروفایل دکتر ادمین (fetch L2358-2364) |
|
||||
| `assets/admin/pages/RepresentationsPage.tsx` | لیست نمایندهها (fetch L51-56) |
|
||||
| `assets/admin/pages/RepresentationDetailPage.tsx` | جزئیات نماینده (fetch `/api/v1/representation/{uuid}` L42) |
|
||||
| `docs/api/admin.md`, `docs/api/doctor.md` | مستندسازی |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
**الگوی aggregateِ آماده (برای adaptation)** — شمارش دکتر نماینده (`RepresentationActionController.php:606`):
|
||||
```php
|
||||
$total = (int) $conn->fetchOne('SELECT COUNT(*) FROM doctors WHERE representation_id = ?', [$repId]);
|
||||
```
|
||||
نوبتهای نماینده (`RepresentationActionController.php:673` تقریبی): `JOIN a.doctor d WHERE d.representationId = :repId`.
|
||||
|
||||
**لیست دکتر ادمین** الان نماینده را select نمیکند (`AdminApiController.php:350-359`):
|
||||
```php
|
||||
$rows = $conn->fetchAllAssociative(
|
||||
"SELECT d.id, d.uuid, d.name, ..., d.owner_status, d.source,
|
||||
u.mobile_number as user_mobile, u.email
|
||||
FROM doctors d JOIN users u ON u.id = d.user_id
|
||||
WHERE $whereStr ORDER BY $orderBy LIMIT $limit OFFSET $offset", $params);
|
||||
```
|
||||
|
||||
**لیست نمایندهها** بدون تعداد دکتر/نوبت (`AdminApiController.php:1063-1078`):
|
||||
```php
|
||||
return [
|
||||
'id' => (int) $r['id'], ...,
|
||||
'commission_percent' => (float) $r['commissionPercent'],
|
||||
'wallet_balance' => 0,
|
||||
'is_active' => (bool) $r['active'],
|
||||
'created_at' => date('c', (int) $r['createdAt']),
|
||||
];
|
||||
```
|
||||
|
||||
**پروفایل دکترِ ادمین** از `toDetailArray()` تغذیه میشود که `representation` ندارد.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. Backend — expose نماینده روی جزئیات دکتر (`show()`)
|
||||
|
||||
در `DoctorController::show()` بعد از ساخت آرایهی دکتر، نماینده را از روی `representation_id` resolve کن و merge کن (مثل الگوی `clinics` که همانجا merge میشود). `null` اگر دکتر نماینده ندارد (مثل ایمپورتهای IRIMC):
|
||||
```php
|
||||
$repId = $doctor->getRepresentationId();
|
||||
$rep = $repId ? $this->representationRepo->find($repId) : null;
|
||||
$data = array_merge($doctor->toDetailArray($schedule), [
|
||||
'clinics' => $clinicData,
|
||||
'representation' => $rep ? ['id' => $rep->getId(), 'uuid' => $rep->getUuid(), 'full_name' => $rep->getFullName()] : null,
|
||||
]);
|
||||
```
|
||||
`RepresentationRepository` را به constructor تزریق کن (اگر نیست). پاسخ همچنان double-nested است؛ شکل را نشکن.
|
||||
|
||||
### ۲. Backend — لیست دکتر ادمین: افزودن نماینده
|
||||
|
||||
در `AdminApiController::doctorsList` کوئری را `LEFT JOIN representations` کن و دو ستون اضافه کن:
|
||||
```php
|
||||
"SELECT d.id, ..., d.owner_status, d.source,
|
||||
d.representation_id, rep.full_name AS representation_name,
|
||||
u.mobile_number as user_mobile, u.email
|
||||
FROM doctors d JOIN users u ON u.id = d.user_id
|
||||
LEFT JOIN representations rep ON rep.id = d.representation_id
|
||||
WHERE $whereStr ORDER BY $orderBy LIMIT $limit OFFSET $offset"
|
||||
```
|
||||
و در `array_map` خروجی:
|
||||
```php
|
||||
'representation_id' => $d['representation_id'] !== null ? (int) $d['representation_id'] : null,
|
||||
'representation_name' => $d['representation_name'] ?? null,
|
||||
```
|
||||
|
||||
### ۳. Backend — لیست نمایندهها: تعداد دکتر + نوبت
|
||||
|
||||
در `AdminApiController::representations` بعد از گرفتن `$rows` و `$repIds`، دو شمارش گروهی با یک کوئری هرکدام بگیر (نه N+1):
|
||||
```php
|
||||
$docCounts = []; // representation_id => count
|
||||
$apptCounts = []; // representation_id => count
|
||||
if ($repIds !== []) {
|
||||
foreach ($this->em->getConnection()->fetchAllAssociative(
|
||||
'SELECT representation_id, COUNT(*) c FROM doctors
|
||||
WHERE representation_id IN (?) GROUP BY representation_id',
|
||||
[$repIds], [\Doctrine\DBAL\ArrayParameterType::INTEGER]) as $row) {
|
||||
$docCounts[(int) $row['representation_id']] = (int) $row['c'];
|
||||
}
|
||||
foreach ($this->em->getConnection()->fetchAllAssociative(
|
||||
'SELECT d.representation_id, COUNT(*) c FROM appointments a
|
||||
JOIN doctors d ON d.id = a.doctor_id
|
||||
WHERE d.representation_id IN (?) GROUP BY d.representation_id',
|
||||
[$repIds], [\Doctrine\DBAL\ArrayParameterType::INTEGER]) as $row) {
|
||||
$apptCounts[(int) $row['representation_id']] = (int) $row['c'];
|
||||
}
|
||||
}
|
||||
```
|
||||
و در آیتم خروجی:
|
||||
```php
|
||||
'doctor_count' => $docCounts[(int) $r['id']] ?? 0,
|
||||
'appointment_count' => $apptCounts[(int) $r['id']] ?? 0,
|
||||
```
|
||||
|
||||
### ۴. Backend — endpoint پزشکان یک نماینده (برای صفحهی جزئیات)
|
||||
|
||||
یک endpoint ادمین اضافه کن: `GET /api/v1/admin/representations/{uuid}/doctors` (paginated) که پزشکان `representation_id = rep.id` را برمیگرداند (همان الگوی `d.representation_id = :repId`). `$this->paginated(...)`. برای نوبتها میتوانی از `appointment_count` تجمیعی وظیفهی ۳ در خود صفحه استفاده کنی، یا در صورت نیاز endpoint مشابه `.../appointments` بساز.
|
||||
|
||||
### ۵. Frontend — ستون «نماینده» در لیست و کارت در پروفایل دکتر
|
||||
|
||||
- `DoctorsPage.tsx`: به `AdminDoctor` تایپ `representation_id`/`representation_name` اضافه کن و یک ستون «نماینده» در `<DataTable>` نشان بده؛ اگر null → «بدون نماینده». (فقط حالت ادمین؛ نقش representation خودش زیرمجموعه است.)
|
||||
- `DoctorDetailPage.tsx`: یک بخش/کارت «نماینده» با نام و لینک به `/representations/${representation.uuid}` از `data.representation` بساز؛ اگر null → «این پزشک به نمایندهای متصل نیست».
|
||||
|
||||
### ۶. Frontend — ستونهای نماینده + بخش پزشکان/نوبتها
|
||||
|
||||
- `RepresentationsPage.tsx`: دو ستون «تعداد پزشکان» (`doctor_count`) و «تعداد نوبت» (`appointment_count`) در جدول.
|
||||
- `RepresentationDetailPage.tsx`: یک بخش «پزشکان این نماینده» با `useQuery` روی `/api/v1/admin/representations/${uuid}/doctors` (paginated: items از `data?.data`) و نمایش `appointment_count`/`doctor_count`.
|
||||
|
||||
### ۷. مستندسازی
|
||||
|
||||
- `docs/api/admin.md`: فیلدهای جدید `doctor_count`/`appointment_count` در پاسخ `representations`؛ فیلدهای `representation_id`/`representation_name` در `doctors`؛ endpoint جدید `GET /api/v1/admin/representations/{uuid}/doctors`.
|
||||
- `docs/api/doctor.md`: فیلد `representation` در پاسخ single (`show`).
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **بدون migration**: `representation_id` از قبل روی `doctors` هست (Doctor.php:76). فقط expose.
|
||||
- **ایمپورتهای IRIMC** `representation_id = NULL` دارند (فقط `managed_by` ست میشود، `DoctorImportService.php:106`) — در UI «بدون نماینده» نشان بده؛ **auto-assign نکن** (خارج از دامنهی این تسک).
|
||||
- **پاسخها**: paginated → items از `data?.data`، total از `data?.meta?.totalRecords`؛ single (`show`) → double-nested `data?.data?.data`. همهی endpointها از `BaseController` (`success`/`paginated`).
|
||||
- شمارشها گروهی (`GROUP BY ... IN (?)`) باشند تا N+1 نشود؛ همان الگوی `cityNames` موجود در `representations` (AdminApiController.php:1048).
|
||||
- `show()` عمومی است و توسط `nobat724_front` هم مصرف میشود؛ افزودن `representation` مشکلی ایجاد نمیکند (سایت آن را نادیده میگیرد) ولی شکل پاسخ را تغییر نده.
|
||||
- تاریخها Unix timestamp؛ نمایش با `formatDate()` شمسی.
|
||||
- تست: `ddev exec php -l ...`، `ddev exec php bin/console cache:clear`، `ddev exec yarn dev`، `ddev exec php bin/console debug:router | grep representations`.
|
||||
Reference in New Issue
Block a user