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 PriceInput from './ui/PriceInput';
|
||||||
import SearchableSelect from './ui/SearchableSelect';
|
import SearchableSelect from './ui/SearchableSelect';
|
||||||
import { WalletChargeLink } from './AppointmentActions';
|
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 Option { uuid: string; name?: string; full_name?: string }
|
||||||
interface PatientRow { uuid: string; user_name?: string; user_mobile?: string; user_national_code?: 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 [status, setStatus] = useState('pending');
|
||||||
const [note, setNote] = useState('');
|
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 effectiveName = pickedPatient?.user_name || name.trim();
|
||||||
const effectiveMobile = pickedPatient?.user_mobile || mobile.trim();
|
const effectiveMobile = pickedPatient?.user_mobile || mobile.trim();
|
||||||
const effectiveNationalCode = (pickedPatient?.user_national_code || nationalCode).replace(/\D/g, '');
|
const effectiveNationalCode = (pickedPatient?.user_national_code || nationalCode).replace(/\D/g, '');
|
||||||
@@ -116,8 +129,9 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
|
|||||||
: serviceMode
|
: serviceMode
|
||||||
? (serviceUuids.length > 0 && !!pickedSlot)
|
? (serviceUuids.length > 0 && !!pickedSlot)
|
||||||
: (!!start && !!end);
|
: (!!start && !!end);
|
||||||
|
const visitPriceValid = !requireVisit || visitPriceToman > 0;
|
||||||
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10
|
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10
|
||||||
&& effectiveNationalCode.length === 10 && timingValid;
|
&& effectiveNationalCode.length === 10 && timingValid && visitPriceValid;
|
||||||
|
|
||||||
const create = useMutation({
|
const create = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -136,6 +150,7 @@ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey
|
|||||||
...(serviceMode ? { service_item_uuids: serviceUuids } : itemUuid ? { service_item_uuid: itemUuid } : {}),
|
...(serviceMode ? { service_item_uuids: serviceUuids } : itemUuid ? { service_item_uuid: itemUuid } : {}),
|
||||||
...(staffUuid ? { staff_uuid: staffUuid } : {}),
|
...(staffUuid ? { staff_uuid: staffUuid } : {}),
|
||||||
...(depositRequired ? { deposit_required: true, deposit_amount_rials: tomanToRial(depositToman) } : {}),
|
...(depositRequired ? { deposit_required: true, deposit_amount_rials: tomanToRial(depositToman) } : {}),
|
||||||
|
...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {}),
|
||||||
...(note.trim() ? { note: note.trim() } : {}),
|
...(note.trim() ? { note: note.trim() } : {}),
|
||||||
};
|
};
|
||||||
const res: any = await api.post('/api/v1/my/appointment', payload);
|
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>
|
<label style={label}>انتخاب وضعیت</label>
|
||||||
<div style={{ margin: '6px 0 12px' }}>
|
<div style={{ margin: '6px 0 12px' }}>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { toast } from 'sonner';
|
|||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import type { PaginatedResponse, ApiResponse } from '../lib/api';
|
import type { PaginatedResponse, ApiResponse } from '../lib/api';
|
||||||
import type { Appointment } from '../types';
|
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 { useAuthStore } from '../stores/authStore';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal';
|
import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal';
|
||||||
@@ -118,6 +119,20 @@ export function NewAppointmentModal({
|
|||||||
const role = useAuthStore(s => s.primaryRole);
|
const role = useAuthStore(s => s.primaryRole);
|
||||||
const createEndpoint = role === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment';
|
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 mobileValid = /^09\d{9}$/.test(mobile);
|
||||||
const serviceTimingValid = !serviceMode || (pick.serviceUuids.length > 0 && !!pick.slot);
|
const serviceTimingValid = !serviceMode || (pick.serviceUuids.length > 0 && !!pick.slot);
|
||||||
// یک بیمارِ یافتشده که کد ملی دارد، بدون فرم اضافی قابل استفاده است.
|
// یک بیمارِ یافتشده که کد ملی دارد، بدون فرم اضافی قابل استفاده است.
|
||||||
@@ -127,7 +142,8 @@ export function NewAppointmentModal({
|
|||||||
const effectiveName = foundWithNationalCode ? (lookup?.name ?? '') : patientName.trim();
|
const effectiveName = foundWithNationalCode ? (lookup?.name ?? '') : patientName.trim();
|
||||||
const effectiveNationalCode = foundWithNationalCode ? (lookup?.national_code ?? '') : nationalCode;
|
const effectiveNationalCode = foundWithNationalCode ? (lookup?.national_code ?? '') : nationalCode;
|
||||||
const detailsValid = effectiveName.length >= 2 && effectiveNationalCode.length === 10;
|
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({
|
const search = useMutation({
|
||||||
mutationFn: () => api.get(`/api/v1/my/appointment/patient-lookup?mobile=${encodeURIComponent(mobile)}`),
|
mutationFn: () => api.get(`/api/v1/my/appointment/patient-lookup?mobile=${encodeURIComponent(mobile)}`),
|
||||||
@@ -149,6 +165,7 @@ export function NewAppointmentModal({
|
|||||||
patient_name: effectiveName,
|
patient_name: effectiveName,
|
||||||
patient_national_code: effectiveNationalCode,
|
patient_national_code: effectiveNationalCode,
|
||||||
...(serviceMode ? { service_item_uuids: pick.serviceUuids } : {}),
|
...(serviceMode ? { service_item_uuids: pick.serviceUuids } : {}),
|
||||||
|
...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {}),
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('نوبت با موفقیت ثبت شد');
|
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' }}>
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||||
<button className="btn sm" onClick={onClose}>انصراف</button>
|
<button className="btn sm" onClick={onClose}>انصراف</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user