feat: add visit price requirement feature

- Introduced a new boolean flag `require_visit_price` in the `EntityInsurancePricing` to enforce visit price for appointments.
- Updated the appointment creation endpoints to validate `visit_price_rials` based on the new flag.
- Added `visit_price_rials` field to the `Appointment` entity to store the visit price.
- Enhanced the `PatientService` to validate visit price during session creation.
- Updated API documentation to reflect changes in appointment and insurance pricing.
- Implemented a new service `VisitPriceRequirementResolver` to determine if a visit price is required for a doctor based on their pricing settings.
- Added migrations to update the database schema for the new fields.
This commit is contained in:
hamed
2026-07-16 19:44:30 +03:30
parent 880c4c06cb
commit a0ddb4c0d1
18 changed files with 497 additions and 22 deletions
+32 -2
View File
@@ -13,7 +13,7 @@ import SearchableSelect from '../components/ui/SearchableSelect';
import { WalletChargeLink } from '../components/AppointmentActions';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
import { tehranWallClockToUnix } from '../lib/utils';
import { tehranWallClockToUnix, rialToToman, tomanToRial } from '../lib/utils';
/**
* افزودن نوبت — صفحهٔ کامل (بازسازی `CreateTurn.jsx` طرح tauri). از همان endpointهای
@@ -104,6 +104,21 @@ export default function AppointmentCreatePage() {
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, '');
@@ -111,7 +126,7 @@ export default function AppointmentCreatePage() {
? (servicePick.serviceUuids.length > 0 && !!servicePick.slot)
: (!!start && !!end);
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10
&& effectiveNationalCode.length === 10 && timingValid;
&& effectiveNationalCode.length === 10 && timingValid && (!requireVisit || visitPriceToman > 0);
const create = useMutation({
mutationFn: async () => {
@@ -131,6 +146,7 @@ export default function AppointmentCreatePage() {
}),
...(staffUuid ? { staff_uuid: staffUuid } : {}),
...(depositRequired ? { deposit_required: true, deposit_amount_rials: depositRials } : {}),
...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {}),
...(note.trim() ? { note: note.trim() } : {}),
};
const res: any = await api.post(createEndpoint, payload);
@@ -458,6 +474,20 @@ export default function AppointmentCreatePage() {
</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' }}>