פיתוח מערכת הזמנת ייעוץ בווידאו
טופס הזמנה פשוט אינו מתחשב בזמינות האמיתית של המומחה. כפילויות בהזמנות, בלבול באזורי זמן ושכחה של לקוחות הם בעיות טיפוסיות. לפי הסטטיסטיקה, עד 30% מהפגישות מתפספסות בגלל היעדר תזכורות, ובתנאי עומס שיא (1000+ בקשות לדקה) הסתברות הקונפליקט עולה ל-15%. לדוגמה, עבור רשת מרכזים רפואיים אחת בנינו מערכת המטפלת בעד 500 בקשות בשנייה עם אפס קונפליקטים. אנו פותרים בעיות אלו ברמת הארכיטקטורה: נעילת פסימיות, המרת אזורי זמן אוטומטית ותזכורות רב-שכבתיות. מאמר זה מכסה כיצד לבנות מערכת הזמנת ייעוץ בווידאו אמינה המסונכרנת עם Google Calendar, עם התראות והגנה מפני קונפליקטים.
בעיות שאנו פותרים
הראשונה היא מרוץ בקשות: שני לקוחות בוחרים את אותו משבצת זמן בו-זמנית. ללא נעילות, שניהם מקבלים אישור. אנו משתמשים בנעילת פסימיות ברמת PostgreSQL, ומבטלים מצבי מרוץ. PostgreSQL מבטיח אטומיות של טרנזקציות עם FOR UPDATE NOWAIT.
שנייה: אזורי זמן. לקוח במוסקבה, מומחה בניו יורק. המערכת ממירה את הזמן אוטומטית באמצעות timestamptz ואזור הזמן של המשתמש. ללא חישוב ידני.
שלישית: תזכורות. בלעדיהן, עד 30% מהפגישות מתפספסות. אנו מגדירים התראות דוא"ל ו-push 24 שעות ושעה לפני הייעוץ.
רביעית: ביטול או שינוי מועד. ללא מנגנונים מובנים, סנכרון יומן והחזרים הופכים לבעייתיים. ה-API שלנו תומך במחזור החיים המלא של ההזמנה.
כיצד למנוע כפילויות בהזמנות?
בליבה נמצאת טרנזקציה עם נעילת שורות FOR UPDATE NOWAIT. בעת ההזמנה, אנו בודקים חפיפת מרווחים בטבלת bookings. אם המשבצת תפוסה, אנו מחזירים 409 Conflict.
app.post('/api/bookings', authenticate, async (req, res) => {
const { specialistId, startsAt, durationMinutes } = req.body;
const endsAt = addMinutes(new Date(startsAt), durationMinutes);
try {
const booking = await db.transaction(async (trx) => {
const conflict = await trx.query(
`SELECT id FROM bookings WHERE specialist_id = $1 AND status = 'confirmed' AND tstzrange(starts_at, ends_at) && tstzrange($2::timestamptz, $3::timestamptz) FOR UPDATE NOWAIT`,
[specialistId, startsAt, endsAt.toISOString()]
);
if (conflict.rows.length > 0) {
throw Object.assign(new Error('Slot taken'), { code: 'CONFLICT' });
}
const [booking] = await trx.query(
`INSERT INTO bookings (specialist_id, client_id, starts_at, ends_at) VALUES ($1, $2, $3, $4) RETURNING *`,
[specialistId, req.user.id, startsAt, endsAt.toISOString()]
);
return booking;
});
await syncToGoogleCalendar(booking);
await sendBookingConfirmation(booking, req.user);
await notifySpecialist(booking, req.user);
await scheduleReminders(booking);
res.json(booking);
} catch (err: any) {
if (err.code === 'CONFLICT') {
return res.status(409).json({ error: 'Slot is no longer available' });
}
throw err;
}
});
כיצד לסנכרן את לוח הזמנים עם Google Calendar?
אנו משתמשים ב-OAuth2 ובלקוח הרשמי של app.post('/api/bookings', authenticate, async (req, res) => { const { specialistId, startsAt, durationMinutes } = req.body; const endsAt = addMinutes(new Date(startsAt), durationMinutes); try { const booking = await db.transaction(async (trx) => { const conflict = await trx.query( `SELECT id FROM bookings WHERE specialist_id = $1 AND status = 'confirmed' AND tstzrange(starts_at, ends_at) && tstzrange($2::timestamptz, $3::timestamptz) FOR UPDATE NOWAIT`, [specialistId, startsAt, endsAt.toISOString()] ); if (conflict.rows.length > 0) { throw Object.assign(new Error('Slot taken'), { code: 'CONFLICT' }); } const [booking] = await trx.query( `INSERT INTO bookings (specialist_id, client_id, starts_at, ends_at) VALUES ($1, $2, $3, $4) RETURNING *`, [specialistId, req.user.id, startsAt, endsAt.toISOString()] ); return booking; }); await syncToGoogleCalendar(booking); await sendBookingConfirmation(booking, req.user); await notifySpecialist(booking, req.user); await scheduleReminders(booking); res.json(booking); } catch (err: any) { if (err.code === 'CONFLICT') { return res.status(409).json({ error: 'Slot is no longer available' }); } throw err; } }); . לאחר יצירת הזמנה, אנו יוצרים אוטומטית אירוע ביומן של המומחה עם קישור לוועידה. בעת ביטול, האירוע נמחק. זה חוסך זמן ומבטל שגיאות.
import { google } from 'googleapis';
async function syncToGoogleCalendar(booking: Booking) {
const specialist = await db.specialists.findById(booking.specialist_id);
if (!specialist.google_calendar_token) return;
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET
);
oauth2Client.setCredentials(specialist.google_calendar_token);
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const client = await db.users.findById(booking.client_id);
const event = await calendar.events.insert({
calendarId: 'primary',
requestBody: {
summary: `Консультация с ${client.name}`,
start: { dateTime: booking.starts_at.toISOString() },
end: { dateTime: booking.ends_at.toISOString() },
attendees: [{ email: client.email }],
conferenceData: {
createRequest: {
requestId: booking.id,
},
},
},
conferenceDataVersion: 1,
});
await db.bookings.update(booking.id, { google_event_id: event.data.id });
} מבנה נתונים ואלגוריתם חיפוש משבצות
אנו מאחסנים לוחות זמנים סטנדרטיים, חריגים (חופשות, חגים) והזמנות. האלגוריתם googleapis בוחר תחילה את השעות הסטנדרטיות ליום בשבוע, בודק החרגות, ולאחר מכן מחסיר מרווחים תפוסים.
CREATE TABLE availability_schedules (
id UUID PRIMARY KEY,
specialist_id UUID REFERENCES specialists(id),
day_of_week SMALLINT NOT NULL, -- 1=Mon ... 7=Sun
start_time TIME NOT NULL,
end_time TIME NOT NULL,
is_active BOOLEAN DEFAULT true
);
CREATE TABLE availability_overrides (
id UUID PRIMARY KEY,
specialist_id UUID REFERENCES specialists(id),
date DATE NOT NULL,
type VARCHAR(50), -- 'blocked' | 'custom_hours'
start_time TIME,
end_time TIME
);
CREATE TABLE bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
specialist_id UUID REFERENCES specialists(id),
client_id UUID REFERENCES users(id),
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
status VARCHAR(50) DEFAULT 'confirmed',
cancel_reason TEXT,
google_event_id VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT now()
); async function getAvailableSlots(
specialistId: string,
date: string,
durationMinutes: number,
userTimezone: string
): Promise<Array<{ start: string; end: string }>> {
const localDate = new Date(`${date}T00:00:00`);
const dayOfWeek = getISODayOfWeek(localDate);
const schedule = await db.query<{ start_time: string; end_time: string }>(
`SELECT start_time, end_time FROM availability_schedules WHERE specialist_id = $1 AND day_of_week = $2 AND is_active = true`,
[specialistId, dayOfWeek]
);
if (!schedule.rows.length) return [];
const override = await db.query(
`SELECT * FROM availability_overrides WHERE specialist_id = $1 AND date = $2`,
[specialistId, date]
);
if (override.rows[0]?.type === 'blocked') return [];
const workStart = override.rows[0]?.start_time ?? schedule.rows[0].start_time;
const workEnd = override.rows[0]?.end_time ?? schedule.rows[0].end_time;
const booked = await db.query<{ starts_at: string; ends_at: string }>(
`SELECT starts_at, ends_at FROM bookings WHERE specialist_id = $1 AND DATE(starts_at AT TIME ZONE $3) = $2 AND status = 'confirmed'`,
[specialistId, date, userTimezone]
);
const slots: Array<{ start: string; end: string }> = [];
let current = parseTime(date, workStart, userTimezone);
const end = parseTime(date, workEnd, userTimezone);
while (current < end) {
const slotEnd = addMinutes(current, durationMinutes);
if (slotEnd > end) break;
const isBusy = booked.rows.some(
b => current < new Date(b.ends_at) && slotEnd > new Date(b.starts_at)
);
if (!isBusy) {
slots.push({
start: current.toISOString(),
end: slotEnd.toISOString(),
});
}
current = addMinutes(current, durationMinutes);
}
return slots;
} רכיב בחירת זמן ב-React
המשתמש בוחר תאריך, והמערכת טוענת משבצות זמינות. הממשק רספונסיבי ועובד במובייל.
function BookingCalendar({ specialistId, durationMinutes }) {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [slots, setSlots] = useState<Slot[]>([]);
const [selectedSlot, setSelectedSlot] = useState<Slot | null>(null);
useEffect(() => {
if (!selectedDate) return;
fetch(`/api/specialists/${specialistId}/slots?date=${formatDate(selectedDate)}&duration=${durationMinutes}`)
.then(r => r.json())
.then(setSlots);
}, [selectedDate]);
return (
<div className="grid grid-cols-2 gap-8">
<CalendarPicker
value={selectedDate}
onChange={setSelectedDate}
minDate={new Date()}
maxDate={addDays(new Date(), 60)}
disabledDates={/* выходные и блокированные дни */}
/>
{selectedDate && (
<div>
<p className="font-semibold mb-3">{formatDate(selectedDate, 'd MMMM')}</p>
{slots.length === 0 ? (
<p className="text-gray-500">Нет доступных слотов</p>
) : (
<div className="grid grid-cols-3 gap-2">
{slots.map(slot => (
<button
key={slot.start}
onClick={() => setSelectedSlot(slot)}
className={`py-2 text-sm rounded-lg border transition ${
selectedSlot?.start === slot.start
? 'border-blue-600 bg-blue-50 text-blue-700'
: 'border-gray-200 hover:border-blue-400'
}`}
>
{formatTime(slot.start)}
</button>
))}
</div>
)}
{selectedSlot && (
<button onClick={confirmBooking} className="mt-4 btn-primary w-full">
Записаться на {formatTime(selectedSlot.start)}
</button>
)}
</div>
)}
</div>
);
} השוואה: פתרון מותאם אישית מול שירותים מוכנים
| קריטריון | הפתרון שלנו | Calendly / YouCanBookMe |
|---|---|---|
| גמישות התאמה אישית | התאמה מלאה לממשק האתר | מוגבל על ידי תבניות |
| אינטגרציה עם 1C/CRM | דרך API | לא |
| עלות לצוות | הערכה אישית | מנוי חודשי (למשתמש) |
| שליטה בנתונים | אחסון על שרתים משלנו | נתונים בצד השירות |
| קוד מקור | גישה מלאה | סגור |
פתרון מותאם אישית מספק עד פי 2 שיפור ביצועים בעומס שיא בשל היעדר תקורה מ-API חיצוניים.
מתי להזמין הזמנה מותאמת אישית?
שירותים מוכנים מתאימים להשקה מהירה, אך אם אתם צריכים ממשק ייחודי, אינטגרציה עם 1C או שליטה מלאה בנתונים, פיתוח מותאם אישית עדיף. זה משתלם על ידי ביטול דמי מנוי חודשיים ומאפשר למערכת לגדול עם העסק. קבלו ייעוץ מהמהנדס שלנו — הוא יעזור לבחור את הדרך האופטימלית.
תהליך ולוחות זמנים
| שלב | משך | תוצאה |
|---|---|---|
| אנליזה | 1-2 ימים | סכמת נתונים, בחירת טכנולוגיות, אב טיפוס אינטגרציה |
| פיתוח API | 3-5 ימים | נקודות קצה REST להזמנות, משבצות, יומן |
| פרונטאנד | 3-5 ימים | יומן, טופס הזמנה, דף ייעוץ |
| אינטגרציית יומן | 1-2 ימים | Google Calendar, Outlook, סנכרון |
| בדיקות | 1-2 ימים | עומס (1000+ בקשות), יחידה, UI |
| פריסה | יום אחד | Docker, CI/CD, הגדרת סביבה |
לוח זמנים משוער: 1.5 עד 3 שבועות תלוי במורכבות האינטגרציה. צרו קשר להערכת פרויקט מדויקת.
הגדרת סנכרון יומנים מרובים
בנוסף ל-Google Calendar, אנו תומכים ב-Outlook Calendar ובכל שירות תואם CalDAV. כל יומן מקבל סשן OAuth משלו. בקוד למעלה, פשוט החליפו את הספק ועדכנו את הטוקן. ניתן להוסיף מתאם מותאם אישית במידת הצורך.מה כלול בתוצאה
- קוד מקור ב-TypeScript ו-React
- REST API עם תיעוד (Swagger)
- הוראות פריסה ותחזוקה
- אחריות ל-30 ימים ללא תקלות
- אפשרויות התאמה אישית לתהליכים עסקיים
הזמינו פיתוח מערכת הזמנות עם אחריות לאיכות. צרו קשר להערכת פרויקט — נחשב תקציב ולוחות זמנים. ניסיון של למעלה מ-7 שנים ו-15+ מערכות הזמנה מיושמות מבטיחים פתרון אמין.







