Files
clinicpro/assets/admin/pages/AppointmentCreatePage.tsx
T

519 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { PlusIcon, ChevronRightIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { useAuthStore } from '../stores/authStore';
import PersianDateInput from '../components/ui/PersianDateInput';
import PriceInput from '../components/ui/PriceInput';
import DigitInput from '../components/ui/DigitInput';
import SearchableSelect from '../components/ui/SearchableSelect';
import { WalletChargeLink } from '../components/AppointmentActions';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
/**
* افزودن نوبت — صفحهٔ کامل (بازسازی `CreateTurn.jsx` طرح tauri). از همان endpointهای
* موجود استفاده می‌کند: `POST /api/v1/my/appointment` (یا admin) + در صورت نیاز
* `PATCH .../status`. هیچ endpoint جدیدی ساخته نشده است.
*/
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')}`;
};
const label = { fontSize: 12.5, color: 'var(--text-3)', display: 'block' } as const;
const sectionTitle: React.CSSProperties = { fontSize: 18, fontWeight: 700, color: 'var(--text)', margin: '22px 0 14px' };
export default function AppointmentCreatePage() {
const navigate = useNavigate();
const [params] = useSearchParams();
const qc = useQueryClient();
const primaryRole = useAuthStore(s => s.primaryRole);
const dbUuid = useAuthStore(s => s.dbUuid);
const isDoctor = primaryRole === 'doctor';
const today = new Date().toISOString().slice(0, 10);
// ── پزشک
const [doctorUuid, setDoctorUuid] = useState(isDoctor && dbUuid ? dbUuid : (params.get('doctor') ?? ''));
const clinicDoctorsQuery = useQuery<ApiResponse<{ data: { uuid: string; name: string }[] }>>({
queryKey: ['clinic-doctors', dbUuid],
queryFn: () => api.get(`/api/v1/clinic/doctor-list/${dbUuid}`),
enabled: !isDoctor && !!dbUuid,
});
const doctorOptions = (clinicDoctorsQuery.data?.data?.data ?? []).map(d => ({ value: d.uuid, label: d.name }));
// ── بیمار: جستجوی رکورد موجود یا ورود شخص جدید
const [patientSearch, setPatientSearch] = useState('');
const [picked, setPicked] = useState<PatientRow | null>(null);
const [newPatient, setNewPatient] = useState(false); // فرم «مراجعه کننده جدید» فعال است؟
const [name, setName] = useState('');
const [mobile, setMobile] = useState('');
const [nationalCode, setNationalCode] = useState('');
const patientsQ = useQuery<ApiResponse<PatientRow[]>>({
queryKey: ['create-patients', patientSearch],
queryFn: () => api.get(`/api/v1/patients?search=${encodeURIComponent(patientSearch)}&limit=10`),
enabled: patientSearch.trim().length >= 2,
});
const patients = useMemo(() => patientsQ.data?.data ?? [], [patientsQ.data]);
// ── مشخصات سرویس
const [sectionUuid, setSectionUuid] = useState(''); // بخشِ در حال مرور (برای دیدن سرویس‌هایش)
// سرویس‌های انتخاب‌شده — انباشته از چند بخش؛ هر کدام قابل حذف. نام را نگه می‌داریم تا در chip نشان دهیم.
const [selectedServices, setSelectedServices] = useState<{ uuid: string; name: string }[]>([]);
const [staffUuid, setStaffUuid] = useState('');
const toggleServiceItem = (uuid: string, name: string) =>
setSelectedServices(prev => prev.some(s => s.uuid === uuid)
? prev.filter(s => s.uuid !== uuid)
: [...prev, { uuid, name }]);
const removeService = (uuid: string) =>
setSelectedServices(prev => prev.filter(s => s.uuid !== uuid));
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') });
// ── روش نوبت‌دهی پزشک (سرویسی/اسلاتی)
const { bookingMode, services } = useDoctorBookingServices(doctorUuid);
const serviceMode = bookingMode === 'service';
const [servicePick, setServicePick] = useState<{ serviceUuids: string[]; durations: Record<string, number>; slot: { start: number; end: number } | null }>({ serviceUuids: [], durations: {}, slot: null });
// ── زمان نوبت
const [date, setDate] = useState(params.get('date') || today);
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]);
// ── بیعانه / وضعیت / توضیحات
const [depositRequired, setDepositRequired] = useState(false);
const [depositToman, setDepositToman] = useState(0);
const [status, setStatus] = useState('pending');
const [note, setNote] = useState('');
// ── هزینه ویزیت — الزامی بودن از تنظیمات «الزامی کردن هزینه ویزیت» (کاربر بدون
// پروفایل doctor/clinic از این endpoint 403 می‌گیرد → فلگ false و فیلد اختیاری می‌ماند)
const pricingQ = useQuery<{ data: { 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;
// فیلد UI تومان است؛ API ریالی (visit_price_rials).
const [visitPriceToman, setVisitPriceToman] = useState(0);
const [visitPriceTouched, setVisitPriceTouched] = useState(false);
useEffect(() => {
if (!visitPriceTouched && freeVisit > 0) setVisitPriceToman(rialToToman(freeVisit));
}, [freeVisit, visitPriceTouched]);
const effectiveName = picked?.user_name || name.trim();
const effectiveMobile = picked?.user_mobile || mobile.trim();
const effectiveNationalCode = (picked?.user_national_code || nationalCode).replace(/\D/g, '');
const timingValid = serviceMode
? (servicePick.serviceUuids.length > 0 && !!servicePick.slot)
: (!!start && !!end);
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10
&& effectiveNationalCode.length === 10 && timingValid && (!requireVisit || visitPriceToman > 0);
const create = useMutation({
mutationFn: async () => {
const createEndpoint = primaryRole === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment';
const payload: Record<string, unknown> = {
doctor_uuid: doctorUuid,
slot_start: serviceMode ? servicePick.slot!.start : toEpoch(date, start),
slot_end: serviceMode ? servicePick.slot!.end : toEpoch(date, end),
patient_name: effectiveName,
patient_mobile: effectiveMobile,
patient_national_code: effectiveNationalCode,
...(serviceMode
? { service_item_uuids: servicePick.serviceUuids, duration_from_services: true, service_durations: servicePick.durations }
: {
...(sectionUuid ? { service_section_uuid: sectionUuid } : {}),
...(selectedServices.length ? { service_item_uuids: selectedServices.map(s => s.uuid) } : {}),
}),
...(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(createEndpoint, payload);
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: ['appointments'] });
toast.success('نوبت با موفقیت ثبت شد');
// به همان روزِ نوبت برگرد، نه امروز.
navigate(`/admin/appointments?date=${date}`);
},
onError: (e: any) => toast.error(e.message || 'خطا در ثبت نوبت'),
});
return (
<div style={{ padding: '20px 24px', maxWidth: 1080, margin: '0 auto' }}>
{/* بردکرامب */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 18 }}>
<button onClick={() => navigate(-1)} className="btn sm" style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<ChevronRightIcon style={{ width: 15, height: 15 }} /> بازگشت
</button>
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>نوبت ها</span>
<span style={{ color: 'var(--text-3)' }}></span>
<span style={{ fontSize: 15, fontWeight: 700, color: 'var(--text)' }}>ثبت نوبت جدید</span>
</div>
<div style={{ background: 'var(--surface)', border: '1px solid var(--border-2)', borderRadius: 'var(--r-sm)', padding: 28 }}>
{/* پزشک — فقط برای admin/clinic */}
{!isDoctor && (
<>
<div style={{ ...sectionTitle, marginTop: 0 }}>پزشک:</div>
<label style={label}>انتخاب پزشک</label>
<div style={{ margin: '6px 0 4px', maxWidth: 400 }}>
<SearchableSelect
options={doctorOptions}
value={doctorUuid || null}
onChange={(v) => setDoctorUuid(v ? String(v) : '')}
placeholder="انتخاب پزشک..."
isClearable
height={44}
/>
</div>
</>
)}
{/* مراجعه کننده */}
<div style={{ ...sectionTitle, marginTop: isDoctor ? 0 : 18 }}>اطلاعات مراجعه کننده :</div>
{/* حالت جستجوی مراجعه‌کنندهٔ موجود */}
{!newPatient && (
<>
<label style={label}>انتخاب مراجعه کننده</label>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12, margin: '6px 0 8px' }}>
<div className="field" style={{ width: 400, maxWidth: '100%' }}>
<MagnifyingGlassIcon style={{ width: 18, height: 18, color: 'var(--text-3)', flexShrink: 0 }} />
<input value={picked ? `${picked.user_name ?? ''}${picked.user_mobile ?? ''}` : patientSearch}
onChange={e => { setPicked(null); setPatientSearch(e.target.value); }}
placeholder="جستجوی نام، شماره تماس، شماره پرونده..." />
</div>
<button type="button" className="btn outline"
onClick={() => { setPicked(null); setPatientSearch(''); setNewPatient(true); }}
style={{ height: 44, minWidth: 161, gap: 6 }}>
<PlusIcon style={{ width: 16, height: 16 }} /> مراجعه کننده جدید
</button>
</div>
{!picked && patients.length > 0 && (
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', marginBottom: 10, overflow: 'hidden', maxWidth: 400 }}>
{patients.map(p => (
<button key={p.uuid}
onClick={() => { setPicked(p); setNationalCode((p.user_national_code ?? '').replace(/\D/g, '').slice(0, 10)); }}
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>
)}
{/* کارت بیمارِ انتخاب‌شده + کد ملی (برای ثبت نوبت الزامی است) */}
{picked && (
<div style={{ maxWidth: 400, border: '1px solid var(--border-2)', borderRadius: 'var(--r-sm)', padding: 12, marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
<span style={{ fontSize: 13.5, fontWeight: 600 }}>
{picked.user_name} <span style={{ color: 'var(--text-3)', direction: 'ltr', fontWeight: 400 }}>{picked.user_mobile}</span>
</span>
<button type="button" className="btn sm ghost"
onClick={() => { setPicked(null); setNationalCode(''); }}>تغییر</button>
</div>
{/^\d{10}$/.test((picked.user_national_code ?? '').replace(/\D/g, '')) ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 13 }}>
<span style={{ color: 'var(--text-3)' }}>کد ملی</span>
<span dir="ltr" style={{ fontWeight: 600 }}>{nationalCode}</span>
</div>
) : (
<>
<label style={label}>کد ملی (ثبت‌نشده وارد کنید)</label>
<div className="field" style={{ marginTop: 6, height: 44 }}>
<DigitInput value={nationalCode} onChange={setNationalCode} maxDigits={10}
placeholder="کد ملی مراجعه کننده" />
</div>
</>
)}
</div>
)}
</>
)}
{/* حالت ثبت مراجعه‌کنندهٔ جدید */}
{newPatient && (
<>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '2px 0 6px' }}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-2)' }}>مراجعه کننده جدید</span>
<button type="button" className="btn sm ghost"
onClick={() => { setName(''); setMobile(''); setNationalCode(''); setNewPatient(false); }}>
انتخاب از لیست موجود
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginTop: 4 }}>
<div>
<label style={label}>نام و نام خانوادگی مراجعه کننده</label>
<div className="field" style={{ marginTop: 6, height: 44 }}>
<input value={name} onChange={e => setName(e.target.value)} placeholder="نام و نام خانوادگی مراجعه کننده" />
</div>
</div>
<div>
<label style={label}>شماره تماس</label>
<div className="field" style={{ marginTop: 6, height: 44 }}>
<DigitInput value={mobile} onChange={setMobile} maxDigits={11} placeholder="شماره تماس مراجعه کننده" />
</div>
</div>
<div>
<label style={label}>کد ملی</label>
<div className="field" style={{ marginTop: 6, height: 44 }}>
<DigitInput value={nationalCode} onChange={setNationalCode} maxDigits={10}
placeholder="کد ملی مراجعه کننده" />
</div>
</div>
</div>
</>
)}
{/* مشخصات سرویس */}
<div style={sectionTitle}>مشخصات سرویس</div>
{!serviceMode ? (
<>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16, marginBottom: 10 }}>
<div>
<label style={label}>بخش</label>
<div style={{ marginTop: 6 }}>
<SearchableSelect
inputId="appt-section-select"
options={(sectionsQ.data?.data ?? []).map(o => ({ value: o.uuid, label: o.name ?? '' }))}
value={sectionUuid || null}
onChange={v => setSectionUuid(v ? String(v) : '')}
placeholder="ابتدا بخش را انتخاب کنید"
isLoading={sectionsQ.isLoading}
isClearable
height={44}
/>
</div>
</div>
<div>
<label style={label}>پرسنل</label>
<div style={{ marginTop: 6 }}>
<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={44}
/>
</div>
</div>
</div>
{/* سرویس‌های بخشِ انتخاب‌شده — چند انتخابی، به لیست انباشته اضافه می‌شوند */}
{sectionUuid && (
<>
<label style={label}>سرویس‌های این بخش (یک یا چند)</label>
{itemsQ.isLoading ? (
<div style={{ fontSize: 12.5, color: 'var(--text-3)', margin: '6px 0 10px' }}>در حال بارگذاری...</div>
) : (itemsQ.data?.data ?? []).length === 0 ? (
<div style={{ fontSize: 12.5, color: 'var(--text-3)', margin: '6px 0 10px' }}>سرویسی در این بخش تعریف نشده است.</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, margin: '6px 0 12px' }}>
{(itemsQ.data?.data ?? []).map(o => {
const active = selectedServices.some(s => s.uuid === o.uuid);
return (
<button key={o.uuid} type="button" onClick={() => toggleServiceItem(o.uuid, o.name ?? '')}
style={{
display: 'flex', alignItems: 'center', gap: 8, padding: '9px 12px', borderRadius: 'var(--r-sm)',
cursor: 'pointer', textAlign: 'right', fontFamily: 'inherit', fontSize: 13,
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
background: active ? 'var(--primary-soft)' : 'var(--surface)',
}}>
<span style={{
width: 16, height: 16, borderRadius: 4, display: 'grid', placeItems: 'center', flexShrink: 0,
border: active ? '1px solid var(--primary)' : '1px solid var(--border-2)',
background: active ? 'var(--primary)' : 'transparent',
}}>
{active && <span style={{ width: 8, height: 8, background: '#fff', borderRadius: 2 }} />}
</span>
{o.name}
</button>
);
})}
</div>
)}
</>
)}
{/* لیستِ انباشتهٔ سرویس‌های انتخاب‌شده (از هر بخش) — قابل حذف */}
{selectedServices.length > 0 && (
<div style={{ margin: '4px 0 12px' }}>
<label style={label}>سرویس‌های انتخاب‌شده ({selectedServices.length})</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 6 }}>
{selectedServices.map(s => (
<span key={s.uuid} style={{
display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 6px 5px 10px',
borderRadius: 'var(--r-pill)', fontSize: 12.5, background: 'var(--primary-soft)',
color: 'var(--primary-700)', border: '1px solid var(--primary)',
}}>
{s.name}
<button type="button" aria-label={`حذف ${s.name}`} onClick={() => removeService(s.uuid)}
style={{
display: 'grid', placeItems: 'center', width: 16, height: 16, borderRadius: 999,
border: 'none', cursor: 'pointer', background: 'var(--primary)', color: '#fff',
fontSize: 12, lineHeight: 1, fontFamily: 'inherit',
}}>×</button>
</span>
))}
</div>
</div>
)}
</>
) : (
<div style={{ maxWidth: 400, marginBottom: 10 }}>
<label style={label}>پرسنل</label>
<div style={{ marginTop: 6 }}>
<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={44}
/>
</div>
</div>
)}
{/* زمان نوبت */}
<div style={sectionTitle}>زمان نوبت</div>
{serviceMode ? (
<>
<label style={label}>انتخاب تاریخ</label>
<div style={{ margin: '6px 0 10px', maxWidth: 400 }}><PersianDateInput value={date} onChange={setDate} /></div>
<div style={{ marginBottom: 12 }}>
{doctorUuid ? (
<ServiceSlotPicker
doctorUuid={doctorUuid}
date={date}
services={services}
onSelect={setServicePick}
/>
) : (
<div style={{ fontSize: 12.5, color: 'var(--text-3)' }}>ابتدا پزشک را انتخاب کنید.</div>
)}
</div>
</>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 12 }}>
<div>
<label style={label}>انتخاب تاریخ</label>
<div style={{ marginTop: 6 }}><PersianDateInput value={date} onChange={setDate} /></div>
</div>
<div>
<label style={label}>زمان پیش فرض (دقیقه)</label>
<div className="field" style={{ marginTop: 6, height: 44 }}>
<input aria-label="زمان پیش فرض" type="number" min={5} value={duration} onChange={e => setDuration(Math.max(5, Number(e.target.value) || 0))} dir="ltr" />
</div>
</div>
<div>
<label style={label}>ساعت شروع</label>
<div className="field" style={{ marginTop: 6, height: 44 }}><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, height: 44 }}><input aria-label="ساعت پایان" type="time" value={end} onChange={e => setEnd(e.target.value)} dir="ltr" /></div>
</div>
</div>
)}
{/* بیعانه */}
<div style={sectionTitle}>بیعانه</div>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 14, cursor: 'pointer' }}>
<span style={{
position: 'relative', width: 42, height: 22, borderRadius: 999, flexShrink: 0,
background: depositRequired ? 'var(--primary)' : '#c4c4c4', transition: 'background .2s',
}}>
<input type="checkbox" checked={depositRequired} onChange={e => setDepositRequired(e.target.checked)}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: 'pointer' }} />
<span style={{
position: 'absolute', top: 2, insetInlineStart: depositRequired ? 22 : 2, width: 18, height: 18,
borderRadius: 999, background: '#fff', transition: 'inset-inline-start .2s', boxShadow: '0 1px 2px rgba(0,0,0,.2)',
}} />
</span>
بیعانه مورد نیاز است.
</label>
{depositRequired && (
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: 12, margin: '12px 0' }}>
<div style={{ width: 300, maxWidth: '100%' }}>
<label style={label}>مبلغ بیعانه (تومان)</label>
<div className="field" style={{ marginTop: 6, height: 44 }}><PriceInput value={depositToman} onChange={setDepositToman} /></div>
</div>
<WalletChargeLink mobile={effectiveMobile} />
</div>
)}
{/* هزینه ویزیت */}
<div style={sectionTitle}>هزینه ویزیت</div>
<div style={{ width: 300, maxWidth: '100%', marginBottom: 12 }}>
<label style={label}>
هزینه ویزیت (تومان){requireVisit && <span style={{ color: 'var(--danger)' }}> *</span>}
</label>
<div className="field" style={{ marginTop: 6, height: 44 }}>
<PriceInput value={visitPriceToman} onChange={(v) => { setVisitPriceToman(v); setVisitPriceTouched(true); }} />
</div>
{requireVisit && visitPriceToman <= 0 && (
<span style={{ fontSize: 12, color: 'var(--danger)', display: 'block', marginTop: 4 }}>هزینه ویزیت الزامی است</span>
)}
</div>
<div style={{ maxWidth: 500 }}>
<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={44}
/>
</div>
</div>
<label style={label}>توضیحات</label>
<div className="field" style={{ height: 'auto', margin: '6px 0 16px' }}>
<textarea value={note} onChange={e => setNote(e.target.value)} rows={4} placeholder="توضیحات..."
style={{ width: '100%', border: 'none', background: 'transparent', fontFamily: 'inherit', resize: 'vertical' }} />
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', paddingTop: 4 }}>
<button className="btn primary lg" disabled={!valid || create.isPending} onClick={() => create.mutate()}>
{create.isPending ? '...' : 'ثبت اطلاعات'}
</button>
</div>
</div>
</div>
);
}