fix(doctors): improve filter performance and resolve modal closing issues

This commit is contained in:
hamed
2026-06-21 15:47:37 +03:30
parent 2fc74a664e
commit cb13ba3f45
10 changed files with 232 additions and 58 deletions
@@ -0,0 +1,196 @@
# فیکس کندی و باگ‌های فیلتر صفحه پزشکان
## پروژه
`nobat724_front`
## زمینه
صفحه `/doctors` بعد از `npm run build && npm run start` (production mode) سه مشکل دارد:
۱. سرعت کلی سایت کند می‌شود
۲. مودال انتخاب شهر/استان بعد از کلیک «تایید و ادامه» دیر بسته می‌شود و فیلتر با تاخیر اعمال می‌شود
۳. دسته‌بندی (category/specialty) در مودال فیلتر درست کار نمی‌کند
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `nobat724_front/app/component/modalSearchCity/Content.js` | مودال انتخاب شهر/استان — دکمه «تایید و ادامه» |
| `nobat724_front/app/component/modalSearchCity/index.js` | wrapper مودال شهر |
| `nobat724_front/components/doctors/modal/ButtonApply.js` | دکمه «اعمال تغییرات» در مودال فیلتر |
| `nobat724_front/components/doctors/modal/Content.js` | محتوای مودال فیلتر (category/specialty) |
| `nobat724_front/components/doctors/modal/form/index.js` | فرم فیلتر — CateSelector برای category و specialty |
| `nobat724_front/components/doctors/head/index.js` | `sendReq` — تابع اصلی fetch |
| `nobat724_front/helper/index.js` (خط ۴۲۰) | `QueryForDoctorsReq` — تبدیل filter به params |
| `nobat724_front/helper/index.js` (خط ۲۱۵) | `filterList` — تولید parentList و childrenList |
## وضعیت فعلی
### باگ ۱ — مودال شهر/استان دیر بسته می‌شود
`Content.js` خط ۲۴: `getStateInfoClient()` در هر render صدا زده می‌شود:
```js
// app/component/modalSearchCity/Content.js
const { matchedState } = getStateInfoClient();
```
و دکمه «تایید و ادامه» منتظر `sendReq()` می‌ماند قبل از بستن:
```js
const handleClick = () => {
setLoading(true);
sendReq().finally(() => {
setLoading(false);
handleClose(); // ← مودال فقط بعد از پایان fetch بسته می‌شود
});
};
```
`sendReq` در `head/index.js` یک HTTP request کامل است — تا پایان fetch مودال باز می‌ماند.
همچنین دو `disabled` prop روی دکمه تایید وجود دارد (یکی `loading` و یکی `!filter?.city || !filter?.state`) که در JSX اشتباه است — دومی override می‌کند.
### باگ ۲ — دسته‌بندی کار نمی‌کند
در `modal/Content.js`:
```js
function Content({ filter, setFilter, updateData, setDataInURL }) {
const changeSpecialty = (name, value) => {
const newFilter = { ...filter, [name]: value };
if (name === "category") {
const childrenList = specialties.filter((item) => item.parent);
const filteredChildrenList = childrenList.filter(
(item) => item.parent === value.id // ← value می‌تواند null باشد (وقتی clear می‌شود)
);
const firstChildren = filteredChildrenList[0];
newFilter.specialty = firstChildren || "";
}
setFilter(newFilter);
setDataInURL(newFilter);
return newFilter;
};
```
وقتی `value` (category) null است (کاربر انتخاب را پاک می‌کند)، `value.id` crash می‌کند.
و `filterList` در `helper/index.js`:
```js
export const filterList = (data) => {
const parentList = specialties.filter((item) => !item.parent);
const childrenList = specialties.filter((item) => item.parent);
const filteredChildrenList =
data && data.category
? childrenList.filter((item) => item.parent === data.category.id)
: [];
// ← item.parent (string از JSON) vs data.category.id (ممکن است number باشد)
```
`specialties.json` فیلد `parent` را به‌صورت string ذخیره می‌کند (مثلاً `"1"`) اما `category.id` ممکن است number باشد — type mismatch.
### باگ ۳ — کندی کلی
احتمال اصلی: `getStateInfoClient()` و `cities.filter()` و `states.find()` در هر render اجرا می‌شوند بدون memoization. همچنین `sendReq` در `ButtonApply` و `Content` هر دو منتظر کامل شدن fetch قبل از بستن مودال هستند.
## وظایف
### ۱. فیکس بستن سریع مودال شهر/استان
در `app/component/modalSearchCity/Content.js` مودال را فوری ببند و fetch در پس‌زمینه ادامه یابد:
```js
const handleClick = () => {
handleClose(); // فوری ببند
sendReq(); // fetch در background
};
```
همچنین دو `disabled` را روی دکمه یکی کن:
```jsx
<Button
disabled={!filter?.city || !filter?.state}
onClick={handleClick}
variant="contained"
>
```
### ۲. فیکس بستن سریع مودال فیلتر
در `components/doctors/modal/ButtonApply.js` همین الگو را اعمال کن:
```js
const handleClick = () => {
setOpen(false); // فوری ببند
sendReq(); // fetch در background
};
```
state `loading` را می‌توانی حذف کنی چون دیگر منتظر fetch نمی‌ماند.
### ۳. فیکس crash دسته‌بندی هنگام clear
در `components/doctors/modal/Content.js` خط مربوط به `value.id`:
```js
const changeSpecialty = (name, value) => {
const newFilter = { ...filter, [name]: value };
if (name === "category") {
if (value) {
const filteredChildrenList = specialties
.filter((item) => item.parent)
.filter((item) => String(item.parent) === String(value.id)); // ← String() برای type safety
newFilter.specialty = filteredChildrenList[0] || null;
} else {
newFilter.specialty = null; // clear category → clear specialty هم
}
}
setFilter(newFilter);
setDataInURL(newFilter);
return newFilter;
};
```
### ۴. فیکس type mismatch در filterList
در `helper/index.js` خط ۲۱۹:
```js
const filteredChildrenList =
data && data.category
? childrenList.filter(
(item) => String(item.parent) === String(data.category.id)
)
: [];
```
### ۵. بررسی کندی کلی
- در `app/component/modalSearchCity/index.js`، `useEffect` وابسته به `stateSelected` هر بار کل `cities` را filter می‌کند — با `useMemo` بهینه کن:
```js
const filteredCities = useMemo(() => {
if (!stateSelected) return [];
const selectedState = states.find((s) => s.name === stateSelected);
if (!selectedState) return [];
return cities.filter((c) => c.province_id === selectedState.id);
}, [stateSelected]);
// حذف useState و useEffect مربوطه
```
- `getStateInfoClient()` در `Content.js` را به بیرون از component ببر یا با `useMemo` cache کن.
## نکات مهم
- `specialties.json` فیلد `parent` را string ذخیره می‌کند (`"1"` نه `1`) — همیشه `String()` برای مقایسه استفاده کن
- مودال‌ها باید فوری بسته شوند (UX) و fetch در background ادامه یابد — کاربر نباید منتظر پاسخ API بماند
- `sendReq` در `head/index.js` یک Promise برمی‌گرداند — می‌توان بدون `await` صدا زد
- `disabled` دو بار روی یک `<Button>` در JSX: دومی اول override می‌کند — فقط یکی نگه‌دار
- بعد از تغییر، با `npm run build && npm run start` تست کن (نه dev mode)
+3 -9
View File
@@ -3,7 +3,6 @@ import { Button } from "@mui/material";
// Icon
import CloseModalD from "@/components/icons/CloseModalD";
import ArrowLeftM from "@/components/icons/ArrowLeftM";
import { useState } from "react";
import AddressSelector from "./AddressSelector";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
@@ -16,14 +15,10 @@ function Content({
setDataInURL,
filteredCities,
}) {
const [loading, setLoading] = useState(false);
const { matchedState } = getStateInfoClient();
const handleClick = () => {
setLoading(true);
sendReq().finally(() => {
setLoading(false);
handleClose();
});
handleClose();
sendReq();
};
const updateData = (name, value) => {
@@ -74,12 +69,11 @@ function Content({
</div>
<div className="w-full flex justify-end">
<Button
disabled={loading}
disabled={!filter?.city || !filter?.state}
className="!w-full md:!w-fit !flex !px-3 items-center justify-center gap-1 mr-auto"
onClick={handleClick}
variant="contained"
color="primary"
disabled={!filter?.city || !filter?.state}
>
تایید و ادامه
<ArrowLeftM />
+6 -18
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useMemo } from "react";
import { Modal, Box } from "@mui/material";
import { styleDefault } from "@/mui";
@@ -20,25 +20,13 @@ function ModalSearchCity({
}) {
const stateSelected = filter?.state?.name;
const [filteredCities, setFilteredCities] = useState([]);
const handleClose = () => setOpen(false);
useEffect(() => {
if (stateSelected) {
const selectedState = states.find(
(state) => state.name === stateSelected
);
if (selectedState) {
const stateId = selectedState.id;
const filteredCities = cities.filter(
(city) => city.province_id === stateId
);
setFilteredCities(filteredCities);
}
} else {
setFilteredCities([]);
}
const filteredCities = useMemo(() => {
if (!stateSelected) return [];
const selectedState = states.find((s) => s.name === stateSelected);
if (!selectedState) return [];
return cities.filter((c) => c.province_id === selectedState.id);
}, [stateSelected]);
return (
+2 -2
View File
@@ -7,10 +7,10 @@ import IconMultipleSelect from "@/components/icons/IconMultipleSelect";
function groupSpecialtiesByParent(specialties) {
const groups = [];
const parents = specialties.filter((item) => item.parent === null);
const parents = specialties.filter((item) => item.parent_id === null || item.parent_id === undefined);
parents.forEach((parent) => {
const children = specialties.filter((item) => item.parent === parent.id);
const children = specialties.filter((item) => String(item.parent_id) === String(parent.id));
if (children.length > 0) {
groups.push({
parent,
@@ -6,13 +6,14 @@ function Content({ filter, setFilter, updateData, setDataInURL }) {
const newFilter = { ...filter, [name]: value };
if (name === "category") {
const childrenList = specialties.filter((item) => item.parent);
const filteredChildrenList = childrenList.filter(
(item) => item.parent === value.id
);
const firstChildren = filteredChildrenList[0];
newFilter.specialty = firstChildren || "";
if (value) {
const filtered = specialties
.filter((item) => item.parent_id)
.filter((item) => String(item.parent_id) === String(value.id));
newFilter.specialty = filtered[0] || null;
} else {
newFilter.specialty = null;
}
}
setFilter(newFilter);
+1 -1
View File
@@ -17,7 +17,7 @@ function DoctorsPage({ matchedCity, matchedState, list }) {
const findItem = (key, name) =>
specialties.find((item) => item[key] === name);
const isParent =
findItem("name", fieldName) && findItem("name", fieldName).parent;
findItem("name", fieldName) && findItem("name", fieldName).parent_id;
const selectedState = searchParams.get("state") || matchedState?.name;
const selectedCity =
+2 -10
View File
@@ -1,22 +1,14 @@
import { Button } from "@mui/material";
import { useState } from "react";
function ButtonApply({ sendReq, setOpen }) {
const [loading, setLoading] = useState(false);
const handleClick = () => {
setLoading(true);
sendReq().finally(() => {
setOpen(false);
setLoading(false);
});
setOpen(false);
sendReq();
};
return (
<div className="flex justify-end w-full">
<Button
disabled={loading}
variant="contained"
onClick={handleClick}
className="!px-3 !py-2 !text-[16px] !font-medium"
+8 -7
View File
@@ -6,13 +6,14 @@ function Content({ filter, setFilter, updateData, setDataInURL }) {
const newFilter = { ...filter, [name]: value };
if (name === "category") {
const childrenList = specialties.filter((item) => item.parent);
const filteredChildrenList = childrenList.filter(
(item) => item.parent === value.id
);
const firstChildren = filteredChildrenList[0];
newFilter.specialty = firstChildren || "";
if (value) {
const filtered = specialties
.filter((item) => item.parent_id)
.filter((item) => String(item.parent_id) === String(value.id));
newFilter.specialty = filtered[0] || null;
} else {
newFilter.specialty = null;
}
}
setFilter(newFilter);
+1 -1
View File
@@ -3,7 +3,7 @@ import specialties from "@/data/specialties.json";
import Link from "next/link";
function FrequentSearches() {
const childrenList = specialties.filter((item) => item.parent);
const childrenList = specialties.filter((item) => item.parent_id);
return (
<div className="flex items-center justify-center gap-[8px] sm:gap-[10px] md:gap-[13px] lg:gap-[16px] w-full">
+5 -3
View File
@@ -213,12 +213,14 @@ export function clearDoctorClinicParams(router, slug) {
router.push(newUrl)
}
export const filterList = (data) => {
const parentList = specialties.filter((item) => !item.parent);
const childrenList = specialties.filter((item) => item.parent);
const parentList = specialties.filter((item) => !item.parent_id);
const childrenList = specialties.filter((item) => item.parent_id);
const filteredChildrenList =
data && data.category
? childrenList.filter((item) => item.parent === data.category.id)
? childrenList.filter(
(item) => String(item.parent_id) === String(data.category.id)
)
: [];
return {