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:
@@ -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` تعداد واقعی پزشکانِ شهر زیر هر تخصص دیده شود؛ جستجو همچنان کار کند.
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user