feat(doctor): wire rich rating/review UI to multi-dimensional API
Connect the existing doctor-page review UI to the rebuilt backend contract
(see clinicpro feat/rating-multidimensional).
- page.js fetches the rating aggregate alongside comments and passes
rateAggregate down to the chart (point / satisfaction / 5 dimensions).
- Submit form sends the five dimensions with doctor_uuid; comment/reply
send {doctor_uuid, comment, parent}; 401/403 ERR_RATING_NOT_ELIGIBLE
surface friendly guidance.
- Like/dislike call POST /like/{uuid} with value and update from the
response; replies render nested.
- ModalAnswer gates the submit button on GET /rate/{uuid}/eligibility.
- services/response.js: getRateEligibility, postCommentsLike(uuid, value),
drop unused patchDoctorRate. Fix hardcoded modal title; empty-state for
no comments. Remove orphaned AnswerField.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
# اتصال UI غنیِ نظرات/امتیاز صفحه پزشک به قرارداد جدید بکاند
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`nobat724_front` (سایت عمومی).
|
||||||
|
|
||||||
|
> **Cross-repo:** این پرامپت به قرارداد غنیِ بکاند وابسته است که در `clinicpro/.claude/prompt/rating-multidimensional-and-rich-comments.md` تعریف شده. **آن پرامپت بکاند باید اول اجرا شود.**
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
UI غنیِ نظرات/امتیاز صفحه پزشک (چارت ۵بُعدی + دایرهی رضایت + ستارهی کلی + نظرات با نام/عکس/لایک/دیسلایک/پاسخ) از قبل در کد وجود دارد و باید **حفظ** شود. این کامپوننتها فیلدهایی میخوانند که بکاندِ سادهی قبلی نمیداد؛ حالا بکاند قرارداد غنی را تأمین میکند و فرانت فقط باید درست به آن وصل شود (و چند نقص جزئی رفع شود).
|
||||||
|
|
||||||
|
> هشدار: یک تلاش قبلی این UI را بهاشتباه «سادهسازی» کرد و سپس به HEAD برگردانده شد. UI غنی نباید ساده شود؛ فقط به قرارداد جدید وصل شود.
|
||||||
|
|
||||||
|
## قرارداد بکاند (مرجع — از پرامپت همتا)
|
||||||
|
|
||||||
|
- `GET /api/v1/rate/{uuid}` (عمومی) → `{ data: { point, satisfaction, averages:[{name,label,progress}] } }`
|
||||||
|
- `POST /api/v1/rate` (AUTH) ← `{ doctor_uuid, waiting_time_at_clinic, accuracy_of_diagnosis, doctor_behavior, clinic_cleanliness, doctor_expertise }` (هر کدام ۰–۱۰۰)
|
||||||
|
- `GET /api/v1/comments/{uuid}` (عمومی) → `{ data: { data: [ { uuid, comment, created, parent, author:{real_name,picture:[{url}]}, like_status:{like_count,dislike_count,current_user_like:{like,dislike}}, replies:[…] } ] } }`
|
||||||
|
- `POST /api/v1/comment` (AUTH) ← `{ doctor_uuid, comment, parent }`
|
||||||
|
- `POST /api/v1/like/{commentUuid}` (AUTH) ← `{ value: 1|-1 }` → `{ data: { like_count, dislike_count, current_user_like } }`
|
||||||
|
- `GET /api/v1/rate/{uuid}/eligibility` (AUTH) → `{ data: { eligible: bool } }` (قبلاً اضافه شده)
|
||||||
|
- گارد بکاند: ثبت نظر/امتیاز فقط برای کاربرِ دارای نوبت confirmed در ۳۰ روز گذشته → در غیر این صورت `403 ERR_RATING_NOT_ELIGIBLE`.
|
||||||
|
|
||||||
|
## فایلهای مرتبط (UI موجود که باید وصل شود)
|
||||||
|
|
||||||
|
| فایل | نقش | اصلاح موردنیاز |
|
||||||
|
|---|---|---|
|
||||||
|
| `app/doctor/[slug]/page.js` | fetch server-side | علاوه بر comments، تجمیع امتیاز از `GET /rate/{uuid}` را fetch کن و بهصورت prop بده |
|
||||||
|
| `components/doctor/index.js` → `detailDoctor/index.js` → `cards/comments/index.js` | زنجیره prop | `rateAggregate` (point/satisfaction/averages) را عبور بده |
|
||||||
|
| `cards/comments/chart/index.js` | چارت ۵بُعدی + ستاره | بهجای `doctor.point/satisfaction/average_rate.averages` از prop `rateAggregate` بخوان |
|
||||||
|
| `cards/comments/chart/ProgressAll.js` | دایرهی رضایت | `satisfaction` را از `rateAggregate` بگیرد |
|
||||||
|
| `cards/comments/modal/ModalAnswer.js` | مودال ثبت | پیشبارگذاری امتیاز قبلی را حذف/سازگار کن؛ eligibility بررسی شود |
|
||||||
|
| `cards/comments/modal/content/index.js` | هدر مودال | عنوان hardcoded «دکتر مهران امینی» → `doctor?.name` |
|
||||||
|
| `cards/comments/modal/content/Form.js` | ثبت امتیاز+نظر | body امتیاز را با `doctor_uuid` بفرست (نه `doctor_id`)؛ مدیریت ۴۰۳/۴۰۱ |
|
||||||
|
| `app/component/comment/ItemUser.js` | رندر نظر | `data.created`→تاریخ، author/like_status/replies حفظ؛ لایک/دیسلایک به `postCommentsLike(uuid, value)` وصل شود |
|
||||||
|
| `app/component/comment/SendAnswer.js` | ثبت پاسخ | body را با `doctor_uuid` و `parent` بفرست |
|
||||||
|
| `services/response.js` | wrappers | `postDoctorRate` بدنهی چندبُعدی؛ `postCommentsLike(uuid, value)`؛ `getRateEligibility` (موجود) |
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. fetch تجمیع امتیاز در صفحه و عبور prop
|
||||||
|
در `app/doctor/[slug]/page.js` کنار fetch فعلیِ doctor/comments، تجمیع امتیاز را بگیر:
|
||||||
|
```jsx
|
||||||
|
const resRate = await axiosInstance.get(`${API_URL}/api/v1/rate/${doctor.uuid}`);
|
||||||
|
const rateAggregate = resRate?.data?.data ?? { point: 0, satisfaction: 0, averages: [] };
|
||||||
|
```
|
||||||
|
سپس `rateAggregate` را از `DoctorPage` → `DetailDoctor` → `Comments` → `Chart`/`ProgressAll` عبور بده.
|
||||||
|
|
||||||
|
### ۲. اتصال چارت به قرارداد جدید (`chart/index.js` + `ProgressAll.js`)
|
||||||
|
- `Chart` بهجای `doctor?.average_rate?.averages` از `rateAggregate.averages` استفاده کند (همان شکل `{name,label,progress}` که `ProgressDetail` انتظار دارد).
|
||||||
|
- `امتیاز کلی کاربران: {rateAggregate.point} از ۵` و ستاره `value={rateAggregate.point}`.
|
||||||
|
- `{rateAggregate.satisfaction}% رضایت کاربران` و `ProgressAll` با `satisfaction={rateAggregate.satisfaction}`.
|
||||||
|
- ساختار/استایل و کامپوننتهای `ProgressDetail`/`ProgressAll`/loadingها حفظ شوند.
|
||||||
|
|
||||||
|
### ۳. فرم ثبت امتیاز چندبُعدی (`Form.js` + `ModalAnswer.js` + `content/index.js`)
|
||||||
|
- ساختار ۵بُعدیِ موجود (`Rates` + `ProgressChange` + `comment.rate`) حفظ شود.
|
||||||
|
- body امتیاز با **`doctor_uuid`** ارسال شود (نه `doctor_id`):
|
||||||
|
```jsx
|
||||||
|
request.postDoctorRate({
|
||||||
|
doctor_uuid: doctor.uuid,
|
||||||
|
waiting_time_at_clinic: rate[0].progress,
|
||||||
|
accuracy_of_diagnosis: rate[1].progress,
|
||||||
|
doctor_behavior: rate[2].progress,
|
||||||
|
clinic_cleanliness: rate[3].progress,
|
||||||
|
doctor_expertise: rate[4].progress,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
(ترتیب ایندکسها را با `progress_detail.json` تطبیق بده — همان مپ فعلیِ Form.)
|
||||||
|
- نظر با `doctor_uuid` ارسال شود: `postDoctorComment({ doctor_uuid: doctor.uuid, comment, parent: null })`.
|
||||||
|
- چون بکاند upsert است، `patchDoctorRate`/`defaultRate` لازم نیست؛ همیشه `postDoctorRate`.
|
||||||
|
- عنوان مودال hardcoded → `doctor?.name`.
|
||||||
|
- در `ModalAnswer`، اگر pre-load امتیاز قبلی مشکلساز است حذفش کن (بکاند endpoint «امتیاز کاربر فعلی» ندارد؛ `getDoctorRate` حالا تجمیع برمیگرداند نه رأی کاربر) — فرم با مقدار اولیهی صفر باز شود.
|
||||||
|
- مدیریت خطا: ۴۰۱ → «برای ثبت باید وارد شوید»؛ ۴۰۳ با `ERR_RATING_NOT_ELIGIBLE` → پیام واجد بودن؛ سایر → خطای عمومی. (با `alert`)
|
||||||
|
|
||||||
|
### ۴. لایک/دیسلایک واقعی (`ItemUser.js` + `services/response.js`)
|
||||||
|
- `postCommentsLike` را به `(commentUuid, value)` تغییر بده: `api.post(`api/v1/like/${uuid}`, { value }, { requireAuth: true })`.
|
||||||
|
- در `ItemUser`، هنگام کلیک لایک `value=1` و دیسلایک `value=-1` بفرست؛ از پاسخ `{like_count,dislike_count,current_user_like}` state محلی را بهروز کن (بهجای دستکاری دستیِ فعلی روی `data.like_status`).
|
||||||
|
- نمایش author/created/replies حفظ شود.
|
||||||
|
|
||||||
|
### ۵. ثبت پاسخ (`SendAnswer.js`)
|
||||||
|
- body را `{ doctor_uuid: doctor.uuid /* یا از data */, comment, parent: data.uuid }` کن (نه `doctor_id`).
|
||||||
|
- منبع `doctor.uuid` را در این کامپوننت بررسی کن؛ اگر در دسترس نیست، از prop عبور بده.
|
||||||
|
|
||||||
|
### ۶. wrapperها (`services/response.js`)
|
||||||
|
- `postDoctorRate(data)` → `POST api/v1/rate` با `requireAuth` (بدنهی چندبُعدی؛ بدون تغییر امضا، فقط دیتای جدید).
|
||||||
|
- `getRateEligibility(uuid)` (موجود) حفظ شود.
|
||||||
|
- `getDoctorRate(uuid)` حالا تجمیع برمیگرداند — مصرفکنندهها (chart) باید `data.point/...` بخوانند.
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- **UI غنی حفظ شود؛ ساده نکن.** فقط به قرارداد جدید وصل کن و نقصهای جزئی (hardcode، `doctor_id`→`doctor_uuid`، toggle لایک) را رفع کن.
|
||||||
|
- `app/component/comment/*` بین صفحه پزشک و بلاگ مشترک است، اما فراخوانی آن در بلاگ (`components/blog/detail/index.js`) کامنتاوت است؛ پس تغییرات صفحه پزشک بلاگ را نمیشکند — ولی هنگام تغییر `ItemUser`/`Comment` این اشتراک را در نظر بگیر و چیزی که build بلاگ را بشکند وارد نکن.
|
||||||
|
- `current_user_like` در لیست عمومی همیشه false است (طبق تصمیم بکاند)؛ وضعیت واقعی پس از کلیک از پاسخ `toggleLike` میآید — UI را optimistic نگهدار.
|
||||||
|
- نظرات نیازمند تأیید ادمیناند؛ لیست لوکال ممکن است خالی باشد → empty-state، نه خطا.
|
||||||
|
- App Router + RTL + Vazir؛ کتابخانهی جدید اضافه نکن.
|
||||||
|
- بعد از تغییر: `npm run build` بدون خطا؛ `/doctor/[slug]` و `/blog/[slug]` هر دو باید کامپایل شوند.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { numberToArStyle } from "@/helper";
|
import { numberToArStyle } from "@/helper";
|
||||||
import { LinearProgress } from "@mui/material";
|
import { LinearProgress } from "@mui/material";
|
||||||
|
|
||||||
const label = [
|
const fallbackLabels = [
|
||||||
"برخورد مناسب پزشک",
|
"برخورد مناسب پزشک",
|
||||||
"تشخیص درست",
|
"تشخیص درست",
|
||||||
"زمان انتظار در مطب",
|
"زمان انتظار در مطب",
|
||||||
@@ -10,13 +10,16 @@ const label = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function ProgressDetail({ data, idx }) {
|
function ProgressDetail({ data, idx }) {
|
||||||
|
const label = data?.label ?? fallbackLabels[idx];
|
||||||
|
const progress = typeof data === "object" && data !== null ? data.progress : data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center justify-start gap-2 w-full">
|
<li className="flex items-center justify-start gap-2 w-full">
|
||||||
<p className="text-[#525252] text-[14px] font-normal min-w-[120px] w-[118px]">
|
<p className="text-[#525252] text-[14px] font-normal min-w-[120px] w-[118px]">
|
||||||
{label[idx]}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
value={data || 0}
|
value={progress || 0}
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
className="!w-full lg:!w-[265px] !rounded-[10px] !bg-[#D7D7D7] !h-[11px]"
|
className="!w-full lg:!w-[265px] !rounded-[10px] !bg-[#D7D7D7] !h-[11px]"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -27,9 +30,9 @@ function ProgressDetail({ data, idx }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={`text-[#525252] font-medium ${data ? "text-[20px]" : "text-[18px]"}`}
|
className={`text-[#525252] font-medium ${progress ? "text-[20px]" : "text-[18px]"}`}
|
||||||
>
|
>
|
||||||
{data ? `${numberToArStyle(data)}%` : "بدون نمره"}
|
{progress ? `${numberToArStyle(progress)}%` : "بدون نمره"}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import ArrowUpComment from "@/components/icons/ArrowUpComment";
|
|
||||||
import { Button } from "@mui/material";
|
|
||||||
|
|
||||||
function AnswerField({ data, update, setUpdate }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-start gap-1 my-2">
|
|
||||||
<Button
|
|
||||||
className="!text-[#0FA1B3] !text-[12px] !font-medium"
|
|
||||||
onClick={() => {
|
|
||||||
data.is_open_reply = !data.is_open_reply;
|
|
||||||
setUpdate(!update);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
مشاهده پاسخ ها ({data.replies.length})
|
|
||||||
<div
|
|
||||||
className={`!mr-1 ${
|
|
||||||
data.is_open_reply ? "!rotate-0" : "!rotate-180"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<ArrowUpComment />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnswerField;
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { imageUrl } from "@/helper";
|
import { imageUrl } from "@/helper";
|
||||||
import { Button, CircularProgress } from "@mui/material";
|
import { Button, CircularProgress } from "@mui/material";
|
||||||
@@ -8,62 +10,53 @@ import BoldLikeComment from "@/components/icons/BoldLikeComment";
|
|||||||
import DisLikeComment from "@/components/icons/DisLikeComment";
|
import DisLikeComment from "@/components/icons/DisLikeComment";
|
||||||
import LikeComment from "@/components/icons/LikeComment";
|
import LikeComment from "@/components/icons/LikeComment";
|
||||||
import Replies from "./Replies";
|
import Replies from "./Replies";
|
||||||
import AnswerField from "./AnswerField";
|
|
||||||
import SendAnswer from "./SendAnswer";
|
import SendAnswer from "./SendAnswer";
|
||||||
import { alert, isUserLoggedIn, timeAgo } from "@/helper";
|
import { alert, isUserLoggedIn, timeAgo } from "@/helper";
|
||||||
import { request } from "@/services/response";
|
import { request } from "@/services/response";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
function ItemUser({ update, setUpdate, data }) {
|
function ItemUser({ data, doctor, isReply = false }) {
|
||||||
const [loadingLike, setLoadingLike] = useState(false);
|
const [loadingLike, setLoadingLike] = useState(false);
|
||||||
const [loadingDislike, setLoadingDislike] = useState(false);
|
const [loadingDislike, setLoadingDislike] = useState(false);
|
||||||
|
const [openAnswer, setOpenAnswer] = useState(false);
|
||||||
|
const [status, setStatus] = useState({
|
||||||
|
like_count: data.like_status?.like_count || 0,
|
||||||
|
dislike_count: data.like_status?.dislike_count || 0,
|
||||||
|
like: data.like_status?.current_user_like?.like || false,
|
||||||
|
dislike: data.like_status?.current_user_like?.dislike || false,
|
||||||
|
});
|
||||||
|
|
||||||
const SendReq = async (type) => {
|
const sendReq = async (type) => {
|
||||||
|
if (!isUserLoggedIn()) {
|
||||||
|
alert("warning", "برای ثبت نظر باید ابتدا وارد حساب کاربری شوید.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type === "like") setLoadingLike(true);
|
if (type === "like") setLoadingLike(true);
|
||||||
else setLoadingDislike(true);
|
else setLoadingDislike(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await request.postCommentsLike(data.uuid ?? data.id);
|
const res = await request.postCommentsLike(data.uuid, type === "like" ? 1 : -1);
|
||||||
|
const d = res.data;
|
||||||
|
setStatus({
|
||||||
|
like_count: d.like_count,
|
||||||
|
dislike_count: d.dislike_count,
|
||||||
|
like: d.current_user_like.like,
|
||||||
|
dislike: d.current_user_like.dislike,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// err
|
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
|
||||||
} finally {
|
} finally {
|
||||||
if (type === "like") setLoadingLike(false);
|
if (type === "like") setLoadingLike(false);
|
||||||
else setLoadingDislike(false);
|
else setLoadingDislike(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLike = (data, type) => {
|
const toggleAnswer = () => {
|
||||||
SendReq(type);
|
if (isUserLoggedIn()) {
|
||||||
|
setOpenAnswer((prev) => !prev);
|
||||||
const { current_user_like } = data.like_status;
|
|
||||||
|
|
||||||
if (type === "like") {
|
|
||||||
if (current_user_like.like) {
|
|
||||||
data.like_status.like_count -= 1;
|
|
||||||
current_user_like.like = false;
|
|
||||||
} else {
|
|
||||||
data.like_status.like_count += 1;
|
|
||||||
current_user_like.like = true;
|
|
||||||
if (current_user_like.dislike) {
|
|
||||||
data.like_status.dislike_count -= 1;
|
|
||||||
current_user_like.dislike = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (current_user_like.dislike) {
|
alert("warning", "برای ارسال پاسخ باید ابتدا وارد حساب کاربری شوید.");
|
||||||
data.like_status.dislike_count -= 1;
|
|
||||||
current_user_like.dislike = false;
|
|
||||||
} else {
|
|
||||||
data.like_status.dislike_count += 1;
|
|
||||||
current_user_like.dislike = true;
|
|
||||||
if (current_user_like.like) {
|
|
||||||
data.like_status.like_count -= 1;
|
|
||||||
current_user_like.like = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpdate(!update);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +71,7 @@ function ItemUser({ update, setUpdate, data }) {
|
|||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-start justify-center gap-[8px]">
|
<div className="flex flex-col items-start justify-center gap-[8px]">
|
||||||
<p className="text-[#343A40] text-[14px] md:text-[16px] font-medium">
|
<p className="text-[#343A40] text-[14px] md:text-[16px] font-medium">
|
||||||
{data.author.real_name}
|
{data.author?.real_name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
|
<p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
|
||||||
{timeAgo(data.created)}
|
{timeAgo(data.created)}
|
||||||
@@ -86,79 +79,68 @@ function ItemUser({ update, setUpdate, data }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-[#495057] text-[12px] md:text-[14px]">
|
<p className="text-[#495057] text-[12px] md:text-[14px]">{data.comment}</p>
|
||||||
{data.comment}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-start mt-2 gap-[28px]">
|
<div className="flex items-center justify-start mt-2 gap-[28px]">
|
||||||
{/* لایک */}
|
{/* لایک */}
|
||||||
<div className="flex items-center justify-start gap-1">
|
<div className="flex items-center justify-start gap-1">
|
||||||
<div
|
<div className="cursor-pointer" onClick={() => sendReq("like")}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleLike(data, "like")}
|
|
||||||
>
|
|
||||||
{loadingLike ? (
|
{loadingLike ? (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
className="!w-[24px] !h-[24px]"
|
className="!w-[24px] !h-[24px]"
|
||||||
sx={{ color: "#7E7E7E" }}
|
sx={{ color: "#7E7E7E" }}
|
||||||
/>
|
/>
|
||||||
) : data.like_status.current_user_like.like ? (
|
) : status.like ? (
|
||||||
<BoldLikeComment />
|
<BoldLikeComment />
|
||||||
) : (
|
) : (
|
||||||
<LikeComment />
|
<LikeComment />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#6C757D] text-[12px] select-none">
|
<p className="text-[#6C757D] text-[12px] select-none">
|
||||||
{data.like_status.like_count || ""}
|
{status.like_count || ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* دیسلایک */}
|
{/* دیسلایک */}
|
||||||
<div className="flex items-center justify-start gap-1">
|
<div className="flex items-center justify-start gap-1">
|
||||||
<div
|
<div className="cursor-pointer" onClick={() => sendReq("dislike")}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleLike(data, "dislike")}
|
|
||||||
>
|
|
||||||
{loadingDislike ? (
|
{loadingDislike ? (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
className="!w-[24px] !h-[24px]"
|
className="!w-[24px] !h-[24px]"
|
||||||
sx={{ color: "#7E7E7E" }}
|
sx={{ color: "#7E7E7E" }}
|
||||||
/>
|
/>
|
||||||
) : data.like_status.current_user_like.dislike ? (
|
) : status.dislike ? (
|
||||||
<BoldDisLikeComment />
|
<BoldDisLikeComment />
|
||||||
) : (
|
) : (
|
||||||
<DisLikeComment />
|
<DisLikeComment />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#6C757D] text-[12px] select-none">
|
<p className="text-[#6C757D] text-[12px] select-none">
|
||||||
{data.like_status.dislike_count || ""}
|
{status.dislike_count || ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{!isReply && (
|
||||||
className="!text-[#495057] !text-[12px] !font-medium"
|
<Button
|
||||||
onClick={() => {
|
className="!text-[#495057] !text-[12px] !font-medium"
|
||||||
if (isUserLoggedIn()) {
|
onClick={toggleAnswer}
|
||||||
data.is_open_answer = !data.is_open_answer;
|
variant="text"
|
||||||
setUpdate(!update);
|
>
|
||||||
} else {
|
پاسخ
|
||||||
alert(
|
</Button>
|
||||||
"warning",
|
)}
|
||||||
"برای ارسال کامنت باید ابتدا وارد حساب کاربری شوید."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
پاسخ
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SendAnswer data={data} />
|
{!isReply && openAnswer && (
|
||||||
{!!data.replies.length && (
|
<SendAnswer
|
||||||
<AnswerField data={data} update={update} setUpdate={setUpdate} />
|
doctor={doctor}
|
||||||
|
parentUuid={data.uuid}
|
||||||
|
onSent={() => setOpenAnswer(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isReply && data.replies?.length > 0 && (
|
||||||
|
<Replies data={data} doctor={doctor} />
|
||||||
)}
|
)}
|
||||||
<Replies data={data} update={update} setUpdate={setUpdate} />
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ItemUser from "./ItemUser";
|
import ItemUser from "./ItemUser";
|
||||||
|
|
||||||
function Replies({ data, update, setUpdate }) {
|
function Replies({ data, doctor }) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul className="flex pr-12 flex-col justify-center mt-12 items-start gap-12 w-full">
|
||||||
className={`flex pr-12 flex-col justify-center transition-all duration-500 overflow-hidden items-start gap-12 w-full
|
{data.replies.map((item) => (
|
||||||
${data.is_open_reply ? "max-h-fit mt-12" : "max-h-0"}`}
|
<ItemUser key={item.uuid} data={item} doctor={doctor} isReply={true} />
|
||||||
>
|
|
||||||
{data.replies.map((item, idx) => (
|
|
||||||
<ItemUser
|
|
||||||
key={idx}
|
|
||||||
data={item}
|
|
||||||
isReply={true}
|
|
||||||
update={update}
|
|
||||||
setUpdate={setUpdate}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,44 +4,42 @@ import { useState } from "react";
|
|||||||
import { request } from "@/services/response";
|
import { request } from "@/services/response";
|
||||||
import { alert } from "@/helper";
|
import { alert } from "@/helper";
|
||||||
|
|
||||||
function SendAnswer({ data }) {
|
function SendAnswer({ doctor, parentUuid, onSent }) {
|
||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const resetData = () => {
|
const sendReq = async () => {
|
||||||
setComment("");
|
if (!comment.trim()) return;
|
||||||
setLoading(false);
|
|
||||||
data.is_open_answer = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendReq = () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const body = {
|
try {
|
||||||
comment,
|
await request.postDoctorComment({
|
||||||
doctor_id: data?.doctor?.id,
|
doctor_uuid: doctor?.uuid,
|
||||||
parent: data?.parent,
|
comment,
|
||||||
};
|
parent: parentUuid,
|
||||||
|
|
||||||
request
|
|
||||||
.postDoctorComment(body)
|
|
||||||
.then(() => {
|
|
||||||
alert(
|
|
||||||
"success",
|
|
||||||
"کامنت شما با موفقیت ثبت شد و پس از تأیید نمایش داده میشود."
|
|
||||||
);
|
|
||||||
resetData();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
|
|
||||||
resetData();
|
|
||||||
});
|
});
|
||||||
|
alert("success", "پاسخ شما ثبت شد و پس از تأیید نمایش داده میشود.");
|
||||||
|
setComment("");
|
||||||
|
onSent?.();
|
||||||
|
} catch (err) {
|
||||||
|
const status = err?.response?.status;
|
||||||
|
const code = err?.response?.data?.errors?.[0]?.code;
|
||||||
|
if (status === 401) {
|
||||||
|
alert("warning", "برای ارسال پاسخ باید ابتدا وارد حساب کاربری شوید.");
|
||||||
|
} else if (status === 403 && code === "ERR_RATING_NOT_ELIGIBLE") {
|
||||||
|
alert(
|
||||||
|
"warning",
|
||||||
|
"فقط کاربرانی که در یک ماه گذشته نزد این پزشک نوبت تاییدشده داشتهاند میتوانند نظر ثبت کنند."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col mt-2 justify-center items-start gap-2 mb-2">
|
||||||
className={`flex flex-col mt-2 justify-center overflow-hidden transition duration-500 items-start gap-2 mb-2
|
|
||||||
${data.is_open_answer ? "max-h-fit" : "max-h-0"}`}
|
|
||||||
>
|
|
||||||
<Field
|
<Field
|
||||||
type="text"
|
type="text"
|
||||||
value={comment}
|
value={comment}
|
||||||
|
|||||||
@@ -1,25 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState } from "react";
|
|
||||||
import ItemUser from "./ItemUser";
|
import ItemUser from "./ItemUser";
|
||||||
|
|
||||||
function Comment({ comments }) {
|
function Comment({ comments, doctor }) {
|
||||||
const [update, setUpdate] = useState(false);
|
if (!comments?.length) {
|
||||||
|
return (
|
||||||
|
<p className="text-[#7E7E7E] text-[14px] font-normal mt-4">
|
||||||
|
هنوز نظری برای این پزشک ثبت نشده است.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul className="flex flex-col comment-list !pl-2.5 justify-start max-h-[492px] overflow-auto items-start gap-12 mt-2">
|
||||||
className={`flex flex-col comment-list !pl-2.5 justify-start max-h-[492px] overflow-auto items-start gap-12 mt-2 ${
|
{comments.map((item) => (
|
||||||
false && "w-full"
|
<ItemUser key={item.uuid} data={item} doctor={doctor} />
|
||||||
}`}
|
))}
|
||||||
>
|
|
||||||
{comments?.length > 0 &&
|
|
||||||
comments.map((item, idx) => (
|
|
||||||
<ItemUser
|
|
||||||
key={idx}
|
|
||||||
data={item}
|
|
||||||
update={update}
|
|
||||||
setUpdate={setUpdate}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,16 +45,19 @@ async function Doctor({ params }) {
|
|||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
let doctor = null;
|
let doctor = null;
|
||||||
let comments = null;
|
let comments = null;
|
||||||
|
let rateAggregate = { point: 0, satisfaction: 0, averages: [] };
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resDoctor = await axiosInstance.get(`${API_URL}/api/v1/doctor/${slug}`);
|
const resDoctor = await axiosInstance.get(`${API_URL}/api/v1/doctor/${slug}`);
|
||||||
doctor = resDoctor.data?.data?.data;
|
doctor = resDoctor.data?.data?.data;
|
||||||
if (doctor) {
|
if (doctor) {
|
||||||
const resComments = await axiosInstance.get(
|
const [resComments, resRate] = await Promise.all([
|
||||||
`${API_URL}/api/v1/comments/${doctor.uuid}`
|
axiosInstance.get(`${API_URL}/api/v1/comments/${doctor.uuid}`),
|
||||||
);
|
axiosInstance.get(`${API_URL}/api/v1/rate/${doctor.uuid}`),
|
||||||
|
]);
|
||||||
comments = resComments?.data?.data?.data;
|
comments = resComments?.data?.data?.data;
|
||||||
|
rateAggregate = resRate?.data?.data?.data ?? rateAggregate;
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
@@ -84,7 +87,12 @@ async function Doctor({ params }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Layout>
|
<Layout>
|
||||||
<DoctorPage doctor={doctor} comments={comments} slug={slug} />
|
<DoctorPage
|
||||||
|
doctor={doctor}
|
||||||
|
comments={comments}
|
||||||
|
rateAggregate={rateAggregate}
|
||||||
|
slug={slug}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import CircularLoading from "@/app/component/loading/Circular";
|
|||||||
import { numberToArStyle } from "@/helper";
|
import { numberToArStyle } from "@/helper";
|
||||||
import { CircularProgress } from "@mui/material";
|
import { CircularProgress } from "@mui/material";
|
||||||
|
|
||||||
function ProgressAll({ doctor }) {
|
function ProgressAll({ satisfaction }) {
|
||||||
return (
|
return (
|
||||||
<div className="hidden lg:flex w-full h-fit relative min-w-[145px] max-w-[150px]">
|
<div className="hidden lg:flex w-full h-fit relative min-w-[145px] max-w-[150px]">
|
||||||
<CircularLoading width={120} height={120}>
|
<CircularLoading width={120} height={120}>
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
value={doctor?.satisfaction}
|
value={satisfaction || 0}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiCircularProgress-circle": {
|
"& .MuiCircularProgress-circle": {
|
||||||
stroke: "#05BA58",
|
stroke: "#05BA58",
|
||||||
@@ -28,7 +28,7 @@ function ProgressAll({ doctor }) {
|
|||||||
رضایت کاربران
|
رضایت کاربران
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#3B3B3B]">
|
<p className="text-[#3B3B3B]">
|
||||||
{numberToArStyle(doctor?.satisfaction)}%
|
{numberToArStyle(satisfaction || 0)}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CircularLoading>
|
</CircularLoading>
|
||||||
|
|||||||
@@ -6,22 +6,24 @@ import ProgressDetail from "@/app/component/ProfressDetail";
|
|||||||
import TextLoading from "@/app/component/loading/Text";
|
import TextLoading from "@/app/component/loading/Text";
|
||||||
import CustomLoading from "@/app/component/loading/Custom";
|
import CustomLoading from "@/app/component/loading/Custom";
|
||||||
|
|
||||||
function Chart({ comments, doctor }) {
|
function Chart({ comments, rateAggregate }) {
|
||||||
const averages = doctor?.average_rate?.averages;
|
const point = rateAggregate?.point ?? 0;
|
||||||
|
const satisfaction = rateAggregate?.satisfaction ?? 0;
|
||||||
|
const averages = rateAggregate?.averages ?? [];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
rounded-lg my-[12px] sm:my-[16px] md:my-[20px] lg:my-[24px] border border-[#EFEFEF] bg-[#FFF]
|
rounded-lg my-[12px] sm:my-[16px] md:my-[20px] lg:my-[24px] border border-[#EFEFEF] bg-[#FFF]
|
||||||
pt-[12px] md:pt-[14px] lg:pt-[16px] pb-[12px] sm:pb-[16px] md:pb-[20px] lg:pb-[24px] px-[12px] sm:px-[16px] md:px-[20px] lg:px-[24px]
|
pt-[12px] md:pt-[14px] lg:pt-[16px] pb-[12px] sm:pb-[16px] md:pb-[20px] lg:pb-[24px] px-[12px] sm:px-[16px] md:px-[20px] lg:px-[24px]
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<TextLoading width={130} height={20}>
|
<TextLoading width={130} height={20}>
|
||||||
<div className="flex items-center justify-start gap-2.5">
|
<div className="flex items-center justify-start gap-2.5">
|
||||||
<p className="text-[#616161] text-[14px] font-medium">
|
<p className="text-[#616161] text-[14px] font-medium">
|
||||||
امتیاز کلی کاربران: {doctor?.point} از 5
|
امتیاز کلی کاربران: {point} از 5
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#7E7E7E] text-[14px] font-normal">
|
<p className="text-[#7E7E7E] text-[14px] font-normal">
|
||||||
(از مجموع {comments?.length} نظر)
|
(از مجموع {comments?.length || 0} نظر)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TextLoading>
|
</TextLoading>
|
||||||
@@ -29,7 +31,8 @@ function Chart({ comments, doctor }) {
|
|||||||
<TextLoading width={160} height={20}>
|
<TextLoading width={160} height={20}>
|
||||||
<Rating
|
<Rating
|
||||||
readOnly
|
readOnly
|
||||||
value={3.5}
|
value={point}
|
||||||
|
precision={0.5}
|
||||||
size="small"
|
size="small"
|
||||||
name="read-only"
|
name="read-only"
|
||||||
className="!mt-4 !mb-6 !hidden lg:!flex"
|
className="!mt-4 !mb-6 !hidden lg:!flex"
|
||||||
@@ -41,26 +44,24 @@ function Chart({ comments, doctor }) {
|
|||||||
<div className="flex p-1 gap-0.5 items-start justify-center">
|
<div className="flex p-1 gap-0.5 items-start justify-center">
|
||||||
<StarDI />
|
<StarDI />
|
||||||
<p className="text-[#616161] text-[12px] md:text-[14px] font-normal">
|
<p className="text-[#616161] text-[12px] md:text-[14px] font-normal">
|
||||||
امتیاز: {doctor?.point}
|
امتیاز: {point}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex p-1 items-start gap-0.5">
|
<div className="flex p-1 items-start gap-0.5">
|
||||||
<LikeDI />
|
<LikeDI />
|
||||||
<p className="text-[#616161] text-[12px] md:text-[14px] font-normal">
|
<p className="text-[#616161] text-[12px] md:text-[14px] font-normal">
|
||||||
{doctor?.satisfaction}% رضایت کاربران
|
{satisfaction}% رضایت کاربران
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CustomLoading>
|
</CustomLoading>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-center gap-[50px]">
|
<div className="flex flex-wrap items-center justify-center gap-[50px]">
|
||||||
<ul className="flex w-full flex-col items-start justify-start gap-1.5 lg:w-fit">
|
<ul className="flex w-full flex-col items-start justify-start gap-1.5 lg:w-fit">
|
||||||
{Object.values(averages ? averages : Array(5).fill(false)).map(
|
{(averages.length ? averages : Array(5).fill(null)).map((item, idx) => (
|
||||||
(item, idx) => (
|
<ProgressDetail data={item} idx={idx} key={idx} />
|
||||||
<ProgressDetail data={item} idx={idx} key={idx} />
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
<ProgressAll doctor={doctor} />
|
<ProgressAll satisfaction={satisfaction} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Chart from "./chart";
|
|||||||
import Comment from "@/app/component/comment";
|
import Comment from "@/app/component/comment";
|
||||||
import ModalAnswer from "./modal/ModalAnswer";
|
import ModalAnswer from "./modal/ModalAnswer";
|
||||||
|
|
||||||
function Comments({ doctor, comments }) {
|
function Comments({ doctor, comments, rateAggregate }) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-[24px] sm:mt-[27px] md:mt-[29px] lg:mt-[32px]">
|
<div className="mt-[24px] sm:mt-[27px] md:mt-[29px] lg:mt-[32px]">
|
||||||
<a
|
<a
|
||||||
@@ -20,8 +20,8 @@ function Comments({ doctor, comments }) {
|
|||||||
</p>
|
</p>
|
||||||
<ModalAnswer doctor={doctor} />
|
<ModalAnswer doctor={doctor} />
|
||||||
</div>
|
</div>
|
||||||
<Chart comments={comments} doctor={doctor} />
|
<Chart comments={comments} rateAggregate={rateAggregate} />
|
||||||
<Comment comments={comments} />
|
<Comment comments={comments} doctor={doctor} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { alert, isUserLoggedIn } from "@/helper";
|
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
function BtnNewComment({ handleOpen }) {
|
function BtnNewComment({ handleOpen }) {
|
||||||
const handleClick = () => {
|
|
||||||
if (isUserLoggedIn()) {
|
|
||||||
handleOpen();
|
|
||||||
} else {
|
|
||||||
alert("warning", "برای ارسال کامنت باید ابتدا وارد حساب کاربری شوید.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleClick}
|
onClick={handleOpen}
|
||||||
className="!py-[6px] md:!py-2 lg:!py-[9px] !px-[16px] sm:!px-[23px] md:!px-[29px]
|
className="!py-[6px] md:!py-2 lg:!py-[9px] !px-[16px] sm:!px-[23px] md:!px-[29px]
|
||||||
lg:!px-[36px] !mr-0 md:!mr-4 !rounded !w-full md:!w-fit"
|
lg:!px-[36px] !mr-0 md:!mr-4 !rounded !w-full md:!w-fit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,46 +7,42 @@ import ProgressDetail from "@/data/progress_detail.json";
|
|||||||
import BtnNewComment from "./BtnNewComment";
|
import BtnNewComment from "./BtnNewComment";
|
||||||
import Content from "./content";
|
import Content from "./content";
|
||||||
import { request } from "@/services/response";
|
import { request } from "@/services/response";
|
||||||
import Cookies from "js-cookie";
|
import { alert, isUserLoggedIn } from "@/helper";
|
||||||
|
|
||||||
function ModalAnswer({ doctor }) {
|
function ModalAnswer({ doctor }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const handleOpen = () => setOpen(true);
|
const [eligible, setEligible] = useState(false);
|
||||||
const handleClose = () => setOpen(false);
|
|
||||||
const [comment, setComment] = useState({
|
const [comment, setComment] = useState({
|
||||||
defaultRate: false,
|
rate: ProgressDetail.map((item) => ({ ...item, progress: 0 })),
|
||||||
uuid: "",
|
detail: { comment: "" },
|
||||||
rate: ProgressDetail,
|
|
||||||
detail: {
|
|
||||||
comment: "",
|
|
||||||
doctor_id: doctor?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isLogged = Cookies.get("access_token");
|
if (!doctor?.uuid || !isUserLoggedIn()) return;
|
||||||
|
|
||||||
if (doctor && doctor.uuid && isLogged) {
|
request
|
||||||
let newRate = comment.rate;
|
.getRateEligibility(doctor.uuid)
|
||||||
request
|
.then((res) => setEligible(!!res.data?.eligible))
|
||||||
.getDoctorRate(doctor.uuid)
|
.catch(() => setEligible(false));
|
||||||
.then((res) => {
|
}, [doctor?.uuid]);
|
||||||
if (res.doctor_id) {
|
|
||||||
newRate.map((item) => (item.progress = res[item.name]));
|
const handleOpen = () => {
|
||||||
setComment({
|
if (!isUserLoggedIn()) {
|
||||||
...comment,
|
alert("warning", "برای ثبت نظر باید ابتدا وارد حساب کاربری شوید.");
|
||||||
defaultRate: true,
|
return;
|
||||||
uuid: res.uuid,
|
|
||||||
rate: newRate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
//
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, []);
|
if (!eligible) {
|
||||||
|
alert(
|
||||||
|
"warning",
|
||||||
|
"فقط کاربرانی که در یک ماه گذشته نزد این پزشک نوبت تاییدشده داشتهاند میتوانند نظر ثبت کنند."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,62 +2,51 @@ import React, { useState } from "react";
|
|||||||
import Field from "@/app/component/fields/Field";
|
import Field from "@/app/component/fields/Field";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { request } from "@/services/response";
|
import { request } from "@/services/response";
|
||||||
|
import { alert } from "@/helper";
|
||||||
import ArrowLeftD from "@/components/icons/ArrowLeftD";
|
import ArrowLeftD from "@/components/icons/ArrowLeftD";
|
||||||
|
|
||||||
function Form({
|
function Form({ doctor, setOpen, comment, loading, changeData, setComment, setLoading }) {
|
||||||
doctor,
|
|
||||||
setOpen,
|
|
||||||
comment,
|
|
||||||
loading,
|
|
||||||
changeData,
|
|
||||||
setComment,
|
|
||||||
setLoading,
|
|
||||||
}) {
|
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
const sendReq = () => {
|
const handleRatingError = (err) => {
|
||||||
if (comment?.detail?.comment) {
|
const status = err?.response?.status;
|
||||||
setLoading(true);
|
const code = err?.response?.data?.errors?.[0]?.code;
|
||||||
|
if (status === 401) {
|
||||||
request
|
alert("warning", "برای ثبت نظر باید ابتدا وارد حساب کاربری شوید.");
|
||||||
.postDoctorComment(comment.detail)
|
} else if (status === 403 && code === "ERR_RATING_NOT_ELIGIBLE") {
|
||||||
.then(() => {
|
alert(
|
||||||
let newComment = comment.rate;
|
"warning",
|
||||||
newComment = {
|
"فقط کاربرانی که در یک ماه گذشته نزد این پزشک نوبت تاییدشده داشتهاند میتوانند نظر ثبت کنند."
|
||||||
doctor_id: doctor?.id,
|
);
|
||||||
accuracy_of_diagnosis: newComment[1].progress,
|
|
||||||
doctor_expertise: newComment[4].progress,
|
|
||||||
doctor_behavior: newComment[2].progress,
|
|
||||||
clinic_cleanliness: newComment[3].progress,
|
|
||||||
waiting_time_at_clinic: newComment[0].progress,
|
|
||||||
};
|
|
||||||
setComment({
|
|
||||||
...comment,
|
|
||||||
detail: {
|
|
||||||
...comment.detail,
|
|
||||||
comment: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
request[comment.defaultRate ? "patchDoctorRate" : "postDoctorRate"](
|
|
||||||
newComment,
|
|
||||||
comment.uuid
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
setLoading(false);
|
|
||||||
setOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLoading(false);
|
|
||||||
setOpen(false);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLoading(false);
|
|
||||||
setOpen(false);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
|
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendReq = async () => {
|
||||||
|
const text = comment?.detail?.comment;
|
||||||
|
if (!text) {
|
||||||
setIsError(true);
|
setIsError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await request.postDoctorComment({ doctor_uuid: doctor.uuid, comment: text });
|
||||||
|
|
||||||
|
const rate = { doctor_uuid: doctor.uuid };
|
||||||
|
comment.rate.forEach((item) => {
|
||||||
|
rate[item.name] = item.progress;
|
||||||
|
});
|
||||||
|
await request.postDoctorRate(rate);
|
||||||
|
|
||||||
|
setComment({ ...comment, detail: { ...comment.detail, comment: "" } });
|
||||||
|
alert("success", "نظر شما ثبت شد و پس از تأیید نمایش داده میشود.");
|
||||||
|
setOpen(false);
|
||||||
|
} catch (err) {
|
||||||
|
handleRatingError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,7 +60,7 @@ function Form({
|
|||||||
<div className="flex flex-col gap-[16px] justify-center items-start mt-[24px]">
|
<div className="flex flex-col gap-[16px] justify-center items-start mt-[24px]">
|
||||||
<p className="text-[#495057] text-[16px] font-bold">متن نظر</p>
|
<p className="text-[#495057] text-[16px] font-bold">متن نظر</p>
|
||||||
<Field
|
<Field
|
||||||
type="number"
|
type="text"
|
||||||
error={isError}
|
error={isError}
|
||||||
multiline={true}
|
multiline={true}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function Content({
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<p className="text-[16px] text-[#525252] font-bold">
|
<p className="text-[16px] text-[#525252] font-bold">
|
||||||
امتیاز شما به دکتر مهران امینی
|
امتیاز شما به {doctor?.name}
|
||||||
</p>
|
</p>
|
||||||
<div className="cursor-pointer" onClick={handleClose}>
|
<div className="cursor-pointer" onClick={handleClose}>
|
||||||
<CloseModalD />
|
<CloseModalD />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import AboutDcotor from "./cards/AboutDcotor";
|
|||||||
import Comments from "./cards/comments";
|
import Comments from "./cards/comments";
|
||||||
import Locations from "./cards/locations";
|
import Locations from "./cards/locations";
|
||||||
|
|
||||||
function DetailDoctor({ doctor, comments }) {
|
function DetailDoctor({ doctor, comments, rateAggregate }) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full lg:w-[59%]">
|
<div className="w-full lg:w-[59%]">
|
||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
@@ -15,7 +15,11 @@ function DetailDoctor({ doctor, comments }) {
|
|||||||
<Link />
|
<Link />
|
||||||
<AboutDcotor doctor={doctor} />
|
<AboutDcotor doctor={doctor} />
|
||||||
<Locations doctor={doctor} />
|
<Locations doctor={doctor} />
|
||||||
<Comments doctor={doctor} comments={comments} />
|
<Comments
|
||||||
|
doctor={doctor}
|
||||||
|
comments={comments}
|
||||||
|
rateAggregate={rateAggregate}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import DetailDoctor from "./detailDoctor";
|
|||||||
import Share from "./detailDoctor/Share";
|
import Share from "./detailDoctor/Share";
|
||||||
import CustomLoading from "@/app/component/loading/Custom";
|
import CustomLoading from "@/app/component/loading/Custom";
|
||||||
|
|
||||||
function DoctorPage({ doctor, comments, slug }) {
|
function DoctorPage({ doctor, comments, rateAggregate, slug }) {
|
||||||
return (
|
return (
|
||||||
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
|
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -23,7 +23,11 @@ function DoctorPage({ doctor, comments, slug }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-center gap-6 mt-[30px]">
|
<div className="flex items-start justify-center gap-6 mt-[30px]">
|
||||||
<DetailDoctor doctor={doctor} comments={comments} />
|
<DetailDoctor
|
||||||
|
doctor={doctor}
|
||||||
|
comments={comments}
|
||||||
|
rateAggregate={rateAggregate}
|
||||||
|
/>
|
||||||
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,11 +89,12 @@ export const request = {
|
|||||||
api.get("api/v1/categorys/doctor_services", removeTokenHead),
|
api.get("api/v1/categorys/doctor_services", removeTokenHead),
|
||||||
postDoctorComment: (data) => api.post("api/v1/comment", data, { requireAuth: true }),
|
postDoctorComment: (data) => api.post("api/v1/comment", data, { requireAuth: true }),
|
||||||
getDoctorRate: (uuid) => api.get(`api/v1/rate/${uuid}`),
|
getDoctorRate: (uuid) => api.get(`api/v1/rate/${uuid}`),
|
||||||
|
getRateEligibility: (uuid) =>
|
||||||
|
api.get(`api/v1/rate/${uuid}/eligibility`, { requireAuth: true }),
|
||||||
getDoctorComments: (uuid) => api.get(`api/v1/comments/${uuid}`),
|
getDoctorComments: (uuid) => api.get(`api/v1/comments/${uuid}`),
|
||||||
postDoctorRate: (data) => api.post("api/v1/rate", data, { requireAuth: true }),
|
postDoctorRate: (data) => api.post("api/v1/rate", data, { requireAuth: true }),
|
||||||
patchDoctorRate: (data) => api.post("api/v1/rate", data, { requireAuth: true }),
|
postCommentsLike: (commentUuid, value) =>
|
||||||
postCommentsLike: (commentUuid) =>
|
api.post(`api/v1/like/${commentUuid}`, { value }, { requireAuth: true }),
|
||||||
api.post(`api/v1/like/${commentUuid}`, null, { requireAuth: true }),
|
|
||||||
getClinicList: (params) =>
|
getClinicList: (params) =>
|
||||||
api.get("/api/v1/clinics", {
|
api.get("/api/v1/clinics", {
|
||||||
params,
|
params,
|
||||||
|
|||||||
Reference in New Issue
Block a user