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>
This commit is contained in:
hamed
2026-07-30 15:50:07 +03:30
co-authored by Claude Opus 5
parent 29881eede0
commit f3659f827e
2 changed files with 57 additions and 6 deletions
+2
View File
@@ -77,6 +77,8 @@ function Container({
dateStep = (
<ServiceSelect
services={bookingServices}
doctorUuid={doctor?.uuid}
clinicUuid={clinicUuid}
onContinue={(uuids) => setSelectedServiceUuids(uuids)}
/>
);
+55 -6
View File
@@ -1,24 +1,70 @@
"use client";
import { useState } from "react";
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 }) {
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 totalMinutes = services
.filter((s) => draft.includes(s.uuid))
.reduce((sum, s) => sum + (Number(s.duration_minutes) || 0), 0);
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">
@@ -70,7 +116,10 @@ function ServiceSelect({ services = [], onContinue }) {
{draft.length > 0 && (
<div className="mt-4 text-[13px] text-[#7A7A7A]">
مدت کل: <b className="text-[#3B3B3B]">{totalMinutes} دقیقه</b>
{/* تا وقتی عدد سرور نرسیده، «تقریبی» است — عددِ جمعِ کلاینت ممکن است با
مدت واقعی نوبت یکی نباشد. */}
{serverMinutes === null ? "مدت تقریبی" : "مدت کل"}:{" "}
<b className="text-[#3B3B3B]">{totalMinutes} دقیقه</b>
</div>
)}