Two faults, one root: the per-context booking work updated ScheduleSection but left the rest of the panel calling slot endpoints without clinic_uuid. Absent clinic_uuid means the personal practice, so the panel asked about a schedule the doctor barely uses and got nothing back. - useClinicContext() resolves the current environment once and is used by the appointments page, useDoctorBookingServices, ServiceSlotPicker and both queries in NewAppointmentDrawer (a fifth call site a sweep turned up). It returns null in a doctor's personal environment so the mirror-image bug — a doctor seeing the clinic's schedule at their own practice — cannot appear. clinicUuid is part of every query key; without it the cache leaks across environments. - appointment-slots returns empty_reason (no_schedule | holiday | day_off | outside_window). TurnsTimeline rendered «این روز تعطیل است» for any empty day, which is what the bug report actually saw; it now says which of the four it is. - booking-locations lists a location only when the context has an address and an active shift points at it. The dev data had three "personal" schedules whose shifts referenced the clinic's address, so the public site advertised a personal practice that could never be booked. - ?date= adds available_on_date per location, validated as a real calendar date. - MyAppointmentsController and AdminApiController resolved the appointment address with no context and could store the wrong one. Both now go through the new BookingContextResolver, which also replaces AppointmentController's private copy of the same membership check. - app:schedule:audit-locations reports shifts pointing at a missing or foreign address; --fix deactivates them rather than deleting. Verified against the reported doctor: same date, no clinic_uuid -> 0 sessions, with it -> 1 session; a full week matches the configured Sat/Tue/Wed/Thu. Suite: 417 tests, 2 failures — both pre-existing and unrelated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
398 lines
22 KiB
TypeScript
398 lines
22 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||
import { PlusIcon } from '@heroicons/react/24/outline';
|
||
import { toast } from 'sonner';
|
||
import { api } from '../lib/api';
|
||
import type { ApiResponse } from '../lib/api';
|
||
import { useClinicContext } from '../hooks/useClinicContext';
|
||
import Modal from './ui/Modal';
|
||
import PersianDateInput from './ui/PersianDateInput';
|
||
import PriceInput from './ui/PriceInput';
|
||
import SearchableSelect from './ui/SearchableSelect';
|
||
import { WalletChargeLink } from './AppointmentActions';
|
||
import { tehranWallClockToUnix, tomanToRial, rialToToman, digitsOnly, sanitizeMobileInput } from '../lib/utils';
|
||
|
||
interface Option { uuid: string; name?: string; full_name?: string }
|
||
interface PatientRow { uuid: string; user_name?: string; user_mobile?: string; user_national_code?: string }
|
||
|
||
const toEpoch = (isoDate: string, time: string) => tehranWallClockToUnix(isoDate, time);
|
||
const addMinutes = (time: string, min: number) => {
|
||
const [h, m] = time.split(':').map(Number);
|
||
const t = h * 60 + m + min;
|
||
return `${String(Math.floor(t / 60) % 24).padStart(2, '0')}:${String(t % 60).padStart(2, '0')}`;
|
||
};
|
||
|
||
/**
|
||
* اضافه کردن نوبت جدید (Figma add.pdf) — rich create form: patient
|
||
* search-or-new, بخش/سرویس/پرسنل, date + default-duration + start/end time,
|
||
* deposit toggle, status and notes. POSTs the extended /my/appointment.
|
||
*/
|
||
export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey, onClose, isReserve = false }: {
|
||
doctorUuid: string;
|
||
/** ISO Y-m-d — the currently viewed day. */
|
||
defaultDate: string;
|
||
queryKey: unknown[];
|
||
onClose: () => void;
|
||
/** true → «اضافه کردن نوبت رزرو» (day-level entry, no time fields). */
|
||
isReserve?: boolean;
|
||
}) {
|
||
const qc = useQueryClient();
|
||
const clinicUuid = useClinicContext();
|
||
|
||
// ── patient: pick an existing record or enter a new person ────────────────
|
||
const [patientSearch, setPatientSearch] = useState('');
|
||
const [pickedPatient, setPickedPatient] = useState<PatientRow | null>(null);
|
||
const [name, setName] = useState('');
|
||
const [mobile, setMobile] = useState('');
|
||
const [nationalCode, setNationalCode] = useState('');
|
||
|
||
const patientsQ = useQuery<ApiResponse<PatientRow[]>>({
|
||
queryKey: ['drawer-patients', patientSearch],
|
||
queryFn: () => api.get(`/api/v1/patients?search=${encodeURIComponent(patientSearch)}&limit=10`),
|
||
enabled: patientSearch.trim().length >= 2,
|
||
});
|
||
|
||
// ── service specs ──────────────────────────────────────────────────────────
|
||
const [sectionUuid, setSectionUuid] = useState('');
|
||
const [itemUuid, setItemUuid] = useState('');
|
||
const [staffUuid, setStaffUuid] = useState('');
|
||
|
||
// روش نوبتدهی پزشک: در حالت «سرویس» زمان از مدت سرویس محاسبه و پیشنهاد میشود.
|
||
const scheduleQ = useQuery<ApiResponse<any>>({
|
||
queryKey: ['drawer-schedule', doctorUuid, clinicUuid],
|
||
queryFn: () => api.get(
|
||
`/api/v1/appointment-settings/weekly-schedule/${doctorUuid}`
|
||
+ (clinicUuid ? `?clinic_uuid=${encodeURIComponent(clinicUuid)}` : '')
|
||
),
|
||
enabled: !!doctorUuid,
|
||
});
|
||
const bookingMode: 'slot' | 'service' =
|
||
((scheduleQ.data?.data as any)?.data?.meta ?? (scheduleQ.data?.data as any)?.meta)?.booking_mode === 'service'
|
||
? 'service' : 'slot';
|
||
const serviceMode = bookingMode === 'service' && !isReserve;
|
||
|
||
const sectionsQ = useQuery<ApiResponse<Option[]>>({
|
||
queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'),
|
||
});
|
||
const itemsQ = useQuery<ApiResponse<Option[]>>({
|
||
queryKey: ['service-items', sectionUuid],
|
||
queryFn: () => api.get(`/api/v1/service-items/${sectionUuid}`),
|
||
enabled: !!sectionUuid,
|
||
});
|
||
const staffQ = useQuery<ApiResponse<Option[]>>({
|
||
queryKey: ['staff-list'], queryFn: () => api.get('/api/v1/staff'),
|
||
});
|
||
|
||
// ── timing ─────────────────────────────────────────────────────────────────
|
||
const [date, setDate] = useState(defaultDate);
|
||
const [duration, setDuration] = useState(40);
|
||
const [start, setStart] = useState('15:00');
|
||
const [end, setEnd] = useState(addMinutes('15:00', 40));
|
||
useEffect(() => { setEnd(addMinutes(start, duration)); }, [start, duration]);
|
||
|
||
// ── service-mode: چند سرویس + زمانهای خالیِ پیشنهادی ─────────────────────────
|
||
const [serviceUuids, setServiceUuids] = useState<string[]>([]);
|
||
const [svcNames, setSvcNames] = useState<Record<string, string>>({});
|
||
const [pickedSlot, setPickedSlot] = useState<{ start: number; end: number } | null>(null);
|
||
useEffect(() => { setPickedSlot(null); }, [serviceUuids, date]);
|
||
|
||
const svcSlotsQ = useQuery<ApiResponse<any>>({
|
||
queryKey: ['drawer-service-slots', doctorUuid, date, serviceUuids, clinicUuid],
|
||
queryFn: () => api.get(
|
||
`/api/v1/appointment-service-slots?doctor_uuid=${doctorUuid}&date=${date}`
|
||
+ serviceUuids.map(u => `&service_item_uuids[]=${encodeURIComponent(u)}`).join('')
|
||
+ (clinicUuid ? `&clinic_uuid=${encodeURIComponent(clinicUuid)}` : '')
|
||
),
|
||
enabled: serviceMode && !!date && serviceUuids.length > 0,
|
||
});
|
||
const svcSlots = ((svcSlotsQ.data?.data as any)?.start_times ?? []) as Array<{ start: number; end: number; start_time: string }>;
|
||
const totalMinutes = (svcSlotsQ.data?.data as any)?.total_duration_minutes as number | undefined;
|
||
|
||
// ── deposit / status / notes ───────────────────────────────────────────────
|
||
const [depositRequired, setDepositRequired] = useState(false);
|
||
const [depositToman, setDepositToman] = useState(0);
|
||
const [status, setStatus] = useState('pending');
|
||
const [note, setNote] = useState('');
|
||
|
||
// ── هزینه ویزیت — الزامی بودن از تنظیمات «الزامی کردن هزینه ویزیت» (فیلد UI تومان،
|
||
// API ریالی). بدون این مقدار وقتی فلگ فعال است backend خطای ۴۲۲ میدهد.
|
||
const pricingQ = useQuery<ApiResponse<{ free_visit_price_rials: number; require_visit_price: boolean }>>({
|
||
queryKey: ['insurance-pricing'], queryFn: () => api.get('/api/v1/insurance-pricing'),
|
||
});
|
||
const freeVisit = (pricingQ.data as any)?.data?.free_visit_price_rials ?? 0;
|
||
const requireVisit = (pricingQ.data as any)?.data?.require_visit_price ?? false;
|
||
const [visitPriceToman, setVisitPriceToman] = useState(0);
|
||
const [visitPriceTouched, setVisitPriceTouched] = useState(false);
|
||
useEffect(() => {
|
||
if (!visitPriceTouched && freeVisit > 0) setVisitPriceToman(rialToToman(freeVisit));
|
||
}, [freeVisit, visitPriceTouched]);
|
||
|
||
const effectiveName = pickedPatient?.user_name || name.trim();
|
||
const effectiveMobile = pickedPatient?.user_mobile || mobile.trim();
|
||
const effectiveNationalCode = (pickedPatient?.user_national_code || nationalCode).replace(/\D/g, '');
|
||
const timingValid = isReserve
|
||
? true
|
||
: serviceMode
|
||
? (serviceUuids.length > 0 && !!pickedSlot)
|
||
: (!!start && !!end);
|
||
const visitPriceValid = !requireVisit || visitPriceToman > 0;
|
||
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10
|
||
&& effectiveNationalCode.length === 10 && timingValid && visitPriceValid;
|
||
|
||
const create = useMutation({
|
||
mutationFn: async () => {
|
||
const slotStart = isReserve ? toEpoch(date, '00:00') : serviceMode ? pickedSlot!.start : toEpoch(date, start);
|
||
const slotEnd = isReserve ? toEpoch(date, '00:00') : serviceMode ? pickedSlot!.end : toEpoch(date, end);
|
||
const payload: Record<string, unknown> = {
|
||
doctor_uuid: doctorUuid,
|
||
slot_start: slotStart,
|
||
slot_end: slotEnd,
|
||
patient_name: effectiveName,
|
||
patient_mobile: effectiveMobile,
|
||
patient_national_code: effectiveNationalCode,
|
||
is_reserve: isReserve,
|
||
...(sectionUuid ? { service_section_uuid: sectionUuid } : {}),
|
||
// حالت سرویس: چند سرویس؛ حالت اسلاتی: تک سرویسِ workflow (اختیاری).
|
||
...(serviceMode ? { service_item_uuids: serviceUuids } : itemUuid ? { service_item_uuid: itemUuid } : {}),
|
||
...(staffUuid ? { staff_uuid: staffUuid } : {}),
|
||
...(depositRequired ? { deposit_required: true, deposit_amount_rials: tomanToRial(depositToman) } : {}),
|
||
...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {}),
|
||
...(note.trim() ? { note: note.trim() } : {}),
|
||
};
|
||
const res: any = await api.post('/api/v1/my/appointment', payload);
|
||
// POST creates a pending booking; apply the picked status afterwards.
|
||
if (status !== 'pending' && res?.data?.uuid) {
|
||
await api.patch(`/api/v1/appointment/${res.data.uuid}/status`, { status, version: 1 });
|
||
}
|
||
return res;
|
||
},
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey });
|
||
toast.success(isReserve ? 'نوبت رزرو ثبت شد' : 'نوبت با موفقیت ثبت شد');
|
||
onClose();
|
||
},
|
||
onError: (e: any) => toast.error(e.message || 'خطا در ثبت نوبت'),
|
||
});
|
||
|
||
const label = { fontSize: 12.5, color: 'var(--text-3)' } as const;
|
||
const patients = useMemo(() => patientsQ.data?.data ?? [], [patientsQ.data]);
|
||
|
||
return (
|
||
<Modal open title={isReserve ? 'اضافه کردن نوبت رزرو' : 'اضافه کردن نوبت جدید'} onClose={onClose}>
|
||
<div>
|
||
<div style={{ fontSize: 13.5, fontWeight: 700, marginBottom: 10 }}>اطلاعات مراجعه کننده:</div>
|
||
<label style={label}>انتخاب مراجعه کننده</label>
|
||
<div className="field" style={{ margin: '6px 0 8px' }}>
|
||
<input value={pickedPatient ? `${pickedPatient.user_name ?? ''} — ${pickedPatient.user_mobile ?? ''}` : patientSearch}
|
||
onChange={e => { setPickedPatient(null); setPatientSearch(e.target.value); }}
|
||
placeholder="جستجوی نام، شماره تماس، شماره پرونده..." />
|
||
</div>
|
||
{!pickedPatient && patients.length > 0 && (
|
||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', marginBottom: 10, overflow: 'hidden' }}>
|
||
{patients.map(p => (
|
||
<button key={p.uuid} onClick={() => setPickedPatient(p)} style={{
|
||
display: 'block', width: '100%', padding: '8px 10px', fontSize: 13, textAlign: 'right',
|
||
background: 'transparent', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
|
||
}}>
|
||
{p.user_name} <span style={{ color: 'var(--text-3)', direction: 'ltr' }}>{p.user_mobile}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
{pickedPatient === null && (
|
||
<>
|
||
<div style={{ margin: '4px 0 10px' }}>
|
||
<span className="badge" style={{ color: 'var(--primary)', border: '1px solid var(--primary)', borderRadius: 'var(--r-sm)', padding: '5px 10px', fontSize: 12, display: 'inline-flex', gap: 5, alignItems: 'center' }}>
|
||
<PlusIcon style={{ width: 13 }} /> مراجعه کننده جدید
|
||
</span>
|
||
</div>
|
||
<label style={label}>نام و نام خانوادگی مراجعه کننده</label>
|
||
<div className="field" style={{ margin: '6px 0 10px' }}>
|
||
<input value={name} onChange={e => setName(e.target.value)} placeholder="نام و نام خانوادگی مراجعه کننده را وارد نمایید" />
|
||
</div>
|
||
<label style={label}>شماره تماس</label>
|
||
<div className="field" style={{ margin: '6px 0 12px' }}>
|
||
<input value={mobile} onChange={e => setMobile(sanitizeMobileInput(e.target.value))} placeholder="شماره تماس مراجعه کننده را وارد نمایید" dir="ltr" inputMode="numeric" lang="en" maxLength={11} />
|
||
</div>
|
||
<label style={label}>کد ملی</label>
|
||
<div className="field" style={{ margin: '6px 0 12px' }}>
|
||
<input value={nationalCode} onChange={e => setNationalCode(digitsOnly(e.target.value, 10))}
|
||
placeholder="کد ملی مراجعه کننده را وارد نمایید" dir="ltr" inputMode="numeric" lang="en" maxLength={10} />
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
<div style={{ fontSize: 13.5, fontWeight: 700, margin: '6px 0 10px' }}>مشخصات سرویس:</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 10 }}>
|
||
<div>
|
||
<label style={label}>بخش</label>
|
||
<div style={{ marginTop: 6 }}>
|
||
<SearchableSelect
|
||
options={(sectionsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
|
||
value={sectionUuid || null}
|
||
onChange={v => { setSectionUuid(v ? String(v) : ''); setItemUuid(''); }}
|
||
placeholder="انتخاب بخش"
|
||
isLoading={sectionsQ.isLoading}
|
||
isClearable
|
||
height={38}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style={label}>سرویس{serviceMode ? ' (یک یا چند)' : ''}</label>
|
||
<div style={{ marginTop: 6 }}>
|
||
<SearchableSelect
|
||
options={(itemsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
|
||
value={serviceMode ? null : (itemUuid || null)}
|
||
isDisabled={!sectionUuid}
|
||
isLoading={itemsQ.isLoading}
|
||
placeholder={serviceMode ? 'افزودن سرویس' : 'انتخاب سرویس'}
|
||
onChange={v => {
|
||
const uuid = v ? String(v) : '';
|
||
if (!uuid) { if (!serviceMode) setItemUuid(''); return; }
|
||
if (serviceMode) {
|
||
const name = (itemsQ.data?.data ?? []).find(o => o.uuid === uuid)?.name ?? '';
|
||
setServiceUuids(prev => prev.includes(uuid) ? prev : [...prev, uuid]);
|
||
setSvcNames(prev => ({ ...prev, [uuid]: name }));
|
||
} else {
|
||
setItemUuid(uuid);
|
||
}
|
||
}}
|
||
height={38}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{serviceMode && serviceUuids.length > 0 && (
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 10 }}>
|
||
{serviceUuids.map(uuid => (
|
||
<span key={uuid} className="badge" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', padding: '4px 8px' }}>
|
||
{svcNames[uuid] ?? uuid}
|
||
<button type="button" aria-label="حذف سرویس" onClick={() => setServiceUuids(prev => prev.filter(u => u !== uuid))}
|
||
style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--text-3)', fontSize: 14, lineHeight: 1 }}>×</button>
|
||
</span>
|
||
))}
|
||
{totalMinutes != null && <span style={{ fontSize: 12, color: 'var(--text-3)', alignSelf: 'center' }}>مدت کل: {totalMinutes} دقیقه</span>}
|
||
</div>
|
||
)}
|
||
<label style={label}>انتخاب پرسنل</label>
|
||
<div style={{ margin: '6px 0 12px' }}>
|
||
<SearchableSelect
|
||
options={(staffQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.full_name ?? '' }))}
|
||
value={staffUuid || null}
|
||
onChange={v => setStaffUuid(v ? String(v) : '')}
|
||
placeholder="انتخاب..."
|
||
isLoading={staffQ.isLoading}
|
||
isClearable
|
||
height={38}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ fontSize: 13.5, fontWeight: 700, margin: '6px 0 10px' }}>زمان نوبت:</div>
|
||
<label style={label}>انتخاب تاریخ</label>
|
||
<div style={{ margin: '6px 0 10px' }}><PersianDateInput value={date} onChange={setDate} /></div>
|
||
|
||
{serviceMode ? (
|
||
<div style={{ marginBottom: 12 }}>
|
||
<label style={label}>زمانهای خالی پیشنهادی</label>
|
||
{serviceUuids.length === 0 ? (
|
||
<div style={{ fontSize: 12.5, color: 'var(--text-3)', marginTop: 6 }}>ابتدا سرویس را انتخاب کنید.</div>
|
||
) : svcSlotsQ.isLoading ? (
|
||
<div style={{ fontSize: 12.5, color: 'var(--text-3)', marginTop: 6 }}>در حال محاسبه...</div>
|
||
) : svcSlots.length === 0 ? (
|
||
<div style={{ fontSize: 12.5, color: 'var(--danger)', marginTop: 6 }}>برای این سرویس در این روز زمان خالی کافی نیست؛ روز دیگری انتخاب کنید.</div>
|
||
) : (
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 }}>
|
||
{svcSlots.map(s => {
|
||
const active = pickedSlot?.start === s.start;
|
||
return (
|
||
<button key={s.start} type="button" dir="ltr" onClick={() => setPickedSlot({ start: s.start, end: s.end })}
|
||
style={{ fontSize: 13, padding: '6px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontFamily: 'inherit',
|
||
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
|
||
background: active ? 'var(--primary)' : 'var(--surface)', color: active ? '#fff' : 'var(--text)' }}>
|
||
{s.start_time}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<>
|
||
<label style={label}>زمان پیش فرض (دقیقه)</label>
|
||
<div className="field" style={{ margin: '6px 0 10px' }}>
|
||
<input aria-label="زمان پیش فرض" type="text" inputMode="numeric" value={duration} onChange={e => setDuration(Math.max(5, Number(digitsOnly(e.target.value)) || 0))} dir="ltr" />
|
||
</div>
|
||
{!isReserve && (
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 12 }}>
|
||
<div>
|
||
<label style={label}>ساعت شروع</label>
|
||
<div className="field" style={{ marginTop: 6 }}><input aria-label="ساعت شروع" type="time" value={start} onChange={e => setStart(e.target.value)} dir="ltr" /></div>
|
||
</div>
|
||
<div>
|
||
<label style={label}>ساعت پایان</label>
|
||
<div className="field" style={{ marginTop: 6 }}><input aria-label="ساعت پایان" type="time" value={end} onChange={e => setEnd(e.target.value)} dir="ltr" /></div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{!isReserve && (
|
||
<>
|
||
<div style={{ fontSize: 13.5, fontWeight: 700, margin: '6px 0 10px' }}>بیعانه:</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
|
||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
|
||
<input type="checkbox" checked={depositRequired} onChange={e => setDepositRequired(e.target.checked)} />
|
||
بیعانه مورد نیاز است.
|
||
</label>
|
||
</div>
|
||
{depositRequired && (
|
||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 10, marginBottom: 12 }}>
|
||
<div style={{ flex: 1 }}>
|
||
<label style={label}>مبلغ بیعانه (تومان)</label>
|
||
<div style={{ marginTop: 6 }}>
|
||
<PriceInput value={depositToman} onChange={setDepositToman} />
|
||
</div>
|
||
</div>
|
||
<WalletChargeLink mobile={effectiveMobile} />
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
<div style={{ fontSize: 13.5, fontWeight: 700, margin: '6px 0 10px' }}>هزینه ویزیت:</div>
|
||
<label style={label}>
|
||
هزینه ویزیت (تومان){requireVisit && <span style={{ color: 'var(--danger)' }}> *</span>}
|
||
</label>
|
||
<div style={{ margin: '6px 0 4px' }}>
|
||
<PriceInput value={visitPriceToman} onChange={(v) => { setVisitPriceToman(v); setVisitPriceTouched(true); }} />
|
||
</div>
|
||
{requireVisit && visitPriceToman <= 0 && (
|
||
<div style={{ fontSize: 12, color: 'var(--danger)', marginBottom: 12 }}>هزینه ویزیت الزامی است</div>
|
||
)}
|
||
|
||
<label style={label}>انتخاب وضعیت</label>
|
||
<div style={{ margin: '6px 0 12px' }}>
|
||
<SearchableSelect
|
||
options={[{ value: 'pending', label: 'ثبت شده' }, { value: 'confirmed', label: 'قطعی شده' }]}
|
||
value={status || null}
|
||
onChange={v => setStatus(v ? String(v) : '')}
|
||
placeholder="انتخاب وضعیت"
|
||
height={38}
|
||
/>
|
||
</div>
|
||
|
||
<div className="field" style={{ height: 'auto', marginBottom: 16 }}>
|
||
<textarea value={note} onChange={e => setNote(e.target.value)} rows={3} placeholder="توضیحات..."
|
||
style={{ width: '100%', border: 'none', background: 'transparent', fontFamily: 'inherit', resize: 'vertical' }} />
|
||
</div>
|
||
|
||
<button className="btn primary" style={{ width: '100%' }} disabled={!valid || create.isPending} onClick={() => create.mutate()}>
|
||
ثبت نوبت
|
||
</button>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
}
|