feat(appointment): implement service-based booking flow with service selection and slot adaptation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Date from "./date";
|
||||
import ServiceSelect from "./service";
|
||||
|
||||
// Components
|
||||
import Content from "./Content";
|
||||
@@ -38,15 +39,34 @@ function Container({
|
||||
setAppointmentId,
|
||||
appointmentExpiresAt,
|
||||
setAppointmentExpiresAt,
|
||||
// Service-based booking
|
||||
bookingMode,
|
||||
bookingServices,
|
||||
selectedServiceUuids,
|
||||
setSelectedServiceUuids,
|
||||
}) {
|
||||
const serviceMode = bookingMode === "service";
|
||||
const dateStep =
|
||||
serviceMode && selectedServiceUuids.length === 0 ? (
|
||||
<ServiceSelect
|
||||
services={bookingServices}
|
||||
onContinue={(uuids) => setSelectedServiceUuids(uuids)}
|
||||
/>
|
||||
) : (
|
||||
<Date
|
||||
doctor={doctor}
|
||||
setStep={setStep}
|
||||
disabledDates={disabledDates}
|
||||
setSelectedSlot={setSelectedSlot}
|
||||
setSelectedDate={setSelectedDate}
|
||||
serviceMode={serviceMode}
|
||||
selectedServiceUuids={selectedServiceUuids}
|
||||
onChangeService={() => setSelectedServiceUuids([])}
|
||||
/>
|
||||
);
|
||||
|
||||
const elements = [
|
||||
<Date
|
||||
doctor={doctor}
|
||||
setStep={setStep}
|
||||
disabledDates={disabledDates}
|
||||
setSelectedSlot={setSelectedSlot}
|
||||
setSelectedDate={setSelectedDate}
|
||||
/>,
|
||||
dateStep,
|
||||
<LogInPage
|
||||
num={num}
|
||||
setNum={setNum}
|
||||
@@ -76,6 +96,7 @@ function Container({
|
||||
selectedDate={selectedDate}
|
||||
setAppointmentId={setAppointmentId}
|
||||
setAppointmentExpiresAt={setAppointmentExpiresAt}
|
||||
selectedServiceUuids={selectedServiceUuids}
|
||||
/>,
|
||||
<Paying
|
||||
setStep={setStep}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DateTime from "@/app/component/date/dateTime";
|
||||
|
||||
function Hour({ doctor, setStep, date, locateVisit, isStep, setSelectedSlot, setSelectedDate }) {
|
||||
function Hour({ doctor, setStep, date, locateVisit, isStep, setSelectedSlot, setSelectedDate, serviceMode = false, selectedServiceUuids = [] }) {
|
||||
return (
|
||||
<div className="flex relative mt-[24px] flex-col items-start gap-[12px] justify-start">
|
||||
<p className="text-[#3B3B3B] text-[14px] md:text-[16px] font-bold">
|
||||
@@ -13,6 +13,8 @@ function Hour({ doctor, setStep, date, locateVisit, isStep, setSelectedSlot, set
|
||||
locateVisit={locateVisit}
|
||||
setSelectedSlot={setSelectedSlot}
|
||||
setSelectedDate={setSelectedDate}
|
||||
serviceMode={serviceMode}
|
||||
selectedServiceUuids={selectedServiceUuids}
|
||||
/>
|
||||
{!isStep && (
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-[rgba(255,255,255,0.84)]"></div>
|
||||
|
||||
@@ -2,7 +2,16 @@ import { useState } from "react";
|
||||
import Hour from "./hour";
|
||||
import Time from "./Time";
|
||||
|
||||
function Date({ doctor, setStep, disabledDates, setSelectedSlot, setSelectedDate }) {
|
||||
function Date({
|
||||
doctor,
|
||||
setStep,
|
||||
disabledDates,
|
||||
setSelectedSlot,
|
||||
setSelectedDate,
|
||||
serviceMode = false,
|
||||
selectedServiceUuids = [],
|
||||
onChangeService,
|
||||
}) {
|
||||
const [locateVisit, setLocateVisit] = useState(true);
|
||||
const [date, setDate] = useState();
|
||||
|
||||
@@ -11,6 +20,15 @@ function Date({ doctor, setStep, disabledDates, setSelectedSlot, setSelectedDate
|
||||
<div
|
||||
className="p-0 lg:p-[24px] rounded-[8px] border border-solid border-transparent lg:border-[#EFEFEF] bg-transparent lg:bg-[#FFF]"
|
||||
>
|
||||
{serviceMode && onChangeService && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChangeService}
|
||||
className="mb-3 text-[13px] text-[#5559CE] hover:underline"
|
||||
>
|
||||
← تغییر سرویس
|
||||
</button>
|
||||
)}
|
||||
<Time
|
||||
setDate={setDate}
|
||||
locateVisit={locateVisit}
|
||||
@@ -26,6 +44,8 @@ function Date({ doctor, setStep, disabledDates, setSelectedSlot, setSelectedDate
|
||||
date={date}
|
||||
setSelectedSlot={setSelectedSlot}
|
||||
setSelectedDate={setSelectedDate}
|
||||
serviceMode={serviceMode}
|
||||
selectedServiceUuids={selectedServiceUuids}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import Cookies from "js-cookie";
|
||||
import { setAccessToken } from "@/lib/tokenStore";
|
||||
import { useProvince } from "@/context/ProvinceProvider";
|
||||
|
||||
function SubmitData({ setStep, data, prevData, setErrors, doctor, isForAnother, selectedSlot, selectedDate, setAppointmentId, setAppointmentExpiresAt }) {
|
||||
function SubmitData({ setStep, data, prevData, setErrors, doctor, isForAnother, selectedSlot, selectedDate, setAppointmentId, setAppointmentExpiresAt, selectedServiceUuids = [] }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { cityId } = useProvince();
|
||||
const newStep = () => setStep((prev) => prev + 1);
|
||||
@@ -146,6 +146,8 @@ function SubmitData({ setStep, data, prevData, setErrors, doctor, isForAnother,
|
||||
patient_gender: data?.gender?.value?.id || data?.gender?.value || "",
|
||||
// شناسهی شهرِ دامنهی جاری؛ backend برای گاردِ پورسانت نماینده استفاده میکند.
|
||||
city_id: cityId ?? null,
|
||||
// حالت نوبتدهی سرویسی: backend مدت و slot_end را از این سرویسها بازمحاسبه میکند.
|
||||
...(selectedServiceUuids?.length ? { service_item_uuids: selectedServiceUuids } : {}),
|
||||
...(isForAnother
|
||||
? {
|
||||
patient_name: [data?.name?.value, data?.family?.value].filter(Boolean).join(" ").trim(),
|
||||
|
||||
@@ -20,6 +20,7 @@ function Detail({
|
||||
selectedDate,
|
||||
setAppointmentId,
|
||||
setAppointmentExpiresAt,
|
||||
selectedServiceUuids = [],
|
||||
}) {
|
||||
const [insurance, setInsurance] = useState();
|
||||
const [supplementaryInsurance, setSupplementaryInsurance] = useState();
|
||||
@@ -120,6 +121,7 @@ function Detail({
|
||||
selectedDate={selectedDate}
|
||||
setAppointmentId={setAppointmentId}
|
||||
setAppointmentExpiresAt={setAppointmentExpiresAt}
|
||||
selectedServiceUuids={selectedServiceUuids}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,23 @@ function AppointmentPage({ doctor, disabledDates, matchedCity }) {
|
||||
const [appointmentId, setAppointmentId] = useState(null);
|
||||
const [appointmentExpiresAt, setAppointmentExpiresAt] = useState(null);
|
||||
|
||||
// Service-based booking (روش نوبتدهی سرویسی)
|
||||
const [bookingMode, setBookingMode] = useState("slot");
|
||||
const [bookingServices, setBookingServices] = useState([]);
|
||||
const [selectedServiceUuids, setSelectedServiceUuids] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doctor?.uuid) return;
|
||||
request
|
||||
.getBookingServices(doctor.uuid)
|
||||
.then((res) => {
|
||||
const d = res?.data?.data ?? res?.data ?? {};
|
||||
setBookingMode(d.booking_mode === "service" ? "service" : "slot");
|
||||
setBookingServices(Array.isArray(d.services) ? d.services : []);
|
||||
})
|
||||
.catch(() => setBookingMode("slot"));
|
||||
}, [doctor?.uuid]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -167,6 +184,10 @@ function AppointmentPage({ doctor, disabledDates, matchedCity }) {
|
||||
setAppointmentId={setAppointmentId}
|
||||
appointmentExpiresAt={appointmentExpiresAt}
|
||||
setAppointmentExpiresAt={setAppointmentExpiresAt}
|
||||
bookingMode={bookingMode}
|
||||
bookingServices={bookingServices}
|
||||
selectedServiceUuids={selectedServiceUuids}
|
||||
setSelectedServiceUuids={setSelectedServiceUuids}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
function toToman(rials) {
|
||||
if (!rials) return null;
|
||||
return Math.round(rials / 10).toLocaleString("fa-IR");
|
||||
}
|
||||
|
||||
// مرحلهٔ انتخاب سرویس (فقط حالت نوبتدهی سرویسی) — پیش از انتخاب روز.
|
||||
function ServiceSelect({ services = [], onContinue }) {
|
||||
const [draft, setDraft] = useState([]);
|
||||
|
||||
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);
|
||||
|
||||
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]">
|
||||
مدت کل: <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;
|
||||
Reference in New Issue
Block a user