From ae958eee73570397db7f04cdd47b179c4fcd1b50 Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Thu, 30 Jul 2026 15:53:18 +0330
Subject: [PATCH] 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)
---
.../userAccount/sidebars/turns/Card.js | 19 +++++
.../userAccount/sidebars/turns/Card.test.jsx | 85 +++++++++++++++++++
.../sidebars/turns/isTurnsDetails/DetailLg.js | 23 +++++
.../sidebars/turns/isTurnsDetails/DetailSm.js | 25 ++++++
4 files changed, 152 insertions(+)
create mode 100644 components/dashboard/userAccount/sidebars/turns/Card.test.jsx
diff --git a/components/dashboard/userAccount/sidebars/turns/Card.js b/components/dashboard/userAccount/sidebars/turns/Card.js
index 5f09ddc..c161bdb 100644
--- a/components/dashboard/userAccount/sidebars/turns/Card.js
+++ b/components/dashboard/userAccount/sidebars/turns/Card.js
@@ -55,6 +55,25 @@ function Card({ data, loading, setIsTurnsDetails }) {
+
+ {/* فیلدهای حالت نوبتدهی سرویسی. شرط اجباری است: نوبت اسلاتی اینها را ندارد و
+ بدون شرط، کارتِ همهٔ نوبتهای اسلاتی میشکند — یعنی کل پنل کاربر. */}
+ {data.service_items?.length > 0 && (
+
+ سرویس:
+
+ {data.service_items.map((s) => s.name).join("، ")}
+
+
+ )}
+ {!data.is_reserve && data.service_total_minutes ? (
+
+ مدت نوبت:
+
+ {data.service_total_minutes} دقیقه
+
+
+ ) : null}
diff --git a/components/dashboard/userAccount/sidebars/turns/Card.test.jsx b/components/dashboard/userAccount/sidebars/turns/Card.test.jsx
new file mode 100644
index 0000000..dcc8355
--- /dev/null
+++ b/components/dashboard/userAccount/sidebars/turns/Card.test.jsx
@@ -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( {}} />);
+}
+
+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();
+ });
+});
diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailLg.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailLg.js
index 520e848..bf51cf4 100644
--- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailLg.js
+++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailLg.js
@@ -90,6 +90,29 @@ function DetailLg({ appointmentData, loading }) {
+ {/* حالت نوبتدهی سرویسی. شرط اجباری: نوبت اسلاتی این فیلدها را ندارد. */}
+ {appointmentData?.service_items?.length > 0 && (
+
+
+
+ سرویس:{" "}
+
+ {appointmentData.service_items.map((s) => s.name).join("، ")}
+
+
+
+ {!appointmentData.is_reserve && appointmentData.service_total_minutes ? (
+
+
+ مدت نوبت:{" "}
+
+ {appointmentData.service_total_minutes} دقیقه
+
+
+
+ ) : null}
+
+ )}
diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js
index a346b62..92171fc 100644
--- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js
+++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js
@@ -85,6 +85,31 @@ function DetailSm({ appointmentData, loading }) {
+ {/* حالت نوبتدهی سرویسی. شرط اجباری: نوبت اسلاتی این فیلدها را ندارد. */}
+ {appointmentData?.service_items?.length > 0 && (
+
+
+ سرویس:
+
+
+
+ {appointmentData.service_items.map((s) => s.name).join("، ")}
+
+
+
+ )}
+ {!appointmentData?.is_reserve && appointmentData?.service_total_minutes ? (
+
+
+ مدت نوبت:
+
+
+
+ {appointmentData.service_total_minutes} دقیقه
+
+
+
+ ) : null}