אתר של מרפאה עם טופס "השאירו בקשה ונחזור אליכם" מאבד עד 60% מההמרות — משתמשים רוצים לבחור רופא ספציפי, שעה ספציפית, ולקבל אישור מיידי. הזמנה מקוונת עם בחירת משבצת היא סטנדרט לכל מרפאה מודרנית, והיישומים הטובים ביותר משתמשים ברכיבי 1C-Bitrix מתועדים המבטיחים אבטחה וסקלביליות. יישום על 1C-Bitrix מאפשר לקשר את לוח הזמנים למערכת המידע הרפואי (MIS) או לנהל אותו בתוך המערכת אם אין MIS. המהנדסים שלנו, המוסמכים על ידי Bitrix, פרסו פתרונות כאלה ביותר מ-20 מרפאות, והפחיתו את זמן קביעת התורים ב-50% בממוצע.
חיסכון מהיישום: לקוחות מפחיתים את עומס מוקד הטלפוני ב-40% ומשיגים החזר השקעה תוך 6–8 חודשים. בהשוואה לפתרון PHP מותאם אישית, מהירות היישום על 1C-Bitrix מהירה פי 2–3, ועלויות התחזוקה נמוכות ב-30%. רכיבים מוכנים וה-API של הפלטפורמה מזרזים את הפיתוח: הרשאות מובנות, התראות, חשבון אישי והגנה מפני התקפות.
יישום טכני של הזמנה מקוונת
מקור לוח הזמנים הוא נקודה מרכזית בתכנון. נבחן שתי אפשרויות.
אפשרות A: לוח זמנים ב-Bitrix. מנהל המרפאה מנהל את לוח הזמנים של הרופא דרך ממשק Bitrix. התורים נשמרים ב-Bitrix וניתן להעבירם ל-MIS (או לא — למרפאות ללא MIS). מתאים למרפאות קטנות ללא MIS מורכב.
אפשרות B: לוח זמנים מה-MIS. Bitrix מסנכרן את לוח הזמנים מה-MIS כל N דקות. התורים נוצרים דרך ה-API של ה-MIS. האתר הוא רק הממשק; נתוני המקור נמצאים ב-MIS.
נמשיך לתאר את אפשרות A — לוח זמנים עצמאי ב-Bitrix.
מבנה הטבלה
-- Шаблон рабочего времени врача
CREATE TABLE local_doctor_schedule_template (
ID INT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL, -- ID элемента инфоблока «Врачи»
DAY_OF_WEEK TINYINT NOT NULL, -- 1=Пн, 7=Вс
TIME_FROM TIME NOT NULL,
TIME_TO TIME NOT NULL,
SLOT_DURATION INT DEFAULT 30, -- минут на приём
ACTIVE CHAR(1) DEFAULT 'Y'
);
-- Конкретные слоты (генерируются из шаблона)
CREATE TABLE local_doctor_slots (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL,
SLOT_DATE DATE NOT NULL,
SLOT_TIME TIME NOT NULL,
STATUS ENUM('free','reserved','booked','blocked') DEFAULT 'free',
APPOINTMENT_ID BIGINT,
INDEX idx_doctor_date (DOCTOR_ID, SLOT_DATE, STATUS)
);
-- Записи пациентов
CREATE TABLE local_appointments (
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
DOCTOR_ID INT NOT NULL,
SLOT_ID BIGINT NOT NULL,
USER_ID INT, -- NULL для незарегистрированных
PATIENT_NAME VARCHAR(200),
PATIENT_PHONE VARCHAR(20),
PATIENT_EMAIL VARCHAR(200),
SERVICE_ID INT, -- Услуга (инфоблок услуг)
COMMENT TEXT,
STATUS ENUM('pending','confirmed','cancelled','completed') DEFAULT 'pending',
CREATED_AT DATETIME,
CONFIRMED_AT DATETIME,
CANCELLED_AT DATETIME
); יצירת משבצות מתבנית
סוכן (Agent) הפועל מדי יום יוצר משבצות ל-30 הימים הבאים:
function GenerateDoctorSlots(): string {
$targetDate = (new \DateTime())->modify('+30 days');
$today = new \DateTime();
$templates = LocalDoctorScheduleTemplateTable::getList([
'filter' => ['ACTIVE' => 'Y'],
'select' => ['DOCTOR_ID', 'DAY_OF_WEEK', 'TIME_FROM', 'TIME_TO', 'SLOT_DURATION'],
]);
while ($tpl = $templates->fetch()) {
$date = clone $today;
while ($date <= $targetDate) {
if ((int)$date->format('N') === (int)$tpl['DAY_OF_WEEK']) {
generateSlotsForDay($tpl, $date);
}
$date->modify('+1 day');
}
}
return __FUNCTION__ . '();';
}
function generateSlotsForDay(array $tpl, \DateTime $date): void {
$from = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_FROM']);
$to = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_TO']);
$interval = new \DateInterval('PT' . $tpl['SLOT_DURATION'] . 'M');
$current = clone $from;
while ($current < $to) {
// Не создаём дубли
$exists = LocalDoctorSlotsTable::getCount([
'DOCTOR_ID' => $tpl['DOCTOR_ID'],
'SLOT_DATE' => $date->format('Y-m-d'),
'SLOT_TIME' => $current->format('H:i:s'),
]);
if (!$exists) {
LocalDoctorSlotsTable::add([
'DOCTOR_ID' => $tpl['DOCTOR_ID'],
'SLOT_DATE' => $date->format('Y-m-d'),
'SLOT_TIME' => $current->format('H:i:s'),
'STATUS' => 'free',
]);
}
$current->add($interval);
}
} רכיב ההזמנה
הרכיב -- Шаблон рабочего времени врача CREATE TABLE local_doctor_schedule_template ( ID INT AUTO_INCREMENT PRIMARY KEY, DOCTOR_ID INT NOT NULL, -- ID элемента инфоблока «Врачи» DAY_OF_WEEK TINYINT NOT NULL, -- 1=Пн, 7=Вс TIME_FROM TIME NOT NULL, TIME_TO TIME NOT NULL, SLOT_DURATION INT DEFAULT 30, -- минут на приём ACTIVE CHAR(1) DEFAULT 'Y' ); -- Конкретные слоты (генерируются из шаблона) CREATE TABLE local_doctor_slots ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, DOCTOR_ID INT NOT NULL, SLOT_DATE DATE NOT NULL, SLOT_TIME TIME NOT NULL, STATUS ENUM('free','reserved','booked','blocked') DEFAULT 'free', APPOINTMENT_ID BIGINT, INDEX idx_doctor_date (DOCTOR_ID, SLOT_DATE, STATUS) ); -- Записи пациентов CREATE TABLE local_appointments ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, DOCTOR_ID INT NOT NULL, SLOT_ID BIGINT NOT NULL, USER_ID INT, -- NULL для незарегистрированных PATIENT_NAME VARCHAR(200), PATIENT_PHONE VARCHAR(20), PATIENT_EMAIL VARCHAR(200), SERVICE_ID INT, -- Услуга (инфоблок услуг) COMMENT TEXT, STATUS ENUM('pending','confirmed','cancelled','completed') DEFAULT 'pending', CREATED_AT DATETIME, CONFIRMED_AT DATETIME, CANCELLED_AT DATETIME ); בנוי על Component 2.0 עם השלבים הבאים:
שלב 1 — בחירת רופא/התמחות. סינון לפי התמחות מבלוק המידע של הרופאים. AJAX מעדכן את רשימת הרופאים.
שלב 2 — בחירת תאריך ושעה. לוח שנה עם תאריכים זמינים מודגשים. כאשר נבחר תאריך, בקשת AJAX מביאה את המשבצות הזמינות:
// AJAX-обработчик /local/ajax/get-slots.php
$doctorId = (int)$_POST['doctor_id'];
$date = $_POST['date']; // Y-m-d
$slots = LocalDoctorSlotsTable::getList([
'filter' => [
'DOCTOR_ID' => $doctorId,
'SLOT_DATE' => $date,
'STATUS' => 'free',
],
'order' => ['SLOT_TIME' => 'ASC'],
'select' => ['ID', 'SLOT_TIME'],
])->fetchAll();
header('Content-Type: application/json');
echo json_encode(['slots' => $slots]);
שלב 3 — טופס מטופל. שם, טלפון, אימייל, הערה. למשתמשים מחוברים, הנתונים נטענים מהפרופיל. אימות מספר טלפון.
שלב 4 — אישור והזמנה.
public function bookSlot(int $slotId, array $patientData, int $serviceId = 0): int {
$connection = \Bitrix\Main\Application::getConnection();
$connection->startTransaction();
try {
// Атомарное резервирование слота
$connection->queryExecute(" UPDATE local_doctor_slots SET STATUS = 'reserved' WHERE ID = ? AND STATUS = 'free' ", [$slotId]);
if ($connection->getAffectedRowsCount() === 0) {
throw new \RuntimeException('Этот слот уже занят');
}
$appointmentId = LocalAppointmentsTable::add([
'DOCTOR_ID' => $this->getSlotDoctorId($slotId),
'SLOT_ID' => $slotId,
'USER_ID' => $patientData['user_id'] ?? null,
'PATIENT_NAME' => $patientData['name'],
'PATIENT_PHONE' => $patientData['phone'],
'PATIENT_EMAIL' => $patientData['email'],
'SERVICE_ID' => $serviceId,
'COMMENT' => $patientData['comment'] ?? '',
'STATUS' => 'confirmed',
])->getId();
// Обновляем слот — статус и привязка к записи
LocalDoctorSlotsTable::update($slotId, [
'STATUS' => 'booked',
'APPOINTMENT_ID' => $appointmentId,
]);
$connection->commitTransaction();
// Уведомления вне транзакции
$this->sendConfirmationSms($patientData['phone'], $appointmentId);
$this->sendConfirmationEmail($patientData['email'], $appointmentId);
return $appointmentId;
} catch (\Exception $e) {
$connection->rollbackTransaction();
throw $e;
}
}הטרנזקציה עם function GenerateDoctorSlots(): string { $targetDate = (new \DateTime())->modify('+30 days'); $today = new \DateTime(); $templates = LocalDoctorScheduleTemplateTable::getList([ 'filter' => ['ACTIVE' => 'Y'], 'select' => ['DOCTOR_ID', 'DAY_OF_WEEK', 'TIME_FROM', 'TIME_TO', 'SLOT_DURATION'], ]); while ($tpl = $templates->fetch()) { $date = clone $today; while ($date <= $targetDate) { if ((int)$date->format('N') === (int)$tpl['DAY_OF_WEEK']) { generateSlotsForDay($tpl, $date); } $date->modify('+1 day'); } } return __FUNCTION__ . '();'; } function generateSlotsForDay(array $tpl, \DateTime $date): void { $from = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_FROM']); $to = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_TO']); $interval = new \DateInterval('PT' . $tpl['SLOT_DURATION'] . 'M'); $current = clone $from; while ($current < $to) { // Не создаём дубли $exists = LocalDoctorSlotsTable::getCount([ 'DOCTOR_ID' => $tpl['DOCTOR_ID'], 'SLOT_DATE' => $date->format('Y-m-d'), 'SLOT_TIME' => $current->format('H:i:s'), ]); if (!$exists) { LocalDoctorSlotsTable::add([ 'DOCTOR_ID' => $tpl['DOCTOR_ID'], 'SLOT_DATE' => $date->format('Y-m-d'), 'SLOT_TIME' => $current->format('H:i:s'), 'STATUS' => 'free', ]); } $current->add($interval); } } ובדיקת /local/components/local/appointment.booking/ מגנה מפני מצבי מרוץ (race conditions) כאשר שני משתמשים מזמינים את אותה משבצת בו-זמנית. עדכון אטומי (Atomic UPDATE) נועל את השורה ברמת מסד הנתונים. במקרה של התנגשות, הבקשה השנייה זורקת חריגה. זהו תבנית סטנדרטית לטיפול בהזמנות במקביל.
כיצד אנו פותרים את בעיית ההזמנה הכפולה
עדכון אטומי עם בדיקת affectedRows בתוך טרנזקציה. אם שני משתמשים שולחים בקשות בו-זמנית, רק אחד מצליח לעדכן את המשבצת. השני מקבל שגיאה 'המשבצת הזו כבר תפוסה'. בנוסף, אנו נועלים את השורה במסד הנתונים למשך הטרנזקציה — זה מבטיח עקביות.
כיצד לבטל תור מהחשבון האישי
המטופל יכול לבטל תור לא יאוחר מ-N שעות לפני התור:
public function cancelAppointment(int $appointmentId, int $userId): void {
$appointment = LocalAppointmentsTable::getById($appointmentId)->fetch();
if (!$appointment || (int)$appointment['USER_ID'] !== $userId) {
throw new \RuntimeException('Запись не найдена');
}
$slot = LocalDoctorSlotsTable::getById($appointment['SLOT_ID'])->fetch();
$slotDateTime = new \DateTime($slot['SLOT_DATE'] . ' ' . $slot['SLOT_TIME']);
if ($slotDateTime <= (new \DateTime())->modify('+2 hours')) {
throw new \RuntimeException('Отмена записи возможна не позднее чем за 2 часа');
}
LocalAppointmentsTable::update($appointmentId, ['STATUS' => 'cancelled']);
LocalDoctorSlotsTable::update($appointment['SLOT_ID'], ['STATUS' => 'free', 'APPOINTMENT_ID' => null]);
} השוואת אפשרויות אינטגרציה
| מאפיין | לוח זמנים ב-Bitrix | לוח זמנים מה-MIS |
|---|---|---|
| מורכבות היישום | נמוכה — 3–5 שבועות | גבוהה — 6–10 שבועות |
| ניהול נתונים | דרך ניהול Bitrix | דרך MIS, Bitrix רק ממשק |
| עצמאות | מלאה | תלויה ב-MIS |
| מתאים ל | מרפאות ללא MIS או עם MIS פשוט | מרפאות עם MIS קיים |
| עלויות פיתוח | נמוכות ב-50% | גבוהות יותר עקב אינטגרציה |
מה כלול בעבודה
- עיצוב מבנה הטבלה והסוכנים
- פיתוח והתקנת רכיב ההזמנה (בחירת רופא → תאריך → משבצת → טופס → אישור)
- הגדרת סוכן יצירת המשבצות
- מטפלי AJAX לעדכון משבצות דינמי
- יישום הגנה מפני מצבי מרוץ
- אינטגרציה של התראות SMS/אימייל ותזכורות
- חשבון אישי למטופל עם היסטוריה וביטול
- תיעוד API וניהול
- הדרכת מנהלים (2–4 שעות)
- תמיכה טכנית ל-30 יום לאחר ההשקה
תהליך היישום ולוח הזמנים
- ניתוח — לימוד לוח הזמנים הנוכחי, עומס הרופאים, MIS (אם קיים). הערכת היקף: בממוצע, 30 רופאים יוצרים 600 משבצות ביום.
- עיצוב — הסכמה על מבנה הנתונים, לוגיקת המשבצות. יצירת אב טיפוס לממשק.
- יישום — כתיבת קוד, הגדרת רכיבים. הפעלת סוכן יצירת המשבצות ל-30 יום קדימה.
- בדיקות — בדיקה על תרחישים אמיתיים: הזמנה כפולה, שינוי מועד, ביטול. ביצוע בדיקות עומס עד 1000 בקשות במקביל.
- פריסה — פריסה לסביבת הייצור, הגדרת הרשאות גישה. ניטור לוגים ב-24 השעות הראשונות.
- הדרכה — הצגת הממשק למנהלים ונהלי גיבוי. מתן תיעוד.
לוח זמנים: 3–5 שבועות למערכת עצמאית ללא MIS. 6–10 שבועות עם אינטגרציה ל-MIS.
צרו קשר לקבלת הערכת לוח זמנים מדויקת לפרויקט שלכם. קבלו ייעוץ ממהנדס 1C-Bitrix מוסמך.

