diff --git a/.claude/prompt/rating-rich-ui-wiring.md b/.claude/prompt/rating-rich-ui-wiring.md new file mode 100644 index 0000000..cf78ddb --- /dev/null +++ b/.claude/prompt/rating-rich-ui-wiring.md @@ -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]` هر دو باید کامپایل شوند. diff --git a/app/component/ProfressDetail.js b/app/component/ProfressDetail.js index 06c0be6..0e0a5d5 100644 --- a/app/component/ProfressDetail.js +++ b/app/component/ProfressDetail.js @@ -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 (
  • - {label[idx]} + {label}

    - {data ? `${numberToArStyle(data)}%` : "بدون نمره"} + {progress ? `${numberToArStyle(progress)}%` : "بدون نمره"}

  • ); diff --git a/app/component/comment/AnswerField.js b/app/component/comment/AnswerField.js deleted file mode 100644 index 3e9fd32..0000000 --- a/app/component/comment/AnswerField.js +++ /dev/null @@ -1,27 +0,0 @@ -import ArrowUpComment from "@/components/icons/ArrowUpComment"; -import { Button } from "@mui/material"; - -function AnswerField({ data, update, setUpdate }) { - return ( -
    - -
    - ); -} - -export default AnswerField; diff --git a/app/component/comment/ItemUser.js b/app/component/comment/ItemUser.js index 84b422d..a8e7f18 100644 --- a/app/component/comment/ItemUser.js +++ b/app/component/comment/ItemUser.js @@ -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 }) { />

    - {data.author.real_name} + {data.author?.real_name}

    {timeAgo(data.created)} @@ -86,79 +79,68 @@ function ItemUser({ update, setUpdate, data }) {

    -

    - {data.comment} -

    +

    {data.comment}

    {/* لایک */}
    -
    handleLike(data, "like")} - > +
    sendReq("like")}> {loadingLike ? ( - ) : data.like_status.current_user_like.like ? ( + ) : status.like ? ( ) : ( )}

    - {data.like_status.like_count || ""} + {status.like_count || ""}

    {/* دیس‌لایک */}
    -
    handleLike(data, "dislike")} - > +
    sendReq("dislike")}> {loadingDislike ? ( - ) : data.like_status.current_user_like.dislike ? ( + ) : status.dislike ? ( ) : ( )}

    - {data.like_status.dislike_count || ""} + {status.dislike_count || ""}

    - + {!isReply && ( + + )}
    - - {!!data.replies.length && ( - + {!isReply && openAnswer && ( + setOpenAnswer(false)} + /> + )} + {!isReply && data.replies?.length > 0 && ( + )} -
    ); diff --git a/app/component/comment/Replies.js b/app/component/comment/Replies.js index c8e589f..0b3dca1 100644 --- a/app/component/comment/Replies.js +++ b/app/component/comment/Replies.js @@ -1,20 +1,11 @@ import React from "react"; import ItemUser from "./ItemUser"; -function Replies({ data, update, setUpdate }) { +function Replies({ data, doctor }) { return ( -
    ); } diff --git a/components/doctor/detailDoctor/cards/comments/modal/BtnNewComment.js b/components/doctor/detailDoctor/cards/comments/modal/BtnNewComment.js index bd2a9b1..1d3d0ac 100644 --- a/components/doctor/detailDoctor/cards/comments/modal/BtnNewComment.js +++ b/components/doctor/detailDoctor/cards/comments/modal/BtnNewComment.js @@ -1,20 +1,11 @@ -import { alert, isUserLoggedIn } from "@/helper"; import { Button } from "@mui/material"; import React from "react"; function BtnNewComment({ handleOpen }) { - const handleClick = () => { - if (isUserLoggedIn()) { - handleOpen(); - } else { - alert("warning", "برای ارسال کامنت باید ابتدا وارد حساب کاربری شوید."); - } - }; - return (