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:
@@ -1,7 +1,7 @@
|
||||
import { numberToArStyle } from "@/helper";
|
||||
import { LinearProgress } from "@mui/material";
|
||||
|
||||
const label = [
|
||||
const fallbackLabels = [
|
||||
"برخورد مناسب پزشک",
|
||||
"تشخیص درست",
|
||||
"زمان انتظار در مطب",
|
||||
@@ -10,13 +10,16 @@ const label = [
|
||||
];
|
||||
|
||||
function ProgressDetail({ data, idx }) {
|
||||
const label = data?.label ?? fallbackLabels[idx];
|
||||
const progress = typeof data === "object" && data !== null ? data.progress : data;
|
||||
|
||||
return (
|
||||
<li className="flex items-center justify-start gap-2 w-full">
|
||||
<p className="text-[#525252] text-[14px] font-normal min-w-[120px] w-[118px]">
|
||||
{label[idx]}
|
||||
{label}
|
||||
</p>
|
||||
<LinearProgress
|
||||
value={data || 0}
|
||||
value={progress || 0}
|
||||
variant="determinate"
|
||||
className="!w-full lg:!w-[265px] !rounded-[10px] !bg-[#D7D7D7] !h-[11px]"
|
||||
sx={{
|
||||
@@ -27,9 +30,9 @@ function ProgressDetail({ data, idx }) {
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
</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 { imageUrl } from "@/helper";
|
||||
import { Button, CircularProgress } from "@mui/material";
|
||||
@@ -8,62 +10,53 @@ import BoldLikeComment from "@/components/icons/BoldLikeComment";
|
||||
import DisLikeComment from "@/components/icons/DisLikeComment";
|
||||
import LikeComment from "@/components/icons/LikeComment";
|
||||
import Replies from "./Replies";
|
||||
import AnswerField from "./AnswerField";
|
||||
import SendAnswer from "./SendAnswer";
|
||||
import { alert, isUserLoggedIn, timeAgo } from "@/helper";
|
||||
import { request } from "@/services/response";
|
||||
import { useState } from "react";
|
||||
|
||||
function ItemUser({ update, setUpdate, data }) {
|
||||
function ItemUser({ data, doctor, isReply = false }) {
|
||||
const [loadingLike, setLoadingLike] = 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);
|
||||
else setLoadingDislike(true);
|
||||
|
||||
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) {
|
||||
// err
|
||||
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
|
||||
} finally {
|
||||
if (type === "like") setLoadingLike(false);
|
||||
else setLoadingDislike(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLike = (data, type) => {
|
||||
SendReq(type);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
const toggleAnswer = () => {
|
||||
if (isUserLoggedIn()) {
|
||||
setOpenAnswer((prev) => !prev);
|
||||
} else {
|
||||
if (current_user_like.dislike) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
alert("warning", "برای ارسال پاسخ باید ابتدا وارد حساب کاربری شوید.");
|
||||
}
|
||||
|
||||
setUpdate(!update);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -78,7 +71,7 @@ function ItemUser({ update, setUpdate, data }) {
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-center gap-[8px]">
|
||||
<p className="text-[#343A40] text-[14px] md:text-[16px] font-medium">
|
||||
{data.author.real_name}
|
||||
{data.author?.real_name}
|
||||
</p>
|
||||
<p className="text-[#7E7E7E] text-[12px] md:text-[14px] font-medium">
|
||||
{timeAgo(data.created)}
|
||||
@@ -86,79 +79,68 @@ function ItemUser({ update, setUpdate, data }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<p className="text-[#495057] text-[12px] md:text-[14px]">
|
||||
{data.comment}
|
||||
</p>
|
||||
<p className="text-[#495057] text-[12px] md:text-[14px]">{data.comment}</p>
|
||||
|
||||
<div className="flex items-center justify-start mt-2 gap-[28px]">
|
||||
{/* لایک */}
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleLike(data, "like")}
|
||||
>
|
||||
<div className="cursor-pointer" onClick={() => sendReq("like")}>
|
||||
{loadingLike ? (
|
||||
<CircularProgress
|
||||
className="!w-[24px] !h-[24px]"
|
||||
sx={{ color: "#7E7E7E" }}
|
||||
/>
|
||||
) : data.like_status.current_user_like.like ? (
|
||||
) : status.like ? (
|
||||
<BoldLikeComment />
|
||||
) : (
|
||||
<LikeComment />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[#6C757D] text-[12px] select-none">
|
||||
{data.like_status.like_count || ""}
|
||||
{status.like_count || ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* دیسلایک */}
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleLike(data, "dislike")}
|
||||
>
|
||||
<div className="cursor-pointer" onClick={() => sendReq("dislike")}>
|
||||
{loadingDislike ? (
|
||||
<CircularProgress
|
||||
className="!w-[24px] !h-[24px]"
|
||||
sx={{ color: "#7E7E7E" }}
|
||||
/>
|
||||
) : data.like_status.current_user_like.dislike ? (
|
||||
) : status.dislike ? (
|
||||
<BoldDisLikeComment />
|
||||
) : (
|
||||
<DisLikeComment />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[#6C757D] text-[12px] select-none">
|
||||
{data.like_status.dislike_count || ""}
|
||||
{status.dislike_count || ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="!text-[#495057] !text-[12px] !font-medium"
|
||||
onClick={() => {
|
||||
if (isUserLoggedIn()) {
|
||||
data.is_open_answer = !data.is_open_answer;
|
||||
setUpdate(!update);
|
||||
} else {
|
||||
alert(
|
||||
"warning",
|
||||
"برای ارسال کامنت باید ابتدا وارد حساب کاربری شوید."
|
||||
);
|
||||
}
|
||||
}}
|
||||
variant="text"
|
||||
>
|
||||
پاسخ
|
||||
</Button>
|
||||
{!isReply && (
|
||||
<Button
|
||||
className="!text-[#495057] !text-[12px] !font-medium"
|
||||
onClick={toggleAnswer}
|
||||
variant="text"
|
||||
>
|
||||
پاسخ
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SendAnswer data={data} />
|
||||
{!!data.replies.length && (
|
||||
<AnswerField data={data} update={update} setUpdate={setUpdate} />
|
||||
{!isReply && openAnswer && (
|
||||
<SendAnswer
|
||||
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>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import React from "react";
|
||||
import ItemUser from "./ItemUser";
|
||||
|
||||
function Replies({ data, update, setUpdate }) {
|
||||
function Replies({ data, doctor }) {
|
||||
return (
|
||||
<ul
|
||||
className={`flex pr-12 flex-col justify-center transition-all duration-500 overflow-hidden items-start gap-12 w-full
|
||||
${data.is_open_reply ? "max-h-fit mt-12" : "max-h-0"}`}
|
||||
>
|
||||
{data.replies.map((item, idx) => (
|
||||
<ItemUser
|
||||
key={idx}
|
||||
data={item}
|
||||
isReply={true}
|
||||
update={update}
|
||||
setUpdate={setUpdate}
|
||||
/>
|
||||
<ul className="flex pr-12 flex-col justify-center mt-12 items-start gap-12 w-full">
|
||||
{data.replies.map((item) => (
|
||||
<ItemUser key={item.uuid} data={item} doctor={doctor} isReply={true} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -4,44 +4,42 @@ import { useState } from "react";
|
||||
import { request } from "@/services/response";
|
||||
import { alert } from "@/helper";
|
||||
|
||||
function SendAnswer({ data }) {
|
||||
function SendAnswer({ doctor, parentUuid, onSent }) {
|
||||
const [comment, setComment] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const resetData = () => {
|
||||
setComment("");
|
||||
setLoading(false);
|
||||
data.is_open_answer = false;
|
||||
};
|
||||
|
||||
const sendReq = () => {
|
||||
const sendReq = async () => {
|
||||
if (!comment.trim()) return;
|
||||
setLoading(true);
|
||||
const body = {
|
||||
comment,
|
||||
doctor_id: data?.doctor?.id,
|
||||
parent: data?.parent,
|
||||
};
|
||||
|
||||
request
|
||||
.postDoctorComment(body)
|
||||
.then(() => {
|
||||
alert(
|
||||
"success",
|
||||
"کامنت شما با موفقیت ثبت شد و پس از تأیید نمایش داده میشود."
|
||||
);
|
||||
resetData();
|
||||
})
|
||||
.catch(() => {
|
||||
alert("error", "خطایی رخ داد! لطفاً دوباره تلاش کنید.");
|
||||
resetData();
|
||||
try {
|
||||
await request.postDoctorComment({
|
||||
doctor_uuid: doctor?.uuid,
|
||||
comment,
|
||||
parent: parentUuid,
|
||||
});
|
||||
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 (
|
||||
<div
|
||||
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"}`}
|
||||
>
|
||||
<div className="flex flex-col mt-2 justify-center items-start gap-2 mb-2">
|
||||
<Field
|
||||
type="text"
|
||||
value={comment}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import ItemUser from "./ItemUser";
|
||||
|
||||
function Comment({ comments }) {
|
||||
const [update, setUpdate] = useState(false);
|
||||
function Comment({ comments, doctor }) {
|
||||
if (!comments?.length) {
|
||||
return (
|
||||
<p className="text-[#7E7E7E] text-[14px] font-normal mt-4">
|
||||
هنوز نظری برای این پزشک ثبت نشده است.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={`flex flex-col comment-list !pl-2.5 justify-start max-h-[492px] overflow-auto items-start gap-12 mt-2 ${
|
||||
false && "w-full"
|
||||
}`}
|
||||
>
|
||||
{comments?.length > 0 &&
|
||||
comments.map((item, idx) => (
|
||||
<ItemUser
|
||||
key={idx}
|
||||
data={item}
|
||||
update={update}
|
||||
setUpdate={setUpdate}
|
||||
/>
|
||||
))}
|
||||
<ul 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) => (
|
||||
<ItemUser key={item.uuid} data={item} doctor={doctor} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user