feat(representation): add endpoints for doctor statistics and toggling doctor status

This commit is contained in:
hamed
2026-06-24 12:24:53 +03:30
parent 8b419d0272
commit b7df8cf9ee
3 changed files with 113 additions and 4 deletions
+11 -4
View File
@@ -114,10 +114,14 @@ export default function DoctorsPage() {
// ── Queries ──
const statsQ = useQuery({
queryKey: ['doctors-stats'],
queryFn: () => api.get<ApiResponse<DoctorStats>>('/api/v1/admin/doctors/stats'),
queryKey: ['doctors-stats', isRepresentation],
queryFn: () => {
const url = isRepresentation
? '/api/v1/representation/doctors/stats'
: '/api/v1/admin/doctors/stats';
return api.get<ApiResponse<DoctorStats>>(url);
},
staleTime: 30_000,
enabled: !isRepresentation,
});
const specialtiesQ = useQuery({
@@ -163,7 +167,10 @@ export default function DoctorsPage() {
});
const toggleMut = useMutation({
mutationFn: (uuid: string) => api.post<ApiResponse<{ is_active: boolean }>>(`/api/v1/admin/doctors/${uuid}/status`, {}),
mutationFn: (uuid: string) => {
const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors';
return api.post<ApiResponse<{ is_active: boolean }>>(`${base}/${uuid}/status`, {});
},
onSuccess: () => {
toast.success('وضعیت پزشک تغییر کرد');
qc.invalidateQueries({ queryKey: ['admin-doctors'] });
+42
View File
@@ -398,6 +398,48 @@ Get yearly earnings dashboard for a representation.
---
### GET `/api/v1/representation/doctors/stats`
آمار پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (فقط `representation_id = نماینده‌ی کاربر جاری`). شکل پاسخ سازگار با `GET /api/v1/admin/doctors/stats` (بدون `top_specialty`). فرانت‌اند کارت‌های «کل پزشکان / فعال / غیرفعال / مرد / زن» را از این endpoint برای نقش نماینده پر می‌کند.
> **Permission:** `ROLE_REPRESENTATION` — id نماینده از `#[CurrentUser]`.
#### Response `200`
```json
{
"success": true,
"data": { "total": 12, "active": 9, "inactive": 3, "male": 7, "female": 5 }
}
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست |
---
### POST `/api/v1/representation/doctors/{uuid}/status`
فعال/غیرفعال کردن پزشکِ زیرمجموعه‌ی نماینده‌ی جاری (toggle `active_doctor_appointment`). فقط روی پزشکانی که `representation_id` آن‌ها برابر نماینده‌ی کاربر جاری است؛ در غیر این صورت 404.
> **Permission:** `ROLE_REPRESENTATION` — مالکیت از `#[CurrentUser]` چک می‌شود.
#### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
| `uuid` | string | uuid پزشک |
#### Response `200`
```json
{ "success": true, "data": { "is_active": false } }
```
#### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | کاربر جاری نماینده نیست، یا پزشک یافت نشد / متعلق به این نماینده نیست |
---
### GET `/api/v1/representation/clinics`
کلینیک‌های ثبت‌شده توسط نماینده‌ی جاری (فقط `representation_id = نماینده‌ی کاربر جاری`). شکل آیتم سازگار با `GET /api/v1/admin/clinics`.
@@ -391,4 +391,64 @@ class RepresentationActionController extends BaseController
return $this->paginated($items, (int) $total, $page, $limit);
}
#[OA\Get(
path: '/api/v1/representation/doctors/stats',
summary: 'آمار پزشکانِ ثبت‌شده توسط نماینده‌ی جاری (شکلِ یکسان با /admin/doctors/stats)',
security: [['bearerAuth' => []]],
responses: [new OA\Response(response: 200, description: 'آمار پزشکان نماینده')]
)]
#[Route('/api/v1/representation/doctors/stats', methods: ['GET'])]
public function doctorStats(#[CurrentUser] User $user): JsonResponse
{
$rep = $this->representationRepo->findByUser($user);
if ($rep === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده‌ای برای این کاربر یافت نشد', 404);
}
$conn = $this->em->getConnection();
$repId = $rep->getId();
$total = (int) $conn->fetchOne('SELECT COUNT(*) FROM doctors WHERE representation_id = ?', [$repId]);
$active = (int) $conn->fetchOne('SELECT COUNT(*) FROM doctors WHERE representation_id = ? AND active_doctor_appointment = 1', [$repId]);
$male = (int) $conn->fetchOne("SELECT COUNT(*) FROM doctors WHERE representation_id = ? AND gender IN ('man','male')", [$repId]);
$female = (int) $conn->fetchOne("SELECT COUNT(*) FROM doctors WHERE representation_id = ? AND gender IN ('woman','female')", [$repId]);
return $this->success([
'total' => $total,
'active' => $active,
'inactive' => $total - $active,
'male' => $male,
'female' => $female,
]);
}
#[OA\Post(
path: '/api/v1/representation/doctors/{uuid}/status',
summary: 'فعال/غیرفعال کردن پزشکِ زیرمجموعه‌ی نماینده‌ی جاری',
security: [['bearerAuth' => []]],
parameters: [new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
responses: [
new OA\Response(response: 200, description: 'وضعیت جدید پزشک'),
new OA\Response(response: 404, description: 'پزشک یافت نشد یا متعلق به این نماینده نیست'),
]
)]
#[Route('/api/v1/representation/doctors/{uuid}/status', methods: ['POST'])]
public function toggleDoctorStatus(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$rep = $this->representationRepo->findByUser($user);
if ($rep === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نماینده‌ای برای این کاربر یافت نشد', 404);
}
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]);
if ($doctor === null || $doctor->getRepresentationId() !== $rep->getId()) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404);
}
$doctor->setActiveDoctorAppointment(!$doctor->isActiveDoctorAppointment());
$this->em->flush();
return $this->success(['is_active' => $doctor->isActiveDoctorAppointment()]);
}
}