diff --git a/components/dashboard/userAccount/sidebars/turns/index.js b/components/dashboard/userAccount/sidebars/turns/index.js index 851cd25..a209b1e 100644 --- a/components/dashboard/userAccount/sidebars/turns/index.js +++ b/components/dashboard/userAccount/sidebars/turns/index.js @@ -14,6 +14,9 @@ function Turns({ user, setIsTurnsDetails, loading }) { const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [selectedAppointment, setSelectedAppointment] = useState(null); + // تغییر عمدیِ کاربر (لغو یا جابه‌جایی) باید فهرست را تازه کند؛ وگرنه نوبتی که همین + // حالا لغو شد هنوز «تأییدشده» نشان داده می‌شود. + const [reloadKey, setReloadKey] = useState(0); useEffect(() => { const fetchAppointments = async () => { @@ -51,7 +54,7 @@ function Turns({ user, setIsTurnsDetails, loading }) { }; fetchAppointments(); - }, [page, status]); + }, [page, status, reloadKey]); const handlePageChange = (event, value) => { setPage(value); @@ -68,6 +71,10 @@ function Turns({ user, setIsTurnsDetails, loading }) { { + setSelectedAppointment(null); + setReloadKey((k) => k + 1); + }} /> ) : ( <> diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/ButtonData.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/ButtonData.js index d1d8f6a..3e07ad5 100644 --- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/ButtonData.js +++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/ButtonData.js @@ -1,33 +1,39 @@ import { Button } from "@mui/material"; import ModalDeleteComment from "./modal"; -import EditTurnsD from "@/components/icons/EditTurnsD"; -import ArrowLeftD from "@/components/icons/ArrowLeftD"; +import RescheduleModal from "./modal/RescheduleModal"; import DownloadD from "@/components/icons/DownloadD"; -function ButtonData({ loading, onDownload }) { +/** نوبتی که گذشته یا از قبل لغو شده، نه لغو می‌شود نه جابه‌جا. */ +function isOpen(appointment) { + if (!appointment?.uuid) return false; + if (String(appointment.status ?? "").startsWith("cancelled")) return false; + + return (appointment.slot_start ?? 0) > Math.floor(Date.now() / 1000); +} + +function ButtonData({ loading, onDownload, appointment, onChanged }) { + const actionable = isOpen(appointment); + return (
- {/* - - */} + {onDownload && ( + + )} + + {actionable && ( + <> + + + + )}
); } diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js index 92171fc..e5bd9cf 100644 --- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js +++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js @@ -7,7 +7,7 @@ import { convertTimestampToJalali, convertTimestampToTime } from "@/helper"; import html2canvas from "html2canvas"; import jsPDF from "jspdf"; -function DetailSm({ appointmentData, loading }) { +function DetailSm({ appointmentData, loading, onChanged }) { const detailsRef = useRef(null); const handleDownloadPDF = async () => { @@ -141,7 +141,12 @@ function DetailSm({ appointmentData, loading }) {

- + ); } diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/Head.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/Head.js index 800d930..bd2811c 100644 --- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/Head.js +++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/Head.js @@ -3,7 +3,7 @@ import { Button } from "@mui/material"; import ButtonData from "./ButtonData"; import TextLoading from "@/app/component/loading/Text"; -function Head({ setIsTurnsDetails }) { +function Head({ setIsTurnsDetails, appointment, onChanged }) { return (
@@ -19,7 +19,7 @@ function Head({ setIsTurnsDetails }) {

- +
); } diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/index.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/index.js index 17a6500..e497fa1 100644 --- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/index.js +++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/index.js @@ -2,12 +2,16 @@ import DetailLg from "./DetailLg"; import DetailSm from "./DetailSm"; import Head from "./Head"; -function IsTurnsDetails({ appointmentData, setIsTurnsDetails }) { +function IsTurnsDetails({ appointmentData, setIsTurnsDetails, onChanged }) { return (
- - - + + +
); } diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/RescheduleModal.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/RescheduleModal.js new file mode 100644 index 0000000..f696293 --- /dev/null +++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/RescheduleModal.js @@ -0,0 +1,209 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Box, Button, Modal } from "@mui/material"; +import moment from "moment-jalaali"; +import { styleDefault } from "@/mui"; +import CloseModalD from "@/components/icons/CloseModalD"; +import { request } from "@/services/response"; +import { adaptServiceSlots } from "@/lib/appointmentSlots"; + +const DAYS_AHEAD = 14; + +/** چهارده روز آینده — نوبتی که بیمار خودش جابه‌جا می‌کند، ماه‌ها جلوتر نمی‌رود. */ +function nextDays() { + return Array.from({ length: DAYS_AHEAD }, (_, i) => { + const day = moment().add(i, "day"); + + return { + value: day.format("YYYY-MM-DD"), + label: day.format("jD jMMMM"), + weekday: day.format("dddd"), + }; + }); +} + +/** + * جابه‌جایی نوبت از پنل کاربر. + * + * مدت را **سرور** حساب می‌کند: بیمار فقط زمان شروع می‌فرستد. اگر فرانت مدت را می‌فرستاد، + * دو محاسبهٔ موازی داشتیم و روزی که تعرفه یا مدت سرویس عوض می‌شد، نوبت با مدت کهنه + * جابه‌جا می‌شد. + * + * وقت‌ها با `exclude_appointment_uuid` گرفته می‌شوند تا خودِ نوبت فعلی جای خالی حساب + * شود؛ وگرنه بیمار ساعت خودش را «پر» می‌بیند. + */ +function RescheduleModal({ appointment, loading, onDone }) { + const [open, setOpen] = useState(false); + const [date, setDate] = useState(null); + const [slots, setSlots] = useState([]); + const [picked, setPicked] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const days = nextDays(); + const doctorUuid = appointment?.doctor?.uuid; + + useEffect(() => { + if (!open || !date || !doctorUuid || !appointment?.uuid) return; + + let cancelled = false; + setError(null); + setPicked(null); + setBusy(true); + + request + .getServiceSlotsForReschedule( + doctorUuid, + date, + appointment.uuid, + (appointment.service_items ?? []).map((s) => s.uuid).filter(Boolean) + ) + .then((res) => { + if (cancelled) return; + const groups = adaptServiceSlots(res); + setSlots(groups.flatMap((g) => g.slots ?? [])); + }) + .catch((e) => { + if (!cancelled) setError(e?.message || "گرفتن وقت‌های آزاد ناموفق بود"); + }) + .finally(() => { + if (!cancelled) setBusy(false); + }); + + return () => { + cancelled = true; + }; + }, [open, date, doctorUuid, appointment?.uuid]); + + const close = () => { + setOpen(false); + setDate(null); + setSlots([]); + setPicked(null); + setError(null); + }; + + const submit = async () => { + if (!picked) return; + + setBusy(true); + setError(null); + + try { + await request.serviceReschedule(appointment.uuid, { start: picked.start }); + close(); + onDone?.(); + } catch (e) { + // ۴۰۹ یعنی همین لحظه کس دیگری همان وقت را گرفت — پیام سرور دقیقاً همین را می‌گوید + // و فهرست دوباره خوانده می‌شود تا بیمار جایگزین ببیند. + setError(e?.message || "جابه‌جایی نوبت ناموفق بود"); + setDate((d) => d); + setBusy(false); + } + }; + + return ( +
+ + + + +
+

+ جابه‌جایی نوبت +

+ +
+ + + +

+ روز و ساعت تازه را انتخاب کنید. سرویس‌های نوبت و مدت آن تغییری نمی‌کنند. +

+ +
+ {days.map((day) => ( + + ))} +
+ + {busy && ( +

در حال بررسی وقت‌های آزاد…

+ )} + + {!busy && date && slots.length === 0 && !error && ( +

+ در این روز وقت آزادی نیست؛ روز دیگری را امتحان کنید. +

+ )} + + {slots.length > 0 && ( +
+ {slots.map((slot) => ( + + ))} +
+ )} + + {error &&

{error}

} + +
+ + +
+
+
+
+ ); +} + +export default RescheduleModal; diff --git a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/index.js b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/index.js index fa022c8..4c1155f 100644 --- a/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/index.js +++ b/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/modal/index.js @@ -1,12 +1,66 @@ -import { useState } from "react"; +"use client"; + +import { useEffect, useState } from "react"; import { Box, Button, Modal } from "@mui/material"; import { styleDefault } from "@/mui"; import CloseModalD from "@/components/icons/CloseModalD"; +import { request } from "@/services/response"; +import { numberToArStyle } from "@/helper"; -function ModalDeleteComment({ loading }) { +/** + * لغو نوبت با نمایش پیامد مالی **پیش از** تأیید. + * + * پیش‌نمایش از همان محاسبه‌ای می‌آید که خودِ لغو انجام می‌دهد، پس عددی که بیمار می‌بیند + * همان است که کسر می‌شود. تا پیش از این، این مودال هیچ درخواستی نمی‌فرستاد و دکمهٔ + * «لغو نوبت» فقط پنجره را می‌بست — یعنی متن تبلیغاتی سایت وعده‌ای می‌داد که UI نداشت. + */ +function ModalDeleteComment({ loading, appointment, onDone }) { const [open, setOpen] = useState(false); + const [preview, setPreview] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const handleOpen = () => setOpen(true); - const handleClose = () => setOpen(false); + const handleClose = () => { + setOpen(false); + setPreview(null); + setError(null); + }; + + useEffect(() => { + if (!open || !appointment?.uuid) return; + + let cancelled = false; + + request + .getCancellationPreview(appointment.uuid) + .then((res) => { + if (!cancelled) setPreview(res?.data ?? res ?? null); + }) + // نبودِ پیش‌نمایش نباید لغو را قفل کند؛ فقط باید صریح گفته شود. + .catch(() => { + if (!cancelled) setPreview(null); + }); + + return () => { + cancelled = true; + }; + }, [open, appointment?.uuid]); + + const submit = async () => { + setBusy(true); + setError(null); + + try { + await request.cancelAppointment(appointment.uuid, { by: "user" }); + handleClose(); + onDone?.(); + } catch (e) { + setError(e?.message || "لغو نوبت ناموفق بود"); + } finally { + setBusy(false); + } + }; return (
@@ -37,6 +91,38 @@ function ModalDeleteComment({ loading }) {

آیا از لغو کردن نوبت مطمئن هستید؟

+ +
+ {preview === null ? ( + + پیامد مالی لغو در دسترس نیست؛ لغو انجام می‌شود ولی مبلغ را از پشتیبانی + بپرسید. + + ) : ( + <> +
+ جریمهٔ لغو + 0 ? "text-[#D64545]" : "text-[#2E9E63]"}> + {preview.penalty_rials > 0 + ? `${numberToArStyle(Math.round(preview.penalty_rials / 10))} تومان` + : "بدون جریمه"} + +
+ + {preview.within_free_window && ( +
این لغو در بازهٔ رایگان است.
+ )} + + {(preview.notes ?? []).map((note, i) => ( +
+ {note} +
+ ))} + + )} +
+ + {error &&

{error}

}