feat: Enhance specialty handling and navigation across doctor and specialty pages

This commit is contained in:
hamed
2026-08-08 17:33:13 +03:30
parent 56e264e44c
commit 79747c443c
12 changed files with 435 additions and 27 deletions
@@ -5,6 +5,7 @@ import GreenCalendarTurn from "@/components/icons/GreenCalendarTurn";
import { Button } from "@mui/material";
import Link from "next/link";
import DoctorAvatar from "@/app/component/DoctorAvatar";
import SpecialtyChips from "@/app/component/SpecialtyChips";
import { bookingState } from "./bookingState";
function ItemAppointment({ turn, loading, doctorSlug, booking }) {
@@ -26,13 +27,9 @@ function ItemAppointment({ turn, loading, doctorSlug, booking }) {
<p className="text-[#3B3B3B] text-[14px] font-bold">{turn?.name}</p>
</TextLoading>
<TextLoading width={65} height={15} loading={loading}>
<p className="text-[#525252] text-[14px] font-medium">
تخصص:
{turn?.specialties?.map(
(item, idx) =>
`${item.name} ${turn.specialties.length === idx + 1 ? "" : "|"} `
)}
</p>
{/* ستون نوبت‌دهی باریک است؛ همان الگوی کارت پزشک — تخصص اصلی و
شمارندهٔ بقیه — تا شش نام پشت‌سرهم ارتفاعش را چند برابر نکند. */}
<SpecialtyChips specialties={turn?.specialties} />
</TextLoading>
</div>
</div>
+14 -6
View File
@@ -46,12 +46,20 @@ function Title({ doctor }) {
</div>
</TextLoading>
<TextLoading width={100} height={20}>
<h2 className="text-[#525252] text-[14px] md:text-[16px] font-medium">
تخصص:
{doctor?.specialties?.map(
(item, idx) =>
`${item.name} ${doctor.specialties.length === idx + 1 ? "" : "|"} `
)}
{/* اینجا جای فهرست کاملِ تخصص‌هاست — سربرگ صفحه فقط تخصص اصلی را نشان
می‌دهد. چیپ‌های شکننده جای رشتهٔ درهم نشستند: پیش از این خروجی
«تخصص:جراحی عمومی | … | » بود، بدون فاصله بعد از دونقطه و با
جداکنندهٔ آویزان در انتها. */}
<h2 className="flex flex-wrap items-center gap-[6px] text-[#525252] text-[14px] md:text-[16px] font-medium">
<span>تخصص:</span>
{doctor?.specialties?.map((item) => (
<span
key={item.id ?? item.name}
className="px-[10px] py-[3px] rounded-md bg-[#F8F8FF] text-[13px] md:text-[14px] font-normal"
>
{item.name}
</span>
))}
</h2>
</TextLoading>
<div className="flex items-center justify-start gap-11">
+66
View File
@@ -0,0 +1,66 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import DoctorPage from "@/components/doctor";
vi.mock("@/components/doctor/appointmentList", () => ({ default: () => null }));
vi.mock("@/components/doctor/detailDoctor", () => ({ default: () => null }));
vi.mock("@/components/doctor/claim", () => ({ default: () => null }));
vi.mock("@/components/doctor/detailDoctor/Share", () => ({ default: () => null }));
vi.mock("@/components/specialties/detail/Faq", () => ({ default: () => null }));
// همان شش تخصص دکتر محمدباقر جهانتاب.
const JAHANTAB = {
name: "محمدباقر جهانتاب",
specialties: [
{ id: "13", name: "جراحی عمومی", parent_id: null },
{ id: "14", name: "جراحی پلاستیک و زیبایی", parent_id: "13" },
{ id: "167", name: "جراحی لاپاراسکوپی", parent_id: "13" },
{ id: "168", name: "جراح تیروئید", parent_id: "13" },
{ id: "169", name: "جراح گوارش", parent_id: "13" },
{ id: "170", name: "جراحی سرطانها", parent_id: "13" },
],
};
describe("سربرگ صفحهٔ پزشک", () => {
it("فقط تخصص اصلی را نشان می‌دهد، نه هر شش تا", () => {
render(<DoctorPage doctor={JAHANTAB} />);
expect(screen.getByRole("link", { name: "جراحی عمومی" })).toBeInTheDocument();
// بقیه اینجا نمی‌آیند؛ فهرست کامل در بدنهٔ صفحه است و آنجا می‌شکند.
expect(screen.queryByText("جراح گوارش")).not.toBeInTheDocument();
expect(screen.queryByText("جراحی سرطانها")).not.toBeInTheDocument();
});
it("تخصص اصلی به صفحهٔ فرود همان تخصص لینک می‌دهد", () => {
render(<DoctorPage doctor={JAHANTAB} />);
expect(screen.getByRole("link", { name: "جراحی عمومی" })).toHaveAttribute(
"href",
"/specialties/general-surgery"
);
});
it("نام پزشک با عنوان نمایش داده می‌شود", () => {
render(<DoctorPage doctor={JAHANTAB} />);
expect(screen.getByText(/محمدباقر جهانتاب/)).toBeInTheDocument();
});
it("پزشکِ فقط-زیرتخصص، همان زیرتخصص را در سربرگ می‌گیرد", () => {
render(
<DoctorPage
doctor={{ ...JAHANTAB, specialties: [{ id: "169", name: "جراح گوارش", parent_id: "13" }] }}
/>
);
expect(screen.getByRole("link", { name: "جراح گوارش" })).toBeInTheDocument();
});
it("پزشک بدون تخصص سربرگ خالی می‌گیرد و کرش نمی‌کند", () => {
expect(() =>
render(<DoctorPage doctor={{ ...JAHANTAB, specialties: [] }} />)
).not.toThrow();
expect(screen.getByText(/محمدباقر جهانتاب/)).toBeInTheDocument();
});
});
+17 -12
View File
@@ -6,6 +6,7 @@ import Share from "./detailDoctor/Share";
import CustomLoading from "@/app/component/loading/Custom";
import Faq from "@/components/specialties/detail/Faq";
import specialtiesData from "@/data/specialties.json";
import { splitSpecialties } from "@/lib/specialtyDisplay";
import { doctorTitle } from "@/helper";
// مقصد تمیز صفحهٔ فرود تخصص؛ اگر تخصص slug نداشت، لیست پزشکان با کوئری قدیمی.
@@ -15,23 +16,27 @@ const specialtyHref = (name) => {
};
function DoctorPage({ doctor, comments, rateAggregate, addresses, bookableAddressUuids = [], bookingLocations = [], slug, faq = [] }) {
const { primary: primarySpecialty } = splitSpecialties(doctor?.specialties);
return (
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
<div className="flex items-center justify-between">
<CustomLoading width={180} height={25}>
<div className="flex items-center justify-start text-[14px] md:text-[16px] font-normal">
<h2 className="text-[#9B9B9B] text-nowrap flex items-center">
{doctor?.specialties?.map((item, idx) => (
<span key={item.id ?? item.name} className="flex items-center">
<Link href={specialtyHref(item.name)} className="hover:text-[#525252]">
{item.name}
</Link>
<span className="mx-1">
{doctor.specialties.length === idx + 1 ? ">" : "|"}
</span>
</span>
))}
</h2>
{/* این خط یک breadcrumb است، نه فهرست تخصص. چاپ هر شش تخصص با
text-nowrap در موبایل از عرض صفحه بیرون می‌زد. فهرست کامل چند خط
پایین‌تر در Title.js می‌آید و آنجا می‌شکند. */}
{primarySpecialty && (
<h2 className="text-[#9B9B9B] text-nowrap flex items-center min-w-0">
<Link
href={specialtyHref(primarySpecialty.name)}
className="hover:text-[#525252] truncate"
>
{primarySpecialty.name}
</Link>
<span className="mx-1">{">"}</span>
</h2>
)}
<p className="text-[#525252] text-nowrap mr-1">{doctorTitle(doctor?.name)}</p>
</div>
</CustomLoading>
+75
View File
@@ -0,0 +1,75 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import Title from "@/components/doctor/detailDoctor/Title";
import ItemAppointment from "@/components/doctor/appointmentList/ItemAppointment";
vi.mock("@/components/doctor/detailDoctor/Share", () => ({ default: () => null }));
// همان شش تخصص دکتر محمدباقر جهانتاب.
const SPECIALTIES = [
{ id: "13", name: "جراحی عمومی", parent_id: null },
{ id: "14", name: "جراحی پلاستیک و زیبایی", parent_id: "13" },
{ id: "167", name: "جراحی لاپاراسکوپی", parent_id: "13" },
{ id: "168", name: "جراح تیروئید", parent_id: "13" },
{ id: "169", name: "جراح گوارش", parent_id: "13" },
{ id: "170", name: "جراحی سرطانها", parent_id: "13" },
];
const DOCTOR = { name: "محمدباقر جهانتاب", specialties: SPECIALTIES, experience: 5 };
describe("بدنهٔ پروفایل — فهرست کامل تخصص‌ها", () => {
it("هر شش تخصص را جدا نشان می‌دهد، نه یک رشتهٔ درهم", () => {
render(<Title doctor={DOCTOR} />);
for (const s of SPECIALTIES) {
expect(screen.getByText(s.name)).toBeInTheDocument();
}
});
it("بعد از «تخصص:» نام نمی‌چسبد", () => {
// باگ قبلی: خروجی «تخصص:جراحی عمومی» بود.
render(<Title doctor={DOCTOR} />);
expect(screen.getByText("تخصص:")).toBeInTheDocument();
expect(screen.queryByText(/تخصص:جراحی/)).not.toBeInTheDocument();
});
it("جداکنندهٔ آویزان در انتها ندارد", () => {
const { container } = render(<Title doctor={DOCTOR} />);
expect(container.textContent).not.toMatch(/\|\s*$/);
expect(container.textContent).not.toContain("|");
});
it("پزشک بدون تخصص فقط برچسب را نشان می‌دهد و کرش نمی‌کند", () => {
expect(() =>
render(<Title doctor={{ ...DOCTOR, specialties: [] }} />)
).not.toThrow();
});
});
describe("ستون نوبت‌دهی — تخصص اصلی و شمارنده", () => {
const turn = { ...DOCTOR, uuid: "u-1", specialties: SPECIALTIES };
it("فقط تخصص اصلی و شمارندهٔ بقیه را نشان می‌دهد", () => {
render(<ItemAppointment turn={turn} doctorSlug="u-1" />);
expect(screen.getByText(/جراحی عمومی/)).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "نمایش 5 تخصص دیگر" })
).toBeInTheDocument();
expect(screen.queryByText("جراح گوارش")).not.toBeInTheDocument();
});
it("جداکنندهٔ آویزان ندارد", () => {
const { container } = render(<ItemAppointment turn={turn} doctorSlug="u-1" />);
expect(container.textContent).not.toContain("|");
});
it("پزشک بدون تخصص کرش نمی‌کند", () => {
expect(() =>
render(<ItemAppointment turn={{ ...turn, specialties: [] }} doctorSlug="u-1" />)
).not.toThrow();
});
});