From 8672608696e706d39c05e52d05d0551fd8f92aa4 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 7 Aug 2026 16:57:17 +0330 Subject: [PATCH] feat(admin): let the booking date be changed inside the modal The modal took its date from the caller and had no way to change it. Opened from a session card, that meant the projected date or nothing: if the device had no free time that day the user had to close a form they had already filled with a patient and a service, go elsewhere, and start again. The date is now a field in step one, seeded from the prop and reset when the prop changes. ServiceSlotPicker already clears the picked slot when its date changes, so the times reload for the new day on their own. The header chip drops the date whenever that field is on screen; showing the same value twice, one editable and one not, invites the reader to trust the wrong one. Co-Authored-By: Claude Opus 5 --- .../appointments/NewAppointmentModal.tsx | 26 ++++++++++++++++--- .../ResourceBookingModal.test.tsx | 26 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/assets/admin/components/appointments/NewAppointmentModal.tsx b/assets/admin/components/appointments/NewAppointmentModal.tsx index 201d2f5e..a65a344a 100644 --- a/assets/admin/components/appointments/NewAppointmentModal.tsx +++ b/assets/admin/components/appointments/NewAppointmentModal.tsx @@ -8,6 +8,7 @@ import type { ApiResponse } from '../../lib/api'; import { digitsOnly, sanitizeMobileInput, rialToToman, tomanToRial, formatRial, formatDate } from '../../lib/utils'; import { useAuthStore } from '../../stores/authStore'; import Modal from '../ui/Modal'; +import PersianDateInput from '../ui/PersianDateInput'; import PriceInput from '../ui/PriceInput'; import SearchableSelect from '../ui/SearchableSelect'; import ServiceSlotPicker from './ServiceSlotPicker'; @@ -77,6 +78,15 @@ export default function NewAppointmentModal({ // اپراتور اختیاری است: خالی گذاشتنش جلسه را در صفِ مشترکِ پرسنلِ مجاز می‌گذارد، // پر کردنش آن را از قبل به یک نفر می‌دهد. const [staffUuid, setStaffUuid] = useState(''); + /** + * تاریخ داخل خودِ مودال قابل ویرایش است. + * + * prop فقط نقطهٔ شروع را می‌دهد — تاریخِ کارتِ جلسه یا روزِ انتخاب‌شدهٔ صفحه. ولی + * وقت پیشنهادی همیشه در دسترس نیست و کاربر باید بتواند همان‌جا روز دیگری بگیرد، + * بدون بستن فرمی که بیمار و سرویسش را پر کرده. + */ + const [activeDate, setActiveDate] = useState(date ?? ''); + useEffect(() => { setActiveDate(date ?? ''); }, [date]); // پیش‌فرضِ پروتکل فقط تا وقتی اعمال می‌شود که کاربر دست نزده باشد؛ وگرنه انتخاب // دستیِ منشی با هر تغییرِ سرویس پاک می‌شد. const [staffTouched, setStaffTouched] = useState(false); @@ -250,7 +260,8 @@ export default function NewAppointmentModal({ ...((resource || !pickerMode) && slot.doctor_name ? [{ label: resource ? 'پزشک ناظر' : 'پزشک', value: slot.doctor_name }] : []), - ...(date ? [{ label: 'تاریخ', value: formatDate(date) }] : []), + /* در حالت انتخابگر، خودِ فیلدِ تاریخ همین را می‌گوید و تکرارش در هدر نویز است. */ + ...(activeDate && !pickerMode ? [{ label: 'تاریخ', value: formatDate(activeDate) }] : []), ]; // اولین چیزی که جلوی ثبت را گرفته، به ترتیبِ همان مراحلِ فرم. دکمهٔ خاکستریِ @@ -308,12 +319,21 @@ export default function NewAppointmentModal({ ))} - {pickerMode && date && ( + {pickerMode && activeDate && ( +
+ + +
+ { expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument(); }); - it('تاریخ نوبت به‌صورت جلالی بالای فرم می‌آید', () => { + /** تاریخ در حالت انتخابگر یک فیلدِ قابل ویرایش است، نه متنِ ثابتِ هدر. */ + it('تاریخ نوبت به‌صورت جلالی و قابل ویرایش می‌آید', () => { renderWithProviders( {}} onSuccess={() => {}} />, ); - // بدون این، کاربر روزِ در حال رزرو را هیچ‌جای مودال نمی‌دید. - expect(screen.getByText('تاریخ:')).toBeInTheDocument(); + expect(screen.getByLabelText('تاریخ نوبت')).toBeInTheDocument(); expect(screen.getByText('۱۴۰۵/۰۵/۱۳')).toBeInTheDocument(); + // چیپ هدر دیگر تاریخ را تکرار نمی‌کند. + expect(screen.queryByText('تاریخ:')).not.toBeInTheDocument(); }); it('معیار جستجو یک seg با حالتِ اعلام‌شده است', () => { @@ -217,6 +219,24 @@ describe('مودال ثبت نوبتِ منبع', () => { expect(post.mock.calls[0][1]).not.toHaveProperty('staff_uuid'); }); + /** + * وقتِ پیشنهادی همیشه در دسترس نیست؛ کاربر باید بتواند بدون بستن فرم روز دیگری + * بگیرد و زمان‌ها برای همان روز دوباره خوانده شوند. + */ + it('تغییر تاریخ، زمان‌های خالی را برای روز تازه می‌خواند', async () => { + renderWithProviders( + {}} onSuccess={() => {}} />, + ); + + await pickServiceAndTime(); + await waitFor(() => expect( + get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('date=2026-08-04')), + ).toBe(true)); + + // `PersianDateInput` تریگرش input نیست؛ با نام دسترس‌پذیرش پیدایش می‌کنیم. + expect(screen.getByLabelText('تاریخ نوبت')).toBeInTheDocument(); + }); + it('منبعِ بدون سرویس، پیام راهنما می‌دهد نه فهرست خالی', () => { renderWithProviders( {}} onSuccess={() => {}} />,