From d2372d4d21404a988fc383cec00719b4047bac9b Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 12 Jul 2026 14:59:02 +0330 Subject: [PATCH] feat(doctors): add city and province filters for doctor listing --- assets/admin/pages/DoctorsPage.tsx | 53 ++++++++++++++++++++- docs/api/admin.md | 2 + src/Admin/Controller/AdminApiController.php | 11 +++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/assets/admin/pages/DoctorsPage.tsx b/assets/admin/pages/DoctorsPage.tsx index beb008a0..cd03722a 100644 --- a/assets/admin/pages/DoctorsPage.tsx +++ b/assets/admin/pages/DoctorsPage.tsx @@ -107,6 +107,8 @@ export default function DoctorsPage() { const [status, setStatus] = useState(''); const [ownerStatus, setOwnerStatus] = useState(''); const [specialtyId, setSpecialty] = useState(''); + const [provinceId, setProvinceId] = useState(''); + const [cityId, setCityId] = useState(''); const [view, setView] = useState<'table' | 'grid'>('table'); const [deleteTarget, setDeleteTarget] = useState(null); @@ -115,7 +117,7 @@ export default function DoctorsPage() { return () => clearTimeout(t); }, [searchInput]); - useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId]); + useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId, provinceId, cityId]); // ── Queries ── @@ -136,14 +138,29 @@ export default function DoctorsPage() { staleTime: 300_000, }); + const provincesQ = useQuery({ + queryKey: ['provinces-list'], + queryFn: () => api.get>('/api/v1/provinces'), + staleTime: 600_000, + enabled: !isRepresentation, + }); + const citiesQ = useQuery({ + queryKey: ['cities-list', provinceId], + queryFn: () => api.get>(`/api/v1/cities${provinceId ? `?province_id=${provinceId}` : ''}`), + staleTime: 600_000, + enabled: !isRepresentation && !!provinceId, + }); + const doctorsQ = useQuery({ - queryKey: ['admin-doctors', page, limit, search, status, ownerStatus, specialtyId], + queryKey: ['admin-doctors', page, limit, search, status, ownerStatus, specialtyId, provinceId, cityId], queryFn: () => { const p = new URLSearchParams({ page: String(page), limit: String(limit) }); if (search) p.set('search', search); if (status) p.set('status', status); if (ownerStatus && !isRepresentation) p.set('owner_status', ownerStatus); if (specialtyId) p.set('specialty_id', specialtyId); + if (provinceId && !isRepresentation) p.set('state_id', provinceId); + if (cityId && !isRepresentation) p.set('city_id', cityId); const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors'; return api.get>(`${base}?${p}`); }, @@ -159,6 +176,14 @@ export default function DoctorsPage() { () => (specialtiesQ.data?.data as any)?.data ?? specialtiesQ.data?.data ?? [], [specialtiesQ.data] ); + const provinceOptions = useMemo( + () => ((provincesQ.data?.data as any)?.data ?? provincesQ.data?.data ?? []).map((p: { id: number; name: string }) => ({ value: String(p.id), label: p.name })), + [provincesQ.data] + ); + const cityOptions = useMemo( + () => ((citiesQ.data?.data as any)?.data ?? citiesQ.data?.data ?? []).map((c: { id: number; name: string }) => ({ value: String(c.id), label: c.name })), + [citiesQ.data] + ); // ── Mutations ── @@ -259,6 +284,30 @@ export default function DoctorsPage() { /> )} + {!isRepresentation && ( +
+ { setProvinceId(v ? String(v) : ''); setCityId(''); }} + placeholder="همه استان‌ها" + isClearable + height={36} + /> +
+ )} + {!isRepresentation && provinceId && ( +
+ setCityId(v ? String(v) : '')} + placeholder="همه شهرها" + isClearable + height={36} + /> +
+ )}
diff --git a/docs/api/admin.md b/docs/api/admin.md index e8b75834..0bbad7d0 100644 --- a/docs/api/admin.md +++ b/docs/api/admin.md @@ -345,6 +345,8 @@ List all doctors with pagination. | `specialty_id` | integer | ❌ | Filter by specialty | | `owner_status` | string | ❌ | `claimed` \| `unclaimed` \| `pending_transfer` — پروفایل‌های ایمپورت IRIMC (خروجی هم `owner_status` و `source` دارد) | | `unassigned` | string | ❌ | `1` → فقط پزشکانِ بدون نماینده (`representation_id IS NULL`) — برای انتخاب و اتصال به نماینده | +| `city_id` | integer | ❌ | فیلتر بر اساس شهرِ آدرسِ خودِ پزشک (`doctor_addresses.city_id`) | +| `state_id` | integer | ❌ | فیلتر بر اساس استانِ آدرسِ خودِ پزشک (`doctor_addresses.province_id`) | | `sort` | string | ❌ | Sort field | ### Response `200` diff --git a/src/Admin/Controller/AdminApiController.php b/src/Admin/Controller/AdminApiController.php index 7cd9e9e8..1b521b11 100644 --- a/src/Admin/Controller/AdminApiController.php +++ b/src/Admin/Controller/AdminApiController.php @@ -339,6 +339,8 @@ class AdminApiController extends BaseController $status = $request->query->get('status', ''); $gender = trim((string) $request->query->get('gender', '')); $specId = (int) $request->query->get('specialty_id', 0); + $cityId = (int) $request->query->get('city_id', 0); + $stateId = (int) $request->query->get('state_id', 0); $sort = trim((string) $request->query->get('sort', 'newest')); $conn = $this->em->getConnection(); @@ -370,6 +372,15 @@ class AdminApiController extends BaseController if ($request->query->get('unassigned') === '1') { $where[] = 'd.representation_id IS NULL'; } + // فیلتر شهر/استان بر اساس آدرسِ خودِ پزشک (doctor_addresses). + if ($cityId > 0) { + $where[] = 'EXISTS (SELECT 1 FROM doctor_addresses da WHERE da.doctor_id = d.id AND da.city_id = :cityId)'; + $params['cityId'] = $cityId; + } + if ($stateId > 0) { + $where[] = 'EXISTS (SELECT 1 FROM doctor_addresses das WHERE das.doctor_id = d.id AND das.province_id = :stateId)'; + $params['stateId'] = $stateId; + } $whereStr = implode(' AND ', $where); $orderBy = match ($sort) {