feat(doctors): add city and province filters for doctor listing

This commit is contained in:
hamed
2026-07-12 14:59:02 +03:30
parent bb5884d6bd
commit d2372d4d21
3 changed files with 64 additions and 2 deletions
+51 -2
View File
@@ -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<AdminDoctor | null>(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<ApiResponse<{ id: number; name: string }[]>>('/api/v1/provinces'),
staleTime: 600_000,
enabled: !isRepresentation,
});
const citiesQ = useQuery({
queryKey: ['cities-list', provinceId],
queryFn: () => api.get<ApiResponse<{ id: number; name: string }[]>>(`/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<PaginatedResponse<AdminDoctor>>(`${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() {
/>
</div>
)}
{!isRepresentation && (
<div style={{ minWidth: 170 }}>
<SearchableSelect
options={provinceOptions}
value={provinceId || null}
onChange={(v) => { setProvinceId(v ? String(v) : ''); setCityId(''); }}
placeholder="همه استان‌ها"
isClearable
height={36}
/>
</div>
)}
{!isRepresentation && provinceId && (
<div style={{ minWidth: 170 }}>
<SearchableSelect
options={cityOptions}
value={cityId || null}
onChange={(v) => setCityId(v ? String(v) : '')}
placeholder="همه شهرها"
isClearable
height={36}
/>
</div>
)}
<div className="seg">
<button className={!status ? 'on' : ''} onClick={() => setStatus('')}>همه</button>
<button className={status === 'active' ? 'on' : ''} onClick={() => setStatus('active')}>فعال</button>
+2
View File
@@ -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`
@@ -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) {