Files
nobat724_front/components/appointment/service/index.js
T
hamedandClaude Opus 5 f3659f827e feat(booking): take service duration from the backend, not a parallel client sum
The service picker summed duration_minutes itself while the backend already
returns total_duration_minutes. Two sources of truth: when the formula changes to
solo/additional minutes, the site would keep showing the old number and the
patient would see a duration that does not match their appointment.

The picker runs before the date step and appointment-service-slots needs a date,
so it is called with today. total_duration_minutes does not depend on the date —
the backend computes it before touching that day's shifts, so the number is right
even when today is closed and start_times comes back empty.

The label reads "مدت تقریبی" until the server number arrives, then "مدت کل".
A missing field or a failed request falls back to the client sum with a
console.warn rather than blanking the step.

Staleness is derived from the selection key instead of reset in the effect body,
which also clears the set-state-in-effect lint warning.

Task: clinicpro/docs/new_feture/taskes/task-00b-nobat724-service-mode/

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:50:07 +03:30

139 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import moment from "jalali-moment";
import { request } from "@/services/response";
function toToman(rials) {
if (!rials) return null;
return Math.round(rials / 10).toLocaleString("fa-IR");
}
/**
* جمعِ سمتِ کلاینت — فقط fallback برای بک‌اندی که `total_duration_minutes` نمی‌دهد.
* مدت، دادهٔ سرور است: فرمولش قرار است به «زمان تنها / زمان اضافه» تغییر کند و هر
* محاسبهٔ موازی در فرانت از آن روز عددِ غلط نشان می‌دهد.
*/
function fallbackSum(services, uuids) {
return services
.filter((s) => uuids.includes(s.uuid))
.reduce((sum, s) => sum + (Number(s.duration_minutes) || 0), 0);
}
// مرحلهٔ انتخاب سرویس (فقط حالت نوبت‌دهی سرویسی) — پیش از انتخاب روز.
function ServiceSelect({ services = [], onContinue, doctorUuid, clinicUuid = null }) {
const [draft, setDraft] = useState([]);
/** `{ key, minutes }` — کلید همان انتخابی است که عدد به آن تعلق دارد. */
const [serverDuration, setServerDuration] = useState(null);
const toggle = (uuid) =>
setDraft((prev) =>
prev.includes(uuid) ? prev.filter((u) => u !== uuid) : [...prev, uuid]
);
const draftKey = draft.join(",");
// مدت را سرور حساب می‌کند. این مرحله پیش از انتخاب روز است، ولی
// `total_duration_minutes` به تاریخ وابسته نیست — بک‌اند آن را پیش از لمسِ
// شیفت‌های آن روز برمی‌گرداند، پس حتی اگر امروز تعطیل باشد و `start_times`
// خالی بیاید، عدد درست است.
useEffect(() => {
if (!doctorUuid || draftKey === "") return;
let cancelled = false;
const today = moment().format("YYYY-MM-DD");
request
.getServiceSlots(doctorUuid, today, draftKey.split(","), clinicUuid)
.then((res) => {
if (cancelled) return;
const minutes = (res?.data?.data ?? res?.data)?.total_duration_minutes;
if (typeof minutes !== "number") {
console.warn("[booking] total_duration_minutes missing — falling back to client sum");
return;
}
setServerDuration({ key: draftKey, minutes });
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [doctorUuid, clinicUuid, draftKey]);
// کهنه‌بودن **مشتق** می‌شود، نه با reset: عددِ متعلق به انتخاب قبلی خودکار نادیده
// گرفته می‌شود و effect لازم نیست در بدنه‌اش setState صدا بزند.
const serverMinutes = serverDuration?.key === draftKey ? serverDuration.minutes : null;
const totalMinutes = serverMinutes ?? fallbackSum(services, draft);
return (
<div className="w-full max-w-[520px] mx-auto">
<h2 className="text-[16px] font-bold text-[#3B3B3B] mb-4">۱. انتخاب سرویس</h2>
{services.length === 0 ? (
<p className="text-[14px] text-[#7A7A7A]">
در حال حاضر سرویسی برای نوبتدهی آنلاین تعریف نشده است.
</p>
) : (
<div className="flex flex-col gap-2">
{services.map((s) => {
const active = draft.includes(s.uuid);
const toman = toToman(s.price_rials);
return (
<button
key={s.uuid}
type="button"
onClick={() => toggle(s.uuid)}
className={`flex items-center justify-between gap-3 p-3 rounded-xl border text-right transition-colors ${
active
? "border-[#5559CE] bg-[#5559CE]/5"
: "border-gray-200 bg-white hover:border-[#5559CE]"
}`}
>
<span className="flex items-center gap-2 min-w-0">
<span
className={`w-4 h-4 rounded border shrink-0 grid place-items-center ${
active ? "border-[#5559CE] bg-[#5559CE]" : "border-gray-300"
}`}
>
{active && (
<span className="w-2 h-2 bg-white rounded-[2px]" />
)}
</span>
<span className="text-[14px] text-[#3B3B3B] truncate">
{s.name}
</span>
</span>
<span className="flex items-center gap-2 shrink-0 text-[12px] text-[#7A7A7A]">
{s.duration_minutes ? <span>{s.duration_minutes} دقیقه</span> : null}
{toman ? <span className="text-[#5559CE]">{toman} تومان</span> : null}
</span>
</button>
);
})}
</div>
)}
{draft.length > 0 && (
<div className="mt-4 text-[13px] text-[#7A7A7A]">
{/* تا وقتی عدد سرور نرسیده، «تقریبی» است — عددِ جمعِ کلاینت ممکن است با
مدت واقعی نوبت یکی نباشد. */}
{serverMinutes === null ? "مدت تقریبی" : "مدت کل"}:{" "}
<b className="text-[#3B3B3B]">{totalMinutes} دقیقه</b>
</div>
)}
<button
type="button"
disabled={draft.length === 0}
onClick={() => onContinue(draft)}
className="mt-5 w-full h-[48px] rounded-xl bg-[#5559CE] text-white text-[15px] font-medium disabled:opacity-50"
>
انتخاب زمان
</button>
</div>
);
}
export default ServiceSelect;