Files
nobat724_front/components/appointment/detail/SubmitData.js
T
hamed 54bebbd41a feat: Implement resource-based appointment booking flow
- Updated DoctorPage component to accept bookingResources prop for appointment list.
- Added serviceQuery function to serialize service item UUIDs for API requests.
- Introduced new API endpoints for fetching booking resources and resource slots.
- Enhanced tests for new resource-based booking functionality, including resource selection and service availability.
- Created ResourceSelect component for selecting appointment types, including doctor and resource options.
- Updated appointment submission logic to include resource_uuid in payload when applicable.
- Ensured UI reflects changes in booking flow without disrupting existing doctor-centric experience.
2026-08-09 10:45:52 +03:30

212 lines
8.5 KiB
JavaScript

import { useState } from "react";
import ButtonFixed from "../paying/ButtonFixed";
import { Button } from "@mui/material";
import ArrowLeftB from "@/components/icons/ArrowLeftB";
import { request } from "@/services/response";
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, selectedServiceUuids = [], clinicUuid = null, resourceUuid = null }) {
const [loading, setLoading] = useState(false);
const { cityId } = useProvince();
const newStep = () => setStep((prev) => prev + 1);
const refreshAccessToken = async () => {
try {
const res = await fetch("/api/auth/refresh", { method: "POST" });
if (!res.ok) return false;
const response = await res.json();
if (response?.access_token) {
setAccessToken(response.access_token);
return true;
}
return false;
} catch {
return false;
}
};
// پاسخ خطای backend: { success:false, errors:[{ code, field, message }] }
// به نگاشت field→message تبدیل می‌شود تا inline در فرم نمایش داده شود.
const fieldErrorsFromResponse = (error) => {
const list = error?.response?.data?.errors;
if (!Array.isArray(list)) return {};
return list.reduce((acc, e) => {
if (e?.field && e?.message) acc[e.field] = e.message;
return acc;
}, {});
};
const isNationalCodeTaken = (error) =>
error?.response?.data?.errors?.some((e) => e?.code === "ERR_PROFILE_001");
const validate = () => {
const errs = {};
const nationalCode = (data?.national_code?.value || "").trim();
if (!data?.name?.value?.trim()) errs.name = "نام الزامی است";
if (!data?.family?.value?.trim()) errs.family = "نام خانوادگی الزامی است";
if (!nationalCode) errs.national_code = "کد ملی الزامی است";
else if (!/^\d{10}$/.test(nationalCode))
errs.national_code = "کد ملی باید ۱۰ رقم باشد";
if (!data?.gender?.value) errs.gender = "جنسیت الزامی است";
if (!data?.basic_insurance?.value?.id) errs.basic_insurance = "نوع بیمه الزامی است";
return errs;
};
const handleSubmit = async () => {
const errs = validate();
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
const isChanged = JSON.stringify(prevData) !== JSON.stringify(data);
setErrors({});
setLoading(true);
try {
if (!isForAnother && isChanged) {
const { basic_insurance, supplementary_insurance, name, family, national_code, gender, insurance_id, uuid } = data;
let payload = {};
// برای PATCH فقط فیلدهای تغییر یافته را ارسال می‌کنیم
if (uuid) {
// بررسی تغییرات فیلد به فیلد
if (JSON.stringify(prevData.name) !== JSON.stringify(name)) {
payload.name = name?.value;
}
if (JSON.stringify(prevData.family) !== JSON.stringify(family)) {
payload.family = family?.value;
}
if (JSON.stringify(prevData.national_code) !== JSON.stringify(national_code)) {
payload.national_code = national_code?.value;
}
if (JSON.stringify(prevData.gender) !== JSON.stringify(gender)) {
payload.gender = gender?.value?.id || gender?.value;
}
if (JSON.stringify(prevData.insurance_id) !== JSON.stringify(insurance_id)) {
payload.insurance_id = insurance_id?.value;
}
if (JSON.stringify(prevData.basic_insurance) !== JSON.stringify(basic_insurance)) {
payload.basic_insurance = basic_insurance?.value?.id ? [basic_insurance.value.id] : [];
}
if (JSON.stringify(prevData.supplementary_insurance) !== JSON.stringify(supplementary_insurance)) {
payload.supplementary_insurance = supplementary_insurance?.value?.id ? [supplementary_insurance.value.id] : [];
}
} else {
// برای POST همه فیلدها را ارسال می‌کنیم
payload = {
name: name?.value,
family: family?.value,
national_code: national_code?.value,
gender: gender?.value?.id || gender?.value,
insurance_id: insurance_id?.value,
basic_insurance: basic_insurance?.value?.id ? [basic_insurance.value.id] : [],
supplementary_insurance: supplementary_insurance?.value?.id ? [supplementary_insurance.value.id] : [],
};
}
if (uuid) {
await request.patchUserProfile(payload, uuid);
} else {
const userUuid = data?.uuid || Cookies.get("uuid");
try {
await request.postUserProfile(payload);
} catch (err) {
// 409 با کد ERR_PROFILE_001 یعنی کد ملی تکراری، نه «پروفایل موجود» → باید throw شود تا inline نمایش یابد
// پروفایل از قبل ساخته شده (lazy-create در سمت backend) → به‌جای ساخت، ویرایش
if (err?.response?.status === 409 && userUuid && !isNationalCodeTaken(err)) {
await request.patchUserProfile(payload, userUuid);
} else {
throw err;
}
}
}
const refreshed = await refreshAccessToken();
if (refreshed) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (doctor && selectedSlot) {
const appointmentPayload = {
doctor_uuid: doctor.uuid,
// محل نوبت‌دهی؛ null یعنی مطب شخصی. بدون این، نوبت در محل اشتباه ثبت می‌شود.
clinic_uuid: clinicUuid,
slot_start: selectedSlot.start,
slot_end: selectedSlot.end,
for_self: !isForAnother,
note: data?.patient_reason?.value || "",
// کد ملی و جنسیت در هر دو حالت (خودِ بیمار / شخص دیگر) الزامی است.
patient_national_code: data?.national_code?.value || "",
patient_gender: data?.gender?.value?.id || data?.gender?.value || "",
// شناسه‌ی شهرِ دامنه‌ی جاری؛ backend برای گاردِ پورسانت نماینده استفاده می‌کند.
city_id: cityId ?? null,
// حالت نوبت‌دهی سرویسی: backend مدت و slot_end را از این سرویس‌ها بازمحاسبه می‌کند.
...(selectedServiceUuids?.length ? { service_item_uuids: selectedServiceUuids } : {}),
// نوبت منبع‌محور: مدت از زنجیرهٔ حلِ همین منبع بازمحاسبه می‌شود، پس
// slot_end فرستاده‌شده فقط پیشنهاد است.
...(resourceUuid ? { resource_uuid: resourceUuid } : {}),
...(isForAnother
? {
patient_name: [data?.name?.value, data?.family?.value].filter(Boolean).join(" ").trim(),
patient_mobile: data?.phone?.value,
patient_reason: data?.patient_reason?.value || "",
}
: {}),
};
const res = await request.postAppointment(appointmentPayload);
const appointment = res?.data?.data;
const appointmentUuid = appointment?.uuid;
if (appointmentUuid) {
setAppointmentId(appointmentUuid);
setAppointmentExpiresAt?.(appointment?.expires_at ?? null);
}
}
setLoading(false);
newStep();
} catch (error) {
setLoading(false);
const fieldErrors = fieldErrorsFromResponse(error);
if (Object.keys(fieldErrors).length > 0) {
setErrors(fieldErrors);
return;
}
if (error?.response?.status === 409) {
setStep(0);
return;
}
}
};
return (
<ButtonFixed>
<Button
disabled={loading}
className={` !flex !w-full md:!w-fit !mt-0 md:!mt-[32px] !gap-[4px] md:!mr-auto !px-[12px] !py-[8px] md:!py-[9px] !min-w-[150px] `}
variant="contained"
onClick={handleSubmit}
>
<p
className={`${loading && "opacity-0"} text-[#EFEFEF] text-[16px] font-medium`}
>
ثبت اطلاعات
</p>
<ArrowLeftB />
</Button>
</ButtonFixed>
);
}
export default SubmitData;