feat(specialties): show per-city doctor count on /specialties

Fetch active specialties with number_of_doctors from
GET /api/v1/specialties/doctor-counts (scoped to the current city via
matchedCity.id) instead of the static specialties.json, so each specialty
card shows the real doctor count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-16 22:27:39 +03:30
co-authored by Claude Opus 4.8
parent 69e591f03e
commit 344285dce5
4 changed files with 130 additions and 9 deletions
@@ -0,0 +1,103 @@
# نمایش تعداد پزشکان هر تخصص در صفحه /specialties
## پروژه
`nobat724_front` (سایت عمومی).
> **Cross-repo:** وابسته به endpoint بک‌اند `GET /api/v1/specialties/doctor-counts?city_id=<id>` (پرامپت `clinicpro/.claude/prompt/specialty-doctor-counts.md`). آن **اول** اجرا شود.
## زمینه
صفحه‌ی `/specialties` کارت هر تخصص را از `data/specialties.json` (ثابت) رندر می‌کند و زیرش `{data.number_of_doctors} پزشک` می‌نویسد — اما `specialties.json` فیلد `number_of_doctors` ندارد، پس همیشه خالی است. باید تعداد واقعی پزشکانِ هر تخصص **در شهرِ دامنه‌ی جاری** از API گرفته و نمایش داده شود.
شهر از subdomain تشخیص داده می‌شود: server-side با `getStateInfo()``matchedCity.id` (همان city id بک‌اند).
## مشکل / هدف
صفحه به‌جای json ثابت، لیست تخصص‌ها را با `number_of_doctors` از endpoint جدید (با `city_id` شهر جاری) بگیرد و نمایش دهد.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `app/specialties/page.js` | Server Component — `matchedCity.id` را از `getStateInfo` بگیر و به صفحه بده |
| `components/specialties/index.js` | `"use client"` — fetch count از API به‌جای json ثابت |
| `components/specialties/list/ItemSpecialties.js` | نمایش `data.number_of_doctors` (موجود — تغییر لازم ندارد) |
| `services/response.js` | افزودن wrapper `getSpecialtyDoctorCounts(cityId)` |
## وضعیت فعلی (کد واقعی)
`ItemSpecialties.js` از قبل تعداد را نشان می‌دهد (فقط داده ندارد):
```jsx
<p className="text-[#7E7E7E] text-[14px] font-medium">
{data.number_of_doctors} پزشک
</p>
```
`components/specialties/index.js`:
```jsx
import specialtiesData from "@/data/specialties.json";
const [filteredSpecialties, setFilteredSpecialties] = useState(specialtiesData);
const handleSearch = (value) => setFilteredSpecialties(searchOnList(specialtiesData, value, "name"));
```
`app/specialties/page.js`:
```jsx
function Specialties() {
return (<Layout name="/specialties"><SpecialtiesPage /></Layout>);
}
```
پاسخ بک‌اند `GET /api/v1/specialties/doctor-counts?city_id=<id>`: `{ success, data: { data: [ {id, name, slug, parent_id, number_of_doctors, ...} ] } }` (double-nested — `success(['data'=>...])`).
## وظایف
### ۱. wrapper در `services/response.js`
```js
getSpecialtyDoctorCounts: (cityId) =>
api.get(`api/v1/specialties/doctor-counts`, { params: { city_id: cityId } }),
```
### ۲. پاس‌دادن city id از صفحه‌ی server به کامپوننت client
در `app/specialties/page.js`:
```jsx
import { getStateInfo } from "@/lib/getStateInfo";
async function Specialties() {
const { matchedCity } = await getStateInfo();
return (
<Layout name="/specialties">
<SpecialtiesPage cityId={matchedCity?.id ?? null} />
</Layout>
);
}
```
(صفحه باید `async` شود؛ `generateMetadata` موجود دست‌نخورده.)
### ۳. fetch در `components/specialties/index.js`
- prop `cityId` بگیر.
- در `useEffect` (mount / تغییر cityId) `request.getSpecialtyDoctorCounts(cityId)` را صدا بزن، `res.data.data` را در state بگذار.
- تا رسیدن داده، می‌توان از `specialties.json` به‌عنوان نمایش اولیه استفاده کرد (یا اسکلت)، ولی منبعِ نهایی API است.
- جستجو روی همان لیستِ API اعمال شود (`searchOnList(list, value, "name")`).
- فقط تخصص‌های ریشه (یا همان رفتار فعلی) نمایش داده شوند؛ اگر API همه را می‌دهد و قبلاً json هم همه را داشت، رفتار را حفظ کن.
```jsx
const [all, setAll] = useState([]);
const [filtered, setFiltered] = useState([]);
useEffect(() => {
request.getSpecialtyDoctorCounts(cityId)
.then((res) => { const items = res?.data?.data ?? []; setAll(items); setFiltered(items); })
.catch(() => { setAll([]); setFiltered([]); });
}, [cityId]);
const handleSearch = (value) => setFiltered(searchOnList(all, value, "name"));
```
### ۴. `ItemSpecialties.js`
بدون تغییر ساختار؛ فقط مطمئن شو `data.number_of_doctors` (که حالا از API می‌آید) و `data.id` (برای لینک `/doctors?specialties=`) درست‌اند. اگر `number_of_doctors` صفر بود، «۰ پزشک» نمایش داده شود (یا در صورت تمایل پنهان شود).
## نکات مهم
- پاسخ double-nested است → `res.data.data`.
- `matchedCity?.id` ممکن است `null` باشد (دامنه‌ی ناشناخته) → بدون `city_id` ارسال شود؛ بک‌اند شمارش سراسری می‌دهد (fallback).
- لینک کارت تخصص از `data.id` استفاده می‌کند (`/doctors?specialties=<id>`) — مطمئن شو `id` در پاسخ API همان است که `/doctors` با `specialty_id`/`specialties` می‌پذیرد.
- App Router؛ `generateMetadata` و `getStateInfo` الگوی موجود؛ RTL/Vazir.
- بعد از تغییر: `npm run build` بدون خطا؛ روی `/specialties` تعداد واقعی پزشکانِ شهر زیر هر تخصص دیده شود؛ جستجو همچنان کار کند.
+3 -2
View File
@@ -17,10 +17,11 @@ export async function generateMetadata() {
}; };
} }
function Specialties() { async function Specialties() {
const { matchedCity } = await getStateInfo();
return ( return (
<Layout name="/specialties"> <Layout name="/specialties">
<SpecialtiesPage /> <SpecialtiesPage cityId={matchedCity?.id ?? null} />
</Layout> </Layout>
); );
} }
+19 -7
View File
@@ -1,18 +1,30 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import List from "./list"; import List from "./list";
import SmSearch from "../searchHead/smSearch"; import SmSearch from "../searchHead/smSearch";
import HeadPageList from "@/app/component/head"; import HeadPageList from "@/app/component/head";
import specialtiesData from "@/data/specialties.json"; import specialtiesData from "@/data/specialties.json";
import { searchOnList } from "@/helper"; import { searchOnList } from "@/helper";
import { request } from "@/services/response";
function SpecialtiesPage() { function SpecialtiesPage({ cityId }) {
const [filteredSpecialties, setFilteredSpecialties] = const [all, setAll] = useState(specialtiesData);
useState(specialtiesData); const [filtered, setFiltered] = useState(specialtiesData);
useEffect(() => {
request
.getSpecialtyDoctorCounts(cityId)
.then((res) => {
const items = res?.data?.data ?? [];
setAll(items);
setFiltered(items);
})
.catch(() => {});
}, [cityId]);
const handleSearch = (value) => const handleSearch = (value) =>
setFilteredSpecialties(searchOnList(specialtiesData, value, "name")); setFiltered(searchOnList(all, value, "name"));
return ( return (
<> <>
@@ -21,12 +33,12 @@ function SpecialtiesPage() {
detail="تخصص مورد نظرتان را جستجو کرده و یا از لیست زیر انتخاب نمایید:" detail="تخصص مورد نظرتان را جستجو کرده و یا از لیست زیر انتخاب نمایید:"
> >
<SmSearch <SmSearch
data={specialtiesData} data={all}
placeholder="جستجوی تخصص" placeholder="جستجوی تخصص"
handleSearch={handleSearch} handleSearch={handleSearch}
/> />
</HeadPageList> </HeadPageList>
<List specialties={filteredSpecialties} /> <List specialties={filtered} />
</> </>
); );
} }
+5
View File
@@ -51,6 +51,11 @@ export const request = {
Authorization: "", Authorization: "",
}, },
}), }),
getSpecialtyDoctorCounts: (cityId) =>
api.get("api/v1/specialties/doctor-counts", {
params: cityId ? { city_id: cityId } : {},
headers: { Authorization: "" },
}),
getAppointmentWeeklySchedule: (uuid) => getAppointmentWeeklySchedule: (uuid) =>
api.get(`api/v1/appointment-settings/weekly-schedule/${uuid}`), api.get(`api/v1/appointment-settings/weekly-schedule/${uuid}`),
postAppointmentWeeklySchedule: () => postAppointmentWeeklySchedule: () =>