feat: add service-based booking mode to appointment scheduling

- Introduced a new booking mode in WeeklySchedule to support service-based appointments.
- Updated SlotCalculatorService to calculate available start times based on selected service durations and buffer times.
- Enhanced AppointmentController to handle service items during booking, calculating slot_end on the server side.
- Implemented validation to ensure at least one bookable service exists for doctors in service mode.
- Added new API endpoint to retrieve available appointment slots based on selected services.
- Updated MyAppointmentsController to accept service items during appointment creation.
- Modified ServiceItem entity to include a bookable flag, allowing services to be marked for scheduling.
- Created migration to add bookable column to service_items table.
- Added tests for service-based slot calculations and validation logic.
This commit is contained in:
hamed
2026-07-15 23:15:45 +03:30
parent 6904361e32
commit 5937f7e176
16 changed files with 817 additions and 26 deletions
+51
View File
@@ -101,11 +101,15 @@ interface BookingMeta {
online_booking_enabled: boolean;
booking_window_value: number;
booking_window_unit: 'week' | 'month';
booking_mode: 'slot' | 'service';
buffer_minutes: number;
}
const DEFAULT_BOOKING_META: BookingMeta = {
online_booking_enabled: true,
booking_window_value: 1,
booking_window_unit: 'month',
booking_mode: 'slot',
buffer_minutes: 0,
};
interface WeeklyScheduleData { uuid: string; doctor_uuid: string; schedule: NewScheduleMap; meta?: BookingMeta; }
@@ -1384,6 +1388,53 @@ export function WeeklyScheduleTab({ doctorUuid, addresses, readOnly = false }: {
</div>
)}
{/* ─ روش نوبت‌دهی */}
<div className="mb-3 rounded-xl border border-slate-200 dark:border-gray-700 overflow-hidden">
<div className="px-4 py-3 bg-slate-50 dark:bg-gray-800/50">
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">روش نوبتدهی</span>
</div>
<div className="px-4 py-3 space-y-3">
<div className="flex flex-wrap gap-2">
{([['slot', 'اسلاتی (مدت ثابت)'], ['service', 'بر اساس سرویس']] as const).map(([val, lbl]) => (
<button
key={val}
type="button"
onClick={() => setMeta(m => ({ ...m, booking_mode: val }))}
className={`px-3 py-1.5 text-sm rounded-lg border transition-colors ${
meta.booking_mode === val
? 'bg-[var(--primary)] text-white border-[var(--primary)]'
: 'bg-white dark:bg-gray-900 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-gray-700'
}`}
>
{lbl}
</button>
))}
</div>
{meta.booking_mode === 'service' ? (
<>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-slate-600 dark:text-slate-400">فاصله بین نوبتها</span>
<input
type="number"
min={0}
value={meta.buffer_minutes}
onChange={(e) => setMeta(m => ({ ...m, buffer_minutes: Math.max(0, Number(e.target.value) || 0) }))}
className="w-16 text-center text-sm rounded-lg border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-1.5 focus:outline-none focus:ring-0"
/>
<span className="text-sm text-slate-600 dark:text-slate-400">دقیقه</span>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500 leading-relaxed">
مدت هر نوبت از «مدت سرویس» انتخابشده تعیین میشود. لازم است حداقل یک سرویس با «نمایش در نوبتدهی» در بخش <span className="font-medium">سرویسها</span> تعریف کنید، وگرنه ذخیره نمیشود.
</p>
</>
) : (
<p className="text-xs text-slate-400 dark:text-slate-500 leading-relaxed">
مدت هر نوبت از «زمان هر نوبت» در شیفتهای زیر تعیین میشود.
</p>
)}
</div>
</div>
{/* ─ نوبت‌دهی آنلاین */}
<div className="mb-3 rounded-xl border border-slate-200 dark:border-gray-700 overflow-hidden">
{/* header + toggle */}