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 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 16:57:17 +03:30
co-authored by Claude Opus 5
parent e385056f09
commit 8672608696
2 changed files with 46 additions and 6 deletions
@@ -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({
))}
</div>
{pickerMode && date && (
{pickerMode && activeDate && (
<Step n={1} title="خدمت و زمان">
<div className="field-block" style={{ marginBottom: 12, maxWidth: 220 }}>
<label htmlFor="appt-date">تاریخ نوبت</label>
<PersianDateInput
value={activeDate}
onChange={setActiveDate}
ariaLabel="تاریخ نوبت"
/>
</div>
<ServiceSlotPicker
doctorUuid={slot.doctor_uuid}
resourceUuid={resource?.uuid}
date={date}
date={activeDate}
services={services}
onSelect={setPick}
clinicUuidOverride={clinicUuid}
@@ -142,14 +142,16 @@ describe('مودال ثبت نوبتِ منبع', () => {
expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument();
});
it('تاریخ نوبت به‌صورت جلالی بالای فرم می‌آید', () => {
/** تاریخ در حالت انتخابگر یک فیلدِ قابل ویرایش است، نه متنِ ثابتِ هدر. */
it('تاریخ نوبت به‌صورت جلالی و قابل ویرایش می‌آید', () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} 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(
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} 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(
<NewAppointmentModal slot={slot} resource={resource} services={[]} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,