refactor: enhance appointment flow by adding selected slot and date management across components

This commit is contained in:
hamed
2025-11-17 15:33:47 +03:30
parent 2777db3b8c
commit b46a017773
12 changed files with 135 additions and 44 deletions
+7 -1
View File
@@ -4,7 +4,7 @@ import Cookies from "js-cookie";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
function SendAppo({ hour, setStep }) { function SendAppo({ hour, setStep, setSelectedSlot, date, setSelectedDate }) {
const params = useParams(); const params = useParams();
const doctorId = params.doctorId; const doctorId = params.doctorId;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -12,6 +12,12 @@ function SendAppo({ hour, setStep }) {
const sendReq = () => { const sendReq = () => {
const isLogged = Cookies.get("access_token"); const isLogged = Cookies.get("access_token");
// ذخیره کردن اطلاعات اسلات انتخاب شده
if (hour && date) {
setSelectedSlot(hour);
setSelectedDate(date);
}
if (isLogged) { if (isLogged) {
// اگر لاگین کرده، به مرحله 3 (Detail) می‌رود // اگر لاگین کرده، به مرحله 3 (Detail) می‌رود
setStep(3); setStep(3);
+8 -2
View File
@@ -9,7 +9,7 @@ import SendAppo from "./SendAppo";
const listTab = ["صبح", "بعد از ظهر"]; const listTab = ["صبح", "بعد از ظهر"];
const nameHours = ["hours_morning", "hours_afternoon"]; const nameHours = ["hours_morning", "hours_afternoon"];
function DateTime({ date, doctor, setStep }) { function DateTime({ date, doctor, setStep, setSelectedSlot, setSelectedDate }) {
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [hour, setHour] = useState(); const [hour, setHour] = useState();
const [appo, setAppo] = useState("تاریخ مورد نظرتان را انتخاب کنید."); const [appo, setAppo] = useState("تاریخ مورد نظرتان را انتخاب کنید.");
@@ -105,7 +105,13 @@ function DateTime({ date, doctor, setStep }) {
nameHours={nameHours} nameHours={nameHours}
locationAddress={locationAddress} locationAddress={locationAddress}
/> />
<SendAppo hour={hour} setStep={setStep} /> <SendAppo
hour={hour}
setStep={setStep}
setSelectedSlot={setSelectedSlot}
date={date}
setSelectedDate={setSelectedDate}
/>
</div> </div>
); );
} }
+14 -1
View File
@@ -29,9 +29,20 @@ function Container({
setUuid, setUuid,
isSendMsg, isSendMsg,
setIsSendMsg, setIsSendMsg,
// Appointment slot states
selectedSlot,
setSelectedSlot,
selectedDate,
setSelectedDate,
}) { }) {
const elements = [ const elements = [
<Date doctor={doctor} setStep={setStep} disabledDates={disabledDates} />, <Date
doctor={doctor}
setStep={setStep}
disabledDates={disabledDates}
setSelectedSlot={setSelectedSlot}
setSelectedDate={setSelectedDate}
/>,
<LogInPage <LogInPage
num={num} num={num}
setNum={setNum} setNum={setNum}
@@ -82,6 +93,8 @@ function Container({
setStep={setStep} setStep={setStep}
doctor={doctor} doctor={doctor}
step={step} step={step}
selectedSlot={selectedSlot}
selectedDate={selectedDate}
> >
{elements[step]} {elements[step]}
</Content> </Content>
+3 -1
View File
@@ -1,7 +1,7 @@
import Information from "./information"; import Information from "./information";
import Location from "./location"; import Location from "./location";
function Content({ doctor, step, setStep, children, isFirst, disableSide }) { function Content({ doctor, step, setStep, children, isFirst, disableSide, selectedSlot, selectedDate }) {
return ( return (
<div <div
className="flex flex-col lg:flex-row justify-center gap-[12px] sm:gap-[16px] md:gap-[20px] lg:gap-[24px] padding-responsive className="flex flex-col lg:flex-row justify-center gap-[12px] sm:gap-[16px] md:gap-[20px] lg:gap-[24px] padding-responsive
@@ -15,6 +15,8 @@ function Content({ doctor, step, setStep, children, isFirst, disableSide }) {
doctor={doctor} doctor={doctor}
step={step} step={step}
setStep={setStep} setStep={setStep}
selectedSlot={selectedSlot}
selectedDate={selectedDate}
/> />
)} )}
{children} {children}
+3 -1
View File
@@ -1,6 +1,6 @@
import DateTime from "@/app/component/date/dateTime"; import DateTime from "@/app/component/date/dateTime";
function Hour({ doctor, setStep, date, locateVisit, isStep }) { function Hour({ doctor, setStep, date, locateVisit, isStep, setSelectedSlot, setSelectedDate }) {
return ( return (
<div className="flex relative mt-[24px] flex-col items-start gap-[12px] justify-start"> <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"> <p className="text-[#3B3B3B] text-[14px] md:text-[16px] font-bold">
@@ -11,6 +11,8 @@ function Hour({ doctor, setStep, date, locateVisit, isStep }) {
doctor={doctor} doctor={doctor}
setStep={setStep} setStep={setStep}
locateVisit={locateVisit} locateVisit={locateVisit}
setSelectedSlot={setSelectedSlot}
setSelectedDate={setSelectedDate}
/> />
{!isStep && ( {!isStep && (
<div className="absolute left-0 top-0 w-full h-full bg-[rgba(255,255,255,0.84)]"></div> <div className="absolute left-0 top-0 w-full h-full bg-[rgba(255,255,255,0.84)]"></div>
+3 -1
View File
@@ -2,7 +2,7 @@ import { useState } from "react";
import Hour from "./hour"; import Hour from "./hour";
import Time from "./Time"; import Time from "./Time";
function Date({ doctor, setStep, disabledDates }) { function Date({ doctor, setStep, disabledDates, setSelectedSlot, setSelectedDate }) {
const [locateVisit, setLocateVisit] = useState(true); const [locateVisit, setLocateVisit] = useState(true);
const [date, setDate] = useState(); const [date, setDate] = useState();
@@ -25,6 +25,8 @@ function Date({ doctor, setStep, disabledDates }) {
setStep={setStep} setStep={setStep}
doctor={doctor} doctor={doctor}
date={date} date={date}
setSelectedSlot={setSelectedSlot}
setSelectedDate={setSelectedDate}
/> />
</div> </div>
</div> </div>
+9
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import AddCircleBlueA from "@/components/icons/AddCircleBlueA"; import AddCircleBlueA from "@/components/icons/AddCircleBlueA";
import ArrowRightS from "@/components/icons/ArrowRightS";
import Form from "./Form"; import Form from "./Form";
import { request } from "@/services/response"; import { request } from "@/services/response";
import SubmitData from "./SubmitData"; import SubmitData from "./SubmitData";
@@ -41,6 +42,14 @@ function Detail({
rounded-[8px] p-[12px] sm:p-[16px] md:p-[20px] lg:p-[24px] rounded-[8px] p-[12px] sm:p-[16px] md:p-[20px] lg:p-[24px]
`} `}
> >
{/* دکمه برگشت */}
<div
className="cursor-pointer mb-[16px]"
onClick={() => setStep(0)}
>
<ArrowRightS />
</div>
<p className="text-[#3B3B3B] text-[20px] font-bold"> <p className="text-[#3B3B3B] text-[20px] font-bold">
{isForAnother ? "اطلاعات کاربر جدید" : "اطلاعات حساب کاربری"} {isForAnother ? "اطلاعات کاربر جدید" : "اطلاعات حساب کاربری"}
</p> </p>
+8
View File
@@ -24,6 +24,10 @@ function AppointmentPage({ doctor, disabledDates, matchedCity }) {
const [uuid, setUuid] = useState(""); const [uuid, setUuid] = useState("");
const [isSendMsg, setIsSendMsg] = useState(false); const [isSendMsg, setIsSendMsg] = useState(false);
// Appointment slot states
const [selectedSlot, setSelectedSlot] = useState(null);
const [selectedDate, setSelectedDate] = useState(null);
useEffect(() => { useEffect(() => {
const userInfo = Cookies.get("userInfo"); const userInfo = Cookies.get("userInfo");
const parsedData = userInfo && JSON.parse(userInfo); const parsedData = userInfo && JSON.parse(userInfo);
@@ -87,6 +91,10 @@ function AppointmentPage({ doctor, disabledDates, matchedCity }) {
setUuid={setUuid} setUuid={setUuid}
isSendMsg={isSendMsg} isSendMsg={isSendMsg}
setIsSendMsg={setIsSendMsg} setIsSendMsg={setIsSendMsg}
selectedSlot={selectedSlot}
setSelectedSlot={setSelectedSlot}
selectedDate={selectedDate}
setSelectedDate={setSelectedDate}
/> />
); );
} }
+40 -9
View File
@@ -1,13 +1,44 @@
"use client";
import ArrowLeftOrangeA from "@/components/icons/ArrowLeftOrangeA"; import ArrowLeftOrangeA from "@/components/icons/ArrowLeftOrangeA";
import CalendarA from "@/components/icons/CalendarA"; import CalendarA from "@/components/icons/CalendarA";
import ClockA from "@/components/icons/ClockA"; import ClockA from "@/components/icons/ClockA";
import LocationA from "@/components/icons/LocationA"; import LocationA from "@/components/icons/LocationA";
import TickCircleOrange from "@/components/icons/TickCircleOrange"; import TickCircleOrange from "@/components/icons/TickCircleOrange";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import moment from "moment-jalaali";
import { useEffect, useState } from "react";
import { request } from "@/services/response";
function Detail({ doctor, step, setStep }) { function Detail({ doctor, step, setStep, selectedSlot, selectedDate }) {
// دریافت اولین آدرس از لیست const [address, setAddress] = useState("آدرس موجود نیست");
const firstAddress = doctor?.address?.[0]?.address || doctor?.location || "آدرس موجود نیست";
// دریافت آدرس بر اساس location_id از اسلات انتخاب شده
useEffect(() => {
if (selectedSlot?.location_id) {
request
.getDoctorAddress(selectedSlot.location_id)
.then((res) => {
if (res?.address) {
setAddress(res.address);
}
})
.catch((err) => {
console.error("Error fetching address:", err);
});
} else if (doctor?.address?.[0]?.address) {
// اگر اسلات location_id نداشت، از آدرس دکتر استفاده کن
setAddress(doctor.address[0].address);
}
}, [selectedSlot, doctor]);
// فرمت کردن تاریخ به شمسی
const formattedDate = selectedDate
? moment.unix(selectedDate).locale('fa').format('jDD jMMMM jYYYY')
: "تاریخ انتخاب نشده";
// فرمت کردن زمان
const formattedTime = selectedSlot?.time || "زمان انتخاب نشده";
return ( return (
<div className=""> <div className="">
@@ -17,7 +48,7 @@ function Detail({ doctor, step, setStep }) {
<LocationA /> <LocationA />
</div> </div>
<p className="text-[#616161] text-[14px] md:text-[16px] font-normal"> <p className="text-[#616161] text-[14px] md:text-[16px] font-normal">
آدرس: {firstAddress} آدرس: {address}
</p> </p>
</li> </li>
<li className="flex items-center justify-between w-full"> <li className="flex items-center justify-between w-full">
@@ -30,7 +61,7 @@ function Detail({ doctor, step, setStep }) {
تاریخ نوبت:{" "} تاریخ نوبت:{" "}
</p> </p>
<p className="text-[#2F2F2F] text-[14px] md:text-[16px] font-medium mr-1"> <p className="text-[#2F2F2F] text-[14px] md:text-[16px] font-medium mr-1">
13 خرداد 1403 {formattedDate}
</p> </p>
</div> </div>
</div> </div>
@@ -59,7 +90,7 @@ function Detail({ doctor, step, setStep }) {
زمان نوبت:{" "} زمان نوبت:{" "}
</p> </p>
<p className="text-[#2F2F2F] text-[14px] md:text-[16px] font-medium mr-1"> <p className="text-[#2F2F2F] text-[14px] md:text-[16px] font-medium mr-1">
ساعت 17:20 ساعت {formattedTime}
</p> </p>
</div> </div>
</div> </div>
@@ -85,7 +116,7 @@ function Detail({ doctor, step, setStep }) {
w-full h-px bg-[#D7D7D7] block" w-full h-px bg-[#D7D7D7] block"
></span> ></span>
<ul className="flex flex-col items-start justify-start gap-[8px] sm:gap-[13px] md:gap-[19px] lg:gap-[24px]"> <ul className="flex flex-col items-start justify-start gap-[8px] sm:gap-[13px] md:gap-[19px] lg:gap-[24px]">
<li className="flex items-center justify-start gap-[4px] md:gap-[6px] lg:gap-[8px]"> {/* <li className="flex items-center justify-start gap-[4px] md:gap-[6px] lg:gap-[8px]">
<div <div
className="min-w-[16px] sm:min-w-[19px] md:min-w-[21px] lg:min-w-[24px] min-h-[16px] sm:min-h-[19px] md:min-h-[21px] lg:min-h-[24px] className="min-w-[16px] sm:min-w-[19px] md:min-w-[21px] lg:min-w-[24px] min-h-[16px] sm:min-h-[19px] md:min-h-[21px] lg:min-h-[24px]
w-[16px] sm:w-[19px] md:w-[21px] lg:w-[24px] h-[16px] sm:h-[19px] md:h-[21px] lg:h-[24px]" w-[16px] sm:w-[19px] md:w-[21px] lg:w-[24px] h-[16px] sm:h-[19px] md:h-[21px] lg:h-[24px]"
@@ -93,10 +124,10 @@ function Detail({ doctor, step, setStep }) {
<TickCircleOrange /> <TickCircleOrange />
</div> </div>
<p className="text-[#616161] text-[12px] md:text-[14px] lg:text-[16px] font-normal"> <p className="text-[#616161] text-[12px] md:text-[14px] lg:text-[16px] font-normal">
فقط در صورت لغو نوبت تا 5 ساعت قبل از زمان ویزیت، امکان استرداد وجه فقط در صورت لغو نوبت تا 5 ساعت قبل از زمان ویزیت، امکان استرداد وجه
ممکن می باشد. ممکن می باشد.
</p> </p>
</li> </li> */}
<li className="flex items-center justify-start gap-[4px] md:gap-[6px] lg:gap-[8px]"> <li className="flex items-center justify-start gap-[4px] md:gap-[6px] lg:gap-[8px]">
<div <div
className="min-w-[16px] sm:min-w-[19px] md:min-w-[21px] lg:min-w-[24px] min-h-[16px] sm:min-h-[19px] md:min-h-[21px] lg:min-h-[24px] className="min-w-[16px] sm:min-w-[19px] md:min-w-[21px] lg:min-w-[24px] min-h-[16px] sm:min-h-[19px] md:min-h-[21px] lg:min-h-[24px]
+16 -10
View File
@@ -1,29 +1,35 @@
"use client";
import Detail from "./Detail"; import Detail from "./Detail";
import Head from "./Head"; import Head from "./Head";
function Information({ doctor, step, setStep, typePay, isPay, disableSide }) { function Information({ doctor, step, setStep, typePay, isPay, disableSide, selectedSlot, selectedDate }) {
return ( return (
<div <div
className={`w-full lg:w-[41%] relative ${ className={`w-full lg:w-[41%] relative ${disableSide ? "hidden lg:flex" : ""
disableSide ? "hidden lg:flex" : "" }`}
}`}
> >
<div <div
className={`p-[12px] sm:p-[16px] md:p-[20px] lg:p-[24px] sticky right-0 w-full opacity-page rounded-[8px] border className={`p-[12px] sm:p-[16px] md:p-[20px] lg:p-[24px] sticky right-0 w-full opacity-page rounded-[8px] border
border-solid border-[#EFEFEF] bg-[#FFF] border-solid border-[#EFEFEF] bg-[#FFF]
top-[calc(12px_+_75px_+_32px)] sm:top-[calc(21px_+_75px_+_45px)] md:top-[calc(30px_+_75px_+_58px)] lg:top-[calc(40px_+_75px_+_78px)] top-[calc(12px_+_75px_+_32px)] sm:top-[calc(21px_+_75px_+_45px)] md:top-[calc(30px_+_75px_+_58px)] lg:top-[calc(40px_+_75px_+_78px)]
${ ${typePay === "success" || typePay === "failed"
typePay === "success" || typePay === "failed" ? "hidden md:block"
? "hidden md:block" : "block"
: "block" }
}
`} `}
> >
<p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold"> <p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-bold">
جزئیات نوبت جزئیات نوبت
</p> </p>
<Head doctor={doctor} /> <Head doctor={doctor} />
<Detail doctor={doctor} step={step} setStep={setStep} /> <Detail
doctor={doctor}
step={step}
setStep={setStep}
selectedSlot={selectedSlot}
selectedDate={selectedDate}
/>
</div> </div>
</div> </div>
); );
+17 -3
View File
@@ -4,17 +4,31 @@ import "react-toastify/dist/ReactToastify.css";
const BASE_URL = process.env.NEXT_PUBLIC_API_URL; const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
const token = Cookies.get("access_token");
const api = axios.create({ const api = axios.create({
baseURL: BASE_URL, baseURL: BASE_URL,
"X-CSRF-Token": "", "X-CSRF-Token": "",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
}, },
}); });
// اضافه کردن interceptor برای افزودن token به ریکوست‌هایی که نیاز به احراز هویت دارند
api.interceptors.request.use(
(config) => {
// فقط اگر requireAuth در config تنظیم شده باشد، token اضافه می‌شود
if (config.requireAuth) {
const token = Cookies.get("access_token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
api.interceptors.response.use( api.interceptors.response.use(
(response) => { (response) => {
return response.data; return response.data;
+7 -15
View File
@@ -35,21 +35,13 @@ export const request = {
}, },
}); });
}, },
getUserProfile: (uuid) => api.get(`/api/v1/user-profile/${uuid}`), getUserProfile: (uuid) => api.get(`/api/v1/user-profile/${uuid}`, { requireAuth: true }),
postUserProfile: (data) => api.post("api/v1/user-profile", data), postUserProfile: (data) => api.post("api/v1/user-profile", data, { requireAuth: true }),
patchUserProfile: (data, uuid, token) => patchUserProfile: (data, uuid) =>
api.patch( api.patch(`api/v1/user-profile/${uuid}`, data, { requireAuth: true }),
`api/v1/user-profile/${uuid}`, getUserInfo: (headers) => api.get("oauth/userinfo", { ...headers, requireAuth: true }),
data, getInsuranceType: (page = 1, limit = 10) =>
token && { api.get(`api/v1/categorys/insurance_type?page=${page}&limit=${limit}`, removeTokenHead),
headers: {
Authorization: `Bearer ${token}`,
},
}
),
getUserInfo: (headers) => api.get("oauth/userinfo", headers),
getInsuranceType: () =>
api.get("api/v1/categorys/insurance_type", removeTokenHead),
getDoctors: (params) => getDoctors: (params) =>
api.get("api/v1/doctors", { api.get("api/v1/doctors", {
params, params,