fix(admin): send visit_price_rials from appointment create modal & drawer
NewAppointmentModal and NewAppointmentDrawer never read the require-visit- price setting nor sent visit_price_rials, so booking failed with 422 "هزینه ویزیت الزامی است" when the flag was on. Add a visit-price field (prefilled from the free-visit price), require it when the flag is set, and include visit_price_rials in the POST payload, matching AppointmentCreatePage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import PersianDateInput from './ui/PersianDateInput';
|
||||
import PriceInput from './ui/PriceInput';
|
||||
import SearchableSelect from './ui/SearchableSelect';
|
||||
import { WalletChargeLink } from './AppointmentActions';
|
||||
import { tehranWallClockToUnix, tomanToRial, toEnglishDigits, sanitizeMobileInput } from '../lib/utils';
|
||||
import { tehranWallClockToUnix, tomanToRial, rialToToman, toEnglishDigits, 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 }
|
||||
@@ -108,6 +108,19 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
|
||||
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, '');
|
||||
@@ -116,8 +129,9 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
|
||||
: 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;
|
||||
&& effectiveNationalCode.length === 10 && timingValid && visitPriceValid;
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -136,6 +150,7 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
|
||||
...(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);
|
||||
@@ -340,6 +355,17 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
|
||||
</>
|
||||
)}
|
||||
|
||||
<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
|
||||
|
||||
@@ -9,7 +9,8 @@ import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import type { PaginatedResponse, ApiResponse } from '../lib/api';
|
||||
import type { Appointment } from '../types';
|
||||
import { formatDate, toGregorianDate, formatTime, toEnglishDigits, sanitizeMobileInput } from '../lib/utils';
|
||||
import { formatDate, toGregorianDate, formatTime, toEnglishDigits, sanitizeMobileInput, rialToToman, tomanToRial } from '../lib/utils';
|
||||
import PriceInput from '../components/ui/PriceInput';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal';
|
||||
@@ -118,6 +119,20 @@ export function NewAppointmentModal({
|
||||
const role = useAuthStore(s => s.primaryRole);
|
||||
const createEndpoint = role === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment';
|
||||
|
||||
// هزینه ویزیت — الزامی بودن از تنظیمات «الزامی کردن هزینه ویزیت». فیلد UI تومان،
|
||||
// API ریالی (visit_price_rials). بدون این مقدار، وقتی فلگ فعال است backend خطای ۴۲۲ میدهد.
|
||||
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;
|
||||
const [visitPriceToman, setVisitPriceToman] = useState(0);
|
||||
const [visitPriceTouched, setVisitPriceTouched] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!visitPriceTouched && freeVisit > 0) setVisitPriceToman(rialToToman(freeVisit));
|
||||
}, [freeVisit, visitPriceTouched]);
|
||||
|
||||
const mobileValid = /^09\d{9}$/.test(mobile);
|
||||
const serviceTimingValid = !serviceMode || (pick.serviceUuids.length > 0 && !!pick.slot);
|
||||
// یک بیمارِ یافتشده که کد ملی دارد، بدون فرم اضافی قابل استفاده است.
|
||||
@@ -127,7 +142,8 @@ export function NewAppointmentModal({
|
||||
const effectiveName = foundWithNationalCode ? (lookup?.name ?? '') : patientName.trim();
|
||||
const effectiveNationalCode = foundWithNationalCode ? (lookup?.national_code ?? '') : nationalCode;
|
||||
const detailsValid = effectiveName.length >= 2 && effectiveNationalCode.length === 10;
|
||||
const isValid = mobileValid && (foundWithNationalCode || (needsDetails && detailsValid)) && serviceTimingValid;
|
||||
const visitPriceValid = !requireVisit || visitPriceToman > 0;
|
||||
const isValid = mobileValid && (foundWithNationalCode || (needsDetails && detailsValid)) && serviceTimingValid && visitPriceValid;
|
||||
|
||||
const search = useMutation({
|
||||
mutationFn: () => api.get(`/api/v1/my/appointment/patient-lookup?mobile=${encodeURIComponent(mobile)}`),
|
||||
@@ -149,6 +165,7 @@ export function NewAppointmentModal({
|
||||
patient_name: effectiveName,
|
||||
patient_national_code: effectiveNationalCode,
|
||||
...(serviceMode ? { service_item_uuids: pick.serviceUuids } : {}),
|
||||
...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {}),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('نوبت با موفقیت ثبت شد');
|
||||
@@ -271,6 +288,20 @@ export function NewAppointmentModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={labelSx}>
|
||||
هزینه ویزیت (تومان){requireVisit && <span style={{ color: 'var(--danger)' }}> *</span>}
|
||||
</label>
|
||||
<PriceInput
|
||||
value={visitPriceToman}
|
||||
onChange={(v) => { setVisitPriceToman(v); setVisitPriceTouched(true); }}
|
||||
style={{ ...inputSx, direction: 'ltr' }}
|
||||
/>
|
||||
{requireVisit && visitPriceToman <= 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 6 }}>هزینه ویزیت الزامی است</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button className="btn sm" onClick={onClose}>انصراف</button>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user