feat(doctors): add city and province filters for doctor listing
This commit is contained in:
@@ -107,6 +107,8 @@ export default function DoctorsPage() {
|
|||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState('');
|
||||||
const [ownerStatus, setOwnerStatus] = useState('');
|
const [ownerStatus, setOwnerStatus] = useState('');
|
||||||
const [specialtyId, setSpecialty] = useState('');
|
const [specialtyId, setSpecialty] = useState('');
|
||||||
|
const [provinceId, setProvinceId] = useState('');
|
||||||
|
const [cityId, setCityId] = useState('');
|
||||||
const [view, setView] = useState<'table' | 'grid'>('table');
|
const [view, setView] = useState<'table' | 'grid'>('table');
|
||||||
const [deleteTarget, setDeleteTarget] = useState<AdminDoctor | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<AdminDoctor | null>(null);
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ export default function DoctorsPage() {
|
|||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [searchInput]);
|
}, [searchInput]);
|
||||||
|
|
||||||
useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId]);
|
useEffect(() => { setPage(1); }, [status, ownerStatus, specialtyId, provinceId, cityId]);
|
||||||
|
|
||||||
// ── Queries ──
|
// ── Queries ──
|
||||||
|
|
||||||
@@ -136,14 +138,29 @@ export default function DoctorsPage() {
|
|||||||
staleTime: 300_000,
|
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({
|
const doctorsQ = useQuery({
|
||||||
queryKey: ['admin-doctors', page, limit, search, status, ownerStatus, specialtyId],
|
queryKey: ['admin-doctors', page, limit, search, status, ownerStatus, specialtyId, provinceId, cityId],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const p = new URLSearchParams({ page: String(page), limit: String(limit) });
|
const p = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||||
if (search) p.set('search', search);
|
if (search) p.set('search', search);
|
||||||
if (status) p.set('status', status);
|
if (status) p.set('status', status);
|
||||||
if (ownerStatus && !isRepresentation) p.set('owner_status', ownerStatus);
|
if (ownerStatus && !isRepresentation) p.set('owner_status', ownerStatus);
|
||||||
if (specialtyId) p.set('specialty_id', specialtyId);
|
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';
|
const base = isRepresentation ? '/api/v1/representation/doctors' : '/api/v1/admin/doctors';
|
||||||
return api.get<PaginatedResponse<AdminDoctor>>(`${base}?${p}`);
|
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?.data as any)?.data ?? specialtiesQ.data?.data ?? [],
|
||||||
[specialtiesQ.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 ──
|
// ── Mutations ──
|
||||||
|
|
||||||
@@ -259,6 +284,30 @@ export default function DoctorsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="seg">
|
||||||
<button className={!status ? 'on' : ''} onClick={() => setStatus('')}>همه</button>
|
<button className={!status ? 'on' : ''} onClick={() => setStatus('')}>همه</button>
|
||||||
<button className={status === 'active' ? 'on' : ''} onClick={() => setStatus('active')}>فعال</button>
|
<button className={status === 'active' ? 'on' : ''} onClick={() => setStatus('active')}>فعال</button>
|
||||||
|
|||||||
@@ -345,6 +345,8 @@ List all doctors with pagination.
|
|||||||
| `specialty_id` | integer | ❌ | Filter by specialty |
|
| `specialty_id` | integer | ❌ | Filter by specialty |
|
||||||
| `owner_status` | string | ❌ | `claimed` \| `unclaimed` \| `pending_transfer` — پروفایلهای ایمپورت IRIMC (خروجی هم `owner_status` و `source` دارد) |
|
| `owner_status` | string | ❌ | `claimed` \| `unclaimed` \| `pending_transfer` — پروفایلهای ایمپورت IRIMC (خروجی هم `owner_status` و `source` دارد) |
|
||||||
| `unassigned` | string | ❌ | `1` → فقط پزشکانِ بدون نماینده (`representation_id IS NULL`) — برای انتخاب و اتصال به نماینده |
|
| `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 |
|
| `sort` | string | ❌ | Sort field |
|
||||||
|
|
||||||
### Response `200`
|
### Response `200`
|
||||||
|
|||||||
@@ -339,6 +339,8 @@ class AdminApiController extends BaseController
|
|||||||
$status = $request->query->get('status', '');
|
$status = $request->query->get('status', '');
|
||||||
$gender = trim((string) $request->query->get('gender', ''));
|
$gender = trim((string) $request->query->get('gender', ''));
|
||||||
$specId = (int) $request->query->get('specialty_id', 0);
|
$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'));
|
$sort = trim((string) $request->query->get('sort', 'newest'));
|
||||||
|
|
||||||
$conn = $this->em->getConnection();
|
$conn = $this->em->getConnection();
|
||||||
@@ -370,6 +372,15 @@ class AdminApiController extends BaseController
|
|||||||
if ($request->query->get('unassigned') === '1') {
|
if ($request->query->get('unassigned') === '1') {
|
||||||
$where[] = 'd.representation_id IS NULL';
|
$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);
|
$whereStr = implode(' AND ', $where);
|
||||||
$orderBy = match ($sort) {
|
$orderBy = match ($sort) {
|
||||||
|
|||||||
Reference in New Issue
Block a user