feat(booking): show services and duration on the user panel appointment
A patient who booked in service mode could not see which services they had reserved or how long the appointment was. Both now appear on the list card and in both detail layouts, reading service_items and service_total_minutes from GET /api/v1/appointments/user. Every field is behind an explicit guard. Slot-mode appointments carry none of them, and an unguarded map would crash the card for every slot-mode appointment, taking the whole panel with it. A reserve entry shows its services but not a duration, because it has no time. Covered by Card.test.jsx: slot-mode appointments render unchanged, absent fields (older backend) do not crash, reserve hides the duration, and a zero duration produces no row. Task: clinicpro/docs/new_feture/taskes/task-00b-nobat724-service-mode/ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,25 @@ function Card({ data, loading, setIsTurnsDetails }) {
|
||||
</p>
|
||||
</TextLoading>
|
||||
</li>
|
||||
|
||||
{/* فیلدهای حالت نوبتدهی سرویسی. شرط اجباری است: نوبت اسلاتی اینها را ندارد و
|
||||
بدون شرط، کارتِ همهٔ نوبتهای اسلاتی میشکند — یعنی کل پنل کاربر. */}
|
||||
{data.service_items?.length > 0 && (
|
||||
<li className="flex items-start justify-between gap-[8px]">
|
||||
<p className="text-[#7E7E7E] text-[14px] font-normal shrink-0">سرویس:</p>
|
||||
<p className="text-[#525252] text-[14px] font-medium text-left">
|
||||
{data.service_items.map((s) => s.name).join("، ")}
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
{!data.is_reserve && data.service_total_minutes ? (
|
||||
<li className="flex items-center justify-between">
|
||||
<p className="text-[#7E7E7E] text-[14px] font-normal">مدت نوبت:</p>
|
||||
<p className="text-[#525252] text-[14px] font-medium">
|
||||
{data.service_total_minutes} دقیقه
|
||||
</p>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
<div className="mt-[12px] flex items-center justify-end px-[12px]">
|
||||
<TextLoading loading={loading} width={80} height={25}>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import Card from "./Card";
|
||||
|
||||
// آیکن و لودینگها به این تست ربطی ندارند؛ فقط باید کودکشان را رندر کنند.
|
||||
vi.mock("@/app/component/loading/Text", () => ({
|
||||
default: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
vi.mock("@/app/component/loading/Circular", () => ({
|
||||
default: ({ children }) => <>{children}</>,
|
||||
}));
|
||||
vi.mock("@/components/icons/ArrowLeftCardD", () => ({ default: () => null }));
|
||||
vi.mock("next/image", () => ({ default: () => null }));
|
||||
|
||||
const base = {
|
||||
uuid: "ap1",
|
||||
status: "confirmed",
|
||||
slot_start: 1785567600,
|
||||
doctor: { name: "دکتر تست" },
|
||||
};
|
||||
|
||||
function renderCard(data) {
|
||||
return render(<Card data={{ ...base, ...data }} loading={false} setIsTurnsDetails={() => {}} />);
|
||||
}
|
||||
|
||||
describe("Card نوبت — فیلدهای حالت سرویسی", () => {
|
||||
// ── ✅ موفق ──────────────────────────────────────────────────────────────
|
||||
|
||||
it("نام همهٔ سرویسها و مدت نوبت را نشان میدهد", () => {
|
||||
renderCard({
|
||||
service_items: [{ uuid: "s1", name: "لیزر صورت" }, { uuid: "s2", name: "لیزر بیکینی" }],
|
||||
service_total_minutes: 35,
|
||||
});
|
||||
|
||||
expect(screen.getByText("سرویس:")).toBeInTheDocument();
|
||||
expect(screen.getByText("لیزر صورت، لیزر بیکینی")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدت نوبت:")).toBeInTheDocument();
|
||||
expect(screen.getByText("35 دقیقه")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── ❌ خط سرخ: نوبت اسلاتی ────────────────────────────────────────────────
|
||||
|
||||
it("نوبت اسلاتی هیچکدام از دو ردیف را نمیگیرد و کرش نمیکند", () => {
|
||||
renderCard({ service_items: [], service_total_minutes: null });
|
||||
|
||||
expect(screen.queryByText("سرویس:")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("مدت نوبت:")).not.toBeInTheDocument();
|
||||
// ردیفهای موجود سر جایشاناند.
|
||||
expect(screen.getByText("تاریخ نوبت:")).toBeInTheDocument();
|
||||
expect(screen.getByText("ساعت نوبت:")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("فیلدهای سرویسی کاملاً غایب (بکاند قدیمی) هم کرش نمیدهد", () => {
|
||||
renderCard({});
|
||||
|
||||
expect(screen.queryByText("سرویس:")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("تاریخ نوبت:")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
|
||||
|
||||
it("نوبت رزرو سرویس را نشان میدهد ولی مدت را نه (زمان ندارد)", () => {
|
||||
renderCard({
|
||||
is_reserve: true,
|
||||
service_items: [{ uuid: "s1", name: "لیزر" }],
|
||||
service_total_minutes: 45,
|
||||
});
|
||||
|
||||
expect(screen.getByText("لیزر")).toBeInTheDocument();
|
||||
expect(screen.queryByText("مدت نوبت:")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("سرویس بدون مدت → فقط ردیف سرویس", () => {
|
||||
renderCard({ service_items: [{ uuid: "s1", name: "مشاوره" }], service_total_minutes: null });
|
||||
|
||||
expect(screen.getByText("مشاوره")).toBeInTheDocument();
|
||||
expect(screen.queryByText("مدت نوبت:")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("مدت صفر ردیف نمیسازد", () => {
|
||||
renderCard({ service_items: [{ uuid: "s1", name: "مشاوره" }], service_total_minutes: 0 });
|
||||
|
||||
expect(screen.queryByText("مدت نوبت:")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -90,6 +90,29 @@ function DetailLg({ appointmentData, loading }) {
|
||||
</div>
|
||||
</TextLoading>
|
||||
</li>
|
||||
{/* حالت نوبتدهی سرویسی. شرط اجباری: نوبت اسلاتی این فیلدها را ندارد. */}
|
||||
{appointmentData?.service_items?.length > 0 && (
|
||||
<li className="flex flex-wrap items-center justify-start gap-y-[12px] gap-[32px]">
|
||||
<TextLoading loading={loading} width={200} height={20}>
|
||||
<p className="text-[#616161] text-[14px] lg:text-[16px] font-normal">
|
||||
سرویس:{" "}
|
||||
<span className="text-[#2F2F2F] text-[14px] lg:text-[16px] font-medium">
|
||||
{appointmentData.service_items.map((s) => s.name).join("، ")}
|
||||
</span>
|
||||
</p>
|
||||
</TextLoading>
|
||||
{!appointmentData.is_reserve && appointmentData.service_total_minutes ? (
|
||||
<TextLoading loading={loading} width={110} height={20}>
|
||||
<p className="text-[#616161] text-[14px] lg:text-[16px] font-normal">
|
||||
مدت نوبت:{" "}
|
||||
<span className="text-[#2F2F2F] text-[14px] lg:text-[16px] font-medium">
|
||||
{appointmentData.service_total_minutes} دقیقه
|
||||
</span>
|
||||
</p>
|
||||
</TextLoading>
|
||||
) : null}
|
||||
</li>
|
||||
)}
|
||||
<li className="flex flex-wrap items-center justify-start gap-y-[12px] gap-[32px]">
|
||||
<TextLoading loading={loading} width={250} height={20}>
|
||||
<div className="flex items-start justify-start gap-[4px]">
|
||||
|
||||
@@ -85,6 +85,31 @@ function DetailSm({ appointmentData, loading }) {
|
||||
</p>
|
||||
</TextLoading>
|
||||
</li>
|
||||
{/* حالت نوبتدهی سرویسی. شرط اجباری: نوبت اسلاتی این فیلدها را ندارد. */}
|
||||
{appointmentData?.service_items?.length > 0 && (
|
||||
<li className="flex flex-wrap gap-x-[12px] items-start w-full justify-between mt-[12px]">
|
||||
<TextLoading loading={loading} width={80} height={16}>
|
||||
<p className="text-[#7E7E7E] text-[16px] font-normal shrink-0">سرویس:</p>
|
||||
</TextLoading>
|
||||
<TextLoading loading={loading} width={140} height={16}>
|
||||
<p className="text-[#525252] text-[16px] font-medium text-left">
|
||||
{appointmentData.service_items.map((s) => s.name).join("، ")}
|
||||
</p>
|
||||
</TextLoading>
|
||||
</li>
|
||||
)}
|
||||
{!appointmentData?.is_reserve && appointmentData?.service_total_minutes ? (
|
||||
<li className="flex flex-wrap items-center w-full justify-between mt-[12px]">
|
||||
<TextLoading loading={loading} width={80} height={16}>
|
||||
<p className="text-[#7E7E7E] text-[16px] font-normal">مدت نوبت:</p>
|
||||
</TextLoading>
|
||||
<TextLoading loading={loading} width={60} height={16}>
|
||||
<p className="text-[#525252] text-[16px] font-medium">
|
||||
{appointmentData.service_total_minutes} دقیقه
|
||||
</p>
|
||||
</TextLoading>
|
||||
</li>
|
||||
) : null}
|
||||
<span className="block w-full h-px bg-[#EFEFEF] mt-[12px] mb-[16px]"></span>
|
||||
<li className="flex flex-wrap gap-x-[12px] gap-y-[12px] items-center w-full justify-between">
|
||||
<TextLoading loading={loading} width={200} height={16}>
|
||||
|
||||
Reference in New Issue
Block a user