Files
clinicpro/.claude/prompt/doctor-representation-visibility.md

10 KiB
Raw Permalink Blame History

نمایش تعلق دکتر به نماینده + صفحه‌ی نماینده با تعداد دکتر و نوبت

زمینه

هر دکتر از طریق ستون doctors.representation_id (FK به representations.id) به یک نماینده وصل می‌شود؛ این ستون از قبل وجود دارد و هنگام ثبتِ دکتر توسط نماینده مقداردهی می‌شود (RepresentationActionController::registerDoctorsetRepresentationId). اما این تعلق هیچ‌جا در پنل ادمین دیده نمی‌شود: نه در لیست دکترها، نه در پروفایل دکتر، و صفحه‌ی نماینده‌ها تعداد دکتر و نوبت نماینده را نشان نمی‌دهد. هدف: مرئی‌کردن این رابطه در ادمین. نیازی به 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):

$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):

$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):

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):

$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 کن و دو ستون اضافه کن:

"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 خروجی:

'representation_id'   => $d['representation_id'] !== null ? (int) $d['representation_id'] : null,
'representation_name' => $d['representation_name'] ?? null,

۳. Backend — لیست نماینده‌ها: تعداد دکتر + نوبت

در AdminApiController::representations بعد از گرفتن $rows و $repIds، دو شمارش گروهی با یک کوئری هرکدام بگیر (نه N+1):

$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'];
    }
}

و در آیتم خروجی:

'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.