bạn phải dựa vào các nguồn liên quan đến hệ thống đặt lịch thôi, và ta chỉ nghiên cứu các domain đặc trưng của hệ thống đặt lịch, những kinh nghiệm khi làm dạng hệ thống này, chứ mấy cái cơ bản hệ thống nào cũng có thì nói làm gì. và viết dạng lách kinh nghiệm ấy. viết kỹ vào

Báo cáo kinh nghiệm thiết kế hệ thống đặt lịch

Ngày viết lại: 30/05/2026 Phạm vi: hệ thống đặt lịch dịch vụ tại nhà kiểu EcoClean Trọng tâm: domain đặc trưng của scheduling/booking, không bàn lan sang bảo mật/API/payment chung nếu không liên quan trực tiếp tới đặt lịch.

1. Cách nghiên cứu đúng phạm vi

Bản này chỉ dựa vào các nguồn có liên quan trực tiếp tới booking/scheduling:

Nhóm nguồnVì sao liên quan
Microsoft Bookings / Microsoft Graph BookingsMột mô hình đặt lịch dịch vụ chuẩn: business, service, staff, appointment, customer, scheduling policy
Cal.com / Calendly / Nylas SchedulerCác sản phẩm scheduling hiện đại: event type, availability, buffer, minimum notice, round robin, collective booking
Google Calendar FreeBusy APICách tư duy availability theo khoảng busy/free trong một time range
RFC 5545 iCalendar và RFC 7953 Calendar AvailabilityChuẩn lịch, recurrence, exception, free/busy, availability override
Dynamics 365 Field Service / Salesforce Field Service / Timefold Field Service RoutingĐặt lịch dịch vụ tại hiện trường: resource, skill, territory, travel time, route, crew, capacity, time off

Điểm chung của các nguồn này: hệ thống đặt lịch không bắt đầu từ "tạo đơn hàng". Nó bắt đầu từ câu hỏi:

> Ai có thể làm việc gì, ở đâu, trong khoảng thời gian nào, với điều kiện nào, và nếu có nhiều phương án thì chọn phương án nào?

Với EcoClean, đây mới là câu hỏi gốc. Order chỉ là kết quả sau khi khách đã chọn một phương án đặt lịch.

2. Bài học lớn nhất

Sau khi nhìn các hệ thống scheduling đúng nghĩa, tôi rút ra một điều: hệ thống đặt lịch có ba lớp khác nhau, nếu trộn vào nhau thì càng sửa càng rối.

LớpCâu hỏi chínhVí dụ EcoClean
Service designKhách đang đặt loại dịch vụ gì?Giúp việc theo giờ, gói tiêu chuẩn, 90m2, thêm thú cưng
AvailabilityCó slot nào thật sự phục vụ được?09:00-12:00, cần 1 cleaner, có 30 phút di chuyển
Fulfillment/operationSau khi đặt thì ai làm, trạng thái gì, đổi lịch/hủy ra sao?Admin gán nhân viên, nhân viên bắt đầu làm, khách đánh giá

Sai lầm phổ biến là tạo thẳng Order ngay khi khách chọn gói. Cách đó nhanh lúc đầu, nhưng sau này mọi thứ bị nhồi vào order: giờ làm, staff, địa chỉ, coupon, option, trạng thái, chat, payment, hủy, đổi lịch, gói tháng. Order phình to và khó kiểm soát.

Kinh nghiệm đúng hơn:

Service/package -> Availability/quote -> Booking confirmation -> Appointment/order -> Fulfillment

3. Domain 1: Service/Event Type

Các nền tảng như Cal.com gọi thứ khách có thể đặt là Event Type: nó có duration, location, availability và booking rules.[^cal-event-types] Microsoft Bookings cũng tách bookingService khỏi bookingAppointment; business có nhiều service, staff được gán làm các service đó.[^ms-bookings-api]

Kinh nghiệm:

  • Đừng xem dịch vụ chỉ là một cái tên và một giá.
  • Dịch vụ phải mang theo thời lượng, rule đặt lịch, option, location, staff phù hợp, buffer, câu hỏi cần hỏi khách.
  • Mỗi dịch vụ có thể override rule chung.

Với EcoClean:

Khái niệm schedulingEcoClean nên map thành
Event type / booking serviceDịch vụ: giúp việc theo giờ, giúp việc theo tháng, giặt ủi, dọn phòng
DurationThời gian làm gói, ví dụ 2h, 3h, 4h
LocationĐịa chỉ khách, không phải online/phone
Booking rulesĐặt trước 1h30, tối đa 7 ngày, giờ làm 07:00-19:00
Invitee questionsHọ tên, SĐT, địa chỉ, ghi chú, có thú cưng không
Service-level overridesGói tháng, cleaner yêu thích, dịch vụ cần 2 người

Kinh nghiệm "xương máu": option của dịch vụ không chỉ thay đổi giá, nó thường thay đổi duration và availability.

Ví dụ:

OptionNếu chỉ cộng tiềnNếu làm đúng scheduling
Vệ sinh tủ lạnh+30k+30k và +25 phút
Ủi đồ+40k+40k và +35 phút
Bạn uống tui dọn+80k+80k và +50 phút
Cleaner yêu thích+30k+30k và đổi minimum notice từ 90 lên 180 phút
Thú cưng+10kCó thể yêu cầu cleaner chấp nhận nhà có thú cưng

Do đó trong model, option nên có:

price_delta

duration_delta_minutes

required_skill_tags

availability_rule_delta

requires_customer_note

4. Domain 2: Scheduling Policy

Microsoft Bookings tách scheduling policy thành những thứ rất đặc trưng: time increments, lead time, maximum days in advance, notification khi booking thay đổi, và cho phép override theo service.[^ms-scheduling-policy]

Đây là domain EcoClean đang cần chuẩn hóa nhất.

4.1. Time increments

Time increment là nhịp mà hệ thống cho phép bắt đầu lịch: 5 phút, 15 phút, 30 phút, 1 giờ. Microsoft Bookings mô tả rõ: appointment 60 phút có thể được đưa ra ở 8:00, 8:15, 8:30 nếu increment là 15 phút.[^ms-scheduling-policy]

Kinh nghiệm:

  • Không nên để user chọn mọi phút tùy ý.
  • Slot nên theo nhịp cố định để dễ vận hành.
  • Với giúp việc, nhịp 30 phút thường hợp lý hơn 5 phút.
  • Nếu sau này có travel time, nhịp 30 phút giúp admin và nhân viên dễ nhìn lịch hơn.

Đề xuất EcoClean:

slot_interval_minutes = 30

4.2. Minimum notice / lead time

Microsoft gọi đây là lead time: doanh nghiệp cần biết trước bao lâu để chuẩn bị staffing.[^ms-scheduling-policy] Cal.com cũng có minimumBookingNotice.[^cal-event-types]

Với EcoClean:

min_notice_minutes = 90

favorite_staff_min_notice_minutes = 180

Kinh nghiệm:

  • Rule này không chỉ là UX. Nó là rule vận hành.
  • App được phép disable slot cho dễ nhìn, nhưng server vẫn phải là nguồn đúng.
  • Minimum notice phải check bằng now + notice, không check lỏng theo ngày.
  • Nếu user chọn hôm nay nhưng không chọn giờ, hệ thống phải mặc định/ép giờ sau mốc sớm nhất, không được pass giờ quá khứ.

4.3. Maximum booking horizon

Microsoft Bookings có maximum days in advance để giới hạn khách đặt quá xa.[^ms-scheduling-policy] Đây là thứ EcoClean cũng đã có trong yêu cầu CEO: không quá 7 ngày.

Kinh nghiệm:

  • Đặt quá xa làm lịch vận hành giả tạo, vì nhân sự/gói/giá có thể đổi.
  • App cleaning mới ra mắt nên horizon 7 ngày là hợp lý.
  • Gói tháng là ngoại lệ, nhưng nó nên là một domain riêng, không phải mở horizon tự do.

4.4. Cancellation/reschedule notice

Các sản phẩm booking đều coi đổi lịch/hủy lịch là một phần scheduling policy, không phải nút phụ.

Kinh nghiệm:

  • Phải chốt khách được hủy trước bao lâu.
  • Phải chốt có phí hủy không.
  • Phải chốt khi nào hoàn điểm/coupon.
  • Với gói tháng, CEO đã nói không cho đổi ngày giờ trên app. Đây là policy rất quan trọng, nên server phải chặn, app chỉ ẩn nút.

EcoClean nên có:

min_cancel_notice_minutes

min_reschedule_notice_minutes

allow_customer_reschedule

allow_customer_cancel

monthly_plan_customer_reschedule_allowed = false

5. Domain 3: Availability không phải là giờ mở cửa

Đây là điểm dễ sai nhất. Nylas tách availability thành default open hours, participant-specific open hours, buffer times, specific date availability và unavailable dates.[^nylas-availability] RFC 7953 cũng có mô hình VAVAILABILITY với các khoảng available có thể lặp lại và có thể override theo thời gian.[^rfc7953]

Kinh nghiệm:

Availability = open hours

+ special open hours

- unavailable dates

- busy events

- buffer

- constraints

Không phải cứ business mở 07:00-19:00 là mọi slot trong đó đều đặt được.

5.1. Open hours

Open hours là khung giờ làm việc gốc.

EcoClean hiện có:

07:00 - 19:00

Nhưng về sau nên tách:

Loại open hoursVí dụ
Business open hoursEcoClean nhận lịch 07:00-19:00
Service open hoursGiặt ủi chỉ nhận 08:00-17:00
Staff open hoursCleaner A làm thứ 2-6, 08:00-16:00
Date-specific open hoursNgày lễ chỉ làm 08:00-12:00

5.2. Unavailable dates / exdates

Nylas có exdates để loại ngày cụ thể khỏi open hours.[^nylas-availability] RFC 5545 cũng có EXDATE cho recurrence exception.[^rfc5545-recurrence]

Kinh nghiệm:

  • Đừng sửa rule tuần chỉ vì nghỉ một ngày.
  • Dùng override/unavailable date cho ngày nghỉ, lễ, nhân viên xin nghỉ.
  • Gói tháng sau này rất cần exception date.

EcoClean nên có model:

StaffTimeOff(staff, start_at, end_at, reason)

ServiceBlackoutDate(service, date, reason)

BusinessHoliday(date, name, is_working, surcharge_rule)

5.3. Busy time

Google Calendar FreeBusy trả về danh sách khoảng bận busy[].startbusy[].end; end là exclusive.[^google-freebusy] RFC 5545 cũng biểu diễn free/busy bằng các khoảng thời gian.[^rfc5545-freebusy]

Kinh nghiệm:

  • Lịch phải tính bằng interval [start, end), không phải một field booking_time.
  • Một đơn 09:00-12:00 sẽ chặn cả khoảng đó.
  • Một đơn mới 11:30-14:30 bị overlap, dù giờ bắt đầu khác.
  • End exclusive giúp đơn 09:00-12:00 và đơn 12:00-15:00 không bị coi overlap, nếu không có buffer.

Pseudo logic:

candidate_start < existing_end

AND candidate_end > existing_start

=> overlap

6. Domain 4: Buffer là một phần của lịch, không phải trang trí UI

Cal.com, Calendly, Microsoft Bookings và Nylas đều có khái niệm buffer trước/sau appointment.[^cal-buffer][^calendly-buffer][^ms-service-availability][^nylas-availability]

Kinh nghiệm quan trọng:

  • Buffer phải được tính như busy time.
  • Buffer có thể dùng để chuẩn bị, dọn dẹp, nghỉ, di chuyển.
  • Buffer của booking cũ và booking mới có thể "stack", nghĩa là tổng thời gian bị chặn lớn hơn bạn tưởng.[^cal-buffer]
  • Khách không cần thấy buffer chi tiết, nhưng slot khả dụng phải đã tính buffer.[^ms-service-availability]

Với EcoClean, buffer không chỉ là nghỉ giữa hai cuộc họp. Nó gần như bắt buộc vì nhân viên phải di chuyển.

Đề xuất ban đầu:

Tình huốngBuffer đề xuất
Chưa có GPS/route30 phút sau mỗi đơn
Có cleaner yêu thích45-60 phút vì phải giữ đúng người
Gói dài hơn 4h30 phút trước và 30 phút sau
Khác phường/quậnSau này tính theo travel time

Mô hình:

service_duration = base_duration + option_duration

effective_start = start_at - pre_buffer

effective_end = start_at + service_duration + post_buffer

Khi check overlap, dùng effective_start/effective_end, không dùng mỗi thời gian làm thật.

7. Domain 5: Resource không chỉ là nhân viên

Dynamics 365 Field Service gọi mọi thứ cần được schedule là bookable resource: người, thiết bị, facility, crew, pool.[^dynamics-resource] Đây là góc nhìn rất đáng học cho EcoClean.

Với EcoClean, resource có thể là:

ResourceVí dụ
PersonCleaner
Crew2 cleaner đi cùng một đơn
EquipmentMáy hút bụi, máy khử khuẩn Nano bạc
FacilityKho/văn phòng, nếu có nhận đồ giặt
PoolNhóm cleaner cùng khu vực, admin chọn người sau

Kinh nghiệm:

  • Ban đầu chỉ cần Staff.
  • Nhưng đừng model quá chặt kiểu mỗi order chỉ có một staff.
  • Dịch vụ tại nhà có thể cần 2 người, hoặc cần thiết bị.
  • Một resource có timezone, work hours, start/end location, skill, territory, capacity.

EcoClean hiện có Order.staff ManyToMany. Về lâu dài nên có Assignment riêng:

Assignment

- order

- staff

- role

- start_at

- end_at

- status: proposed/assigned/accepted/started/completed/cancelled

- assigned_by

- accepted_at

Vì gán nhân viên không chỉ là liên kết many-to-many. Nó có lifecycle riêng.

8. Domain 6: Capacity và overbooking

Dynamics 365 cho resource có capacity, ví dụ capacity 5 thì có thể được book nhiều lần trong work hours.[^dynamics-resource] Với người đi làm dịch vụ tại nhà, capacity thường là 1, nhưng ý tưởng capacity rất quan trọng.

Kinh nghiệm:

  • Cleaner làm tại nhà khách: capacity = 1.
  • Tổng đài tư vấn: capacity có thể >1 nếu là nhóm.
  • Máy móc/thiết bị: capacity = số lượng thiết bị thật.
  • Crew: capacity không phải cộng đơn giản, vì 2 người phải cùng rảnh.

EcoClean:

LoạiCapacity nên hiểu
Cleaner1 đơn tại một thời điểm
Cleaner yêu thích1, không được auto đổi người
Gói cần 2 ngườicần 2 cleaner rảnh cùng khoảng
Máy hút bụinếu khách chọn vật tư từ EcoClean, cần số lượng thiết bị

MVP có thể chưa cần equipment capacity, nhưng nếu "vật tư không bật sẵn" và có option thiết bị, về lâu dài phải biết thiết bị có đủ không.

9. Domain 7: Skill, territory và service compatibility

Salesforce Field Service chia scheduling policy thành work rulesservice objectives: work rules lọc ứng viên có đủ điều kiện, service objectives xếp hạng ứng viên tốt nhất.[^salesforce-policy] Work rules hỏi những câu như: người này có skill không, có ở territory đúng không, có rảnh đúng giờ không.

Kinh nghiệm:

Candidate filtering trước, scoring sau.

Không nên nhảy thẳng vào "chọn nhân viên rating cao nhất". Trước hết phải loại những người không làm được.

Hard rules cho EcoClean:

RuleVí dụ
AvailabilityCleaner rảnh trong khoảng start/end + buffer
TerritoryCleaner làm khu vực/quận/phường đó
SkillCleaner làm được gói có thú cưng, ủi đồ, máy khử khuẩn
Required resourceKhách chọn cleaner yêu thích thì phải đúng cleaner
Time windowSlot nằm trong giờ phục vụ
CapacityKhông nhận quá số đơn cùng lúc

Scoring cho EcoClean:

ObjectiveKhi nào dùng
Minimize travelKhi có địa chỉ/toạ độ
Preferred cleanerKhi khách chọn cleaner yêu thích
RatingKhi muốn ưu tiên cleaner tốt
FairnessKhi muốn chia đều đơn
Avoid overtimeKhi có lịch ca
Skill fitKhi dịch vụ khó

MVP chưa cần solver tự động. Nhưng data model nên để sau này có thể lọc và chấm điểm.

10. Domain 8: Round robin, collective, selected staff

Cal.com và Calendly đều có nhiều kiểu scheduling team: round robin, collective, group.[^cal-event-types][^calendly-team]

Kinh nghiệm:

KiểuNghĩaEcoClean tương ứng
Round robinAi rảnh thì nhận, có thể chia đềuĐơn thường, admin/engine chọn cleaner
CollectiveTất cả người liên quan phải cùng rảnhGói cần 2 cleaner đi cùng
Selected staffKhách chọn một người cụ thểCleaner yêu thích
PoolGiữ chỗ cho một nhóm, chọn người sauGiai đoạn admin xác nhận thủ công

EcoClean hiện nên đi theo:

MVP: pool/manual assignment

Sau đó: selected staff cho cleaner yêu thích

Sau đó: collective cho gói 2 người

Sau đó: round robin/scoring nếu số đơn tăng

Lý do: nếu làm auto dispatch quá sớm sẽ phức tạp, trong khi giai đoạn đầu điều quan trọng là có đơn thật và admin xử lý được.

11. Domain 9: Time window, arrival window và service window

Field service không giống đặt cuộc họp đúng 09:00. Khách thường muốn "khoảng 9-10h", nhân viên có thể đến trong arrival window. Salesforce nhắc tới arrival window và Match Time Rule trong scheduling.[^salesforce-appointment]

EcoClean hiện đang để khách chọn một giờ cụ thể. Cách này dễ hiểu, nhưng vận hành ngoài đời có độ trễ.

Kinh nghiệm:

  • Với MVP, cứ để giờ cụ thể cho dễ bán.
  • Nhưng trong domain model nên phân biệt:
  • requested_start_at: giờ khách chọn.
  • arrival_window_start/end: khoảng EcoClean cam kết tới.
  • service_start_at/end: giờ làm thực tế.
  • completed_at: giờ hoàn thành thật.

Ví dụ:

Khách chọn: 09:00

Arrival window: 09:00-09:30

Service expected: 09:30-12:30

Actual started: 09:20

Actual completed: 12:10

Ban đầu chưa cần hiển thị hết, nhưng log nên chuẩn bị để sau này đo đúng KPI.

12. Domain 10: Location, travel time và route

Field service khác appointment online ở chỗ nhân viên phải di chuyển. Dynamics yêu cầu resource có start/end location để phục vụ scheduling và routing.[^dynamics-resource] Timefold Field Service Routing cũng nhấn mạnh time windows, opening hours, skills, affinity, route optimization, breaks, fairness và real-time planning.[^timefold-routing]

Kinh nghiệm:

  • Nếu chưa có map/GPS, đừng giả vờ tính travel chính xác.
  • Nhưng vẫn phải chừa domain cho travel.
  • Địa chỉ text chỉ đủ cho admin đọc, chưa đủ cho route optimization.
  • Sau này cần geocode địa chỉ thành lat/lng.

EcoClean roadmap:

Giai đoạnCách làm location
MVPLưu địa chỉ text, phường/quận/thành phố
Giai đoạn 2Chuẩn hóa địa chỉ, chọn phường/quận từ danh sách
Giai đoạn 3Geocode địa chỉ, lưu lat/lng
Giai đoạn 4Tính travel buffer theo khoảng cách
Giai đoạn 5Route optimization nếu nhiều đơn/ngày

Kinh nghiệm thực tế: nếu chưa đủ đơn, chưa cần Google Maps ngay. Nhưng nên model address_snapshotgeo tách ra, để sau này thêm không phải đập lại order.

13. Domain 11: Recurrence và gói tháng

RFC 5545 cho recurrence bằng DTSTART, RRULE, RDATE, EXDATE; recurrence set được tạo từ rule rồi loại exception.[^rfc5545-recurrence] Đây là thứ gói tháng của EcoClean sẽ đụng rất rõ.

Gói tháng không phải là một order dài 28 ngày. Nó là một subscription/schedule pattern tạo ra nhiều service appointments.

Kinh nghiệm:

  • Đừng model gói tháng bằng một order đơn có booking_date.
  • Nên model thành RecurringPlan hoặc BookingSeries.
  • Mỗi lần làm trong gói tháng nên là một ServiceVisit/Appointment.
  • Có exception date nếu nghỉ lễ, khách xin nghỉ, nhân viên nghỉ.
  • CEO nói không cho đổi ngày giờ gói tháng trên app: app ẩn nút, server chặn.

Mô hình gợi ý:

BookingSeries

- customer

- service

- package

- start_date

- end_date

- recurrence_rule

- locked_by_policy

- status

ServiceVisit

- series

- scheduled_start_at

- scheduled_end_at

- assigned_staff

- status

Ví dụ:

Gói tháng 28 ngày, mỗi tuần 1 buổi thứ 3 09:00

RRULE:FREQ=WEEKLY;COUNT=4;BYDAY=TU

EXDATE: nếu ngày đó nghỉ lễ hoặc bị admin đổi nội bộ

Không cần expose RRULE cho user. Nhưng backend nên có tư duy series/visit.

14. Domain 12: Booking questions và customer data

Microsoft Bookings có customer objects và booking appointment chứa customer info.[^ms-bookings-api][^ms-appointment] Cal.com có custom fields trong event type.[^cal-event-types]

Kinh nghiệm:

  • Câu hỏi khách phải trả lời phụ thuộc dịch vụ.
  • Không phải mọi dịch vụ đều cần cùng một form.
  • Họ tên/SĐT/địa chỉ là bắt buộc khi đặt dịch vụ tại nhà, không nhất thiết bắt buộc khi đăng ký.
  • Câu trả lời nên snapshot vào appointment/order.

EcoClean nên chia:

Loại fieldVí dụ
Profile fieldTên, email, avatar
Contact field cho bookingHọ tên người nhận, SĐT liên hệ
Address fieldĐịa chỉ phục vụ
Service-specific questionCó thú cưng không, có thang máy không, cần mang vật tư không
Internal noteAdmin/staff ghi chú sau

Kinh nghiệm: đừng biến profile thành nơi chứa tất cả dữ liệu đặt lịch. Profile là dữ liệu hiện tại của user; appointment cần snapshot dữ liệu tại thời điểm đặt.

15. Domain 13: Booking confirmation và trạng thái giữ chỗ

Các scheduling system thường phân biệt chọn slot, xác nhận, cần approval, confirmed, cancelled, rescheduled. Cal.com có confirmation/approval trong event type.[^cal-event-types]

EcoClean cần đặc biệt cẩn thận vì có thể chưa có availability engine hoàn chỉnh.

Kinh nghiệm:

Trạng tháiNghĩa
draftUser đang chọn
quotedServer đã báo giá
requestedKhách gửi yêu cầu đặt
pending_confirmationChờ EcoClean xác nhận/gán nhân viên
confirmedEcoClean đã xác nhận slot
assignedĐã có nhân viên
in_progressĐang làm
completedHoàn thành
cancelledHủy
no_showKhách/nhân viên không xuất hiện

Hiện EcoClean dùng pending_payment, processing, in_progress, completed, cancelled. Với dịch vụ, nên dần thay ngôn ngữ để rõ hơn:

pending_confirmation

confirmed

assigned

on_the_way

in_progress

completed

cancelled

Nếu chưa thể đổi DB ngay, app vẫn có thể map status display cho đúng ngôn ngữ dịch vụ.

16. Domain 14: Reschedule, cancel, no-show

Microsoft Bookings coi acceptable lead and cancellation times là policy cốt lõi.[^ms-scheduling-policy] Calendly Workflows cũng nhấn mạnh reminder/no-show follow-up để giảm việc khách quên lịch.[^calendly-workflows]

Kinh nghiệm:

  • Hủy/đổi lịch là domain booking, không phải chỉ update order date/time.
  • Cần biết ai yêu cầu đổi: customer, admin, staff.
  • Cần biết lý do.
  • Cần biết đổi từ slot nào sang slot nào.
  • Cần biết tác động tới điểm/coupon/thanh toán.

EcoClean nên có log:

BookingChangeLog

- appointment/order

- change_type: reschedule/cancel/no_show/assign

- old_start_at

- new_start_at

- requested_by

- reason

- created_at

Chính sách MVP:

  • Khách được hủy khi đơn chưa confirmed hoặc còn cách giờ làm đủ X phút.
  • Khách chưa tự đổi lịch gói tháng.
  • Admin được đổi lịch nhưng phải tạo log.
  • No-show nên có trạng thái riêng nếu sau này có phạt/hạn chế.

17. Domain 15: Notification và reminder của booking

Hệ thống đặt lịch luôn cần reminder. Microsoft Bookings có automatic notifications khi booking thay đổi.[^ms-scheduling-policy] Calendly có workflows gửi email/SMS trước/sau meeting để giảm no-show.[^calendly-workflows] Microsoft Graph bookingAppointment cũng có reminders và SMS notification flag.[^ms-appointment]

Kinh nghiệm:

  • Notification trong booking không phải feature phụ, nó là một phần chống no-show.
  • Cần thông báo khi tạo lịch, đổi lịch, hủy lịch, nhắc trước giờ làm, nhân viên bắt đầu đi, nhân viên tới nơi, hoàn thành.
  • In-app notification là bản ghi server-side.
  • Push/SMS/email chỉ là kênh gửi.

EcoClean event nên phát notification:

EventNgười nhận
Booking requestedAdmin
Booking confirmedCustomer
Staff assignedCustomer, staff
Reminder trước giờ làmCustomer, staff
Staff on the wayCustomer
Staff startedCustomer/admin
CompletedCustomer/admin
Cancelled/rescheduledCác bên liên quan
Chat messageNgười chưa đọc

MVP nên làm in-app trước, push nền sau.

18. Domain 16: Schedule board cho admin

Field service systems có schedule board vì vận hành không thể nhìn bằng list đơn mãi. Dynamics có schedule board và schedule assistant; Salesforce có Gantt/dispatcher console.[^dynamics-resource][^salesforce-policy]

Kinh nghiệm:

  • List đơn phù hợp với khách.
  • Admin cần lịch theo ngày, theo staff, theo trạng thái.
  • Nhân viên cần lịch hôm nay của mình.
  • Một hệ thống đặt lịch mà admin chỉ có list đơn sẽ rất nhanh rối khi có nhiều đơn.

EcoClean nên có 3 view khác nhau:

ViewNgười dùngMục tiêu
My ordersCustomerXem đơn đã đặt
My scheduleStaffXem việc được giao
Schedule boardAdminGán nhân viên, nhìn slot, xử lý xung đột

Schedule board không nhất thiết phải đẹp ngay. MVP có thể là:

Ngày

07:00 Staff A: trống

07:00 Staff B: đơn SY-...

07:30 Staff A: đơn SY-...

Nhưng tách domain ngay từ đầu sẽ tránh chuyện admin thấy lẫn đơn user.

19. Domain 17: Pricing trong booking khác pricing thương mại điện tử

Microsoft Graph bookingAppointment có price, priceType, duration, preBuffer, postBuffer, staffMemberIds.[^ms-appointment] Điều này cho thấy giá dịch vụ gắn với duration, staff và buffer chứ không chỉ là product price.

Kinh nghiệm:

  • Giá phải đi cùng thời gian.
  • Option tăng thời gian có thể tăng chi phí nhân công.
  • Cleaner yêu thích có thể là phụ phí.
  • 2 nhân viên có thể không phải chỉ x2 giá, vì thời gian rút ngắn hoặc gói khác.
  • Giờ cao điểm/ngày lễ có thể là phụ phí scheduling.

EcoClean pricing nên lưu breakdown:

base_service_price

package_price

option_price_delta

duration_delta

staff_count

surcharge_by_time

surcharge_by_area_or_location

coupon_discount

points_discount

final_total

Kinh nghiệm quan trọng: quote phải snapshot cả giá và duration.

Nếu hôm nay gói 90m2 là 3h giá 300k, tháng sau đổi thành 350k, đơn cũ vẫn phải hiển thị 300k.

20. Domain 18: Slot generation

Slot generation là việc biến rule thành danh sách giờ user có thể chọn.

Từ các nguồn như Microsoft Bookings, Nylas, Google FreeBusy, có thể rút ra công thức:

Input:

- service duration

- slot interval

- open hours

- participant/staff availability

- busy intervals

- buffer

- min notice

- max future

- service-specific rules

Output:

- list of candidate start times

Pseudo:

for each day in booking horizon:

windows = open_hours(day)

windows = apply_special_availability(windows)

windows = subtract_unavailable_dates(windows)

windows = subtract_busy_intervals(windows)

windows = subtract_buffers(windows)

for each start in windows stepped by slot_interval:

end = start + service_duration

if [start, end) fits:

emit slot

Kinh nghiệm:

  • Slot generation nên chạy ở server khi cần chính xác.
  • App có thể generate nháp nếu config đơn giản.
  • Khi có staff/favorite/equipment, server nên trả available slots.
  • Không nên để app tự đoán toàn bộ slot khi hệ thống bắt đầu có nhân viên thật.

EcoClean hiện có thể tạm để app chọn ngày/giờ rồi server validate. Nhưng bước tiếp theo nên là:

GET /booking/available-slots?service_id=&package_id=&date=&staff_id=

21. Domain 19: Appointment vs Order vs Visit

Đây là chỗ rất dễ thiết kế sai.

Khái niệmNghĩa
AppointmentMột lần hẹn cụ thể ở một thời điểm
OrderGiao dịch/đơn mua dịch vụ
VisitMột lần nhân viên tới làm tại nhà
SeriesChuỗi appointment lặp lại, ví dụ gói tháng

Với đơn một lần:

Order 1-1 Appointment/Visit

Với gói tháng:

Order 1-1 Series

Series 1-n Visits

Kinh nghiệm:

  • Nếu chỉ có dịch vụ một lần, order và appointment có thể tạm chung model.
  • Khi có gói tháng, tách ra là bắt buộc.
  • Nếu không tách, mọi logic gói tháng sẽ biến thành if monthly khắp code.

EcoClean nên chuẩn bị:

Order: tiền và giao dịch

Appointment/Visit: lịch làm cụ thể

BookingSeries: lịch lặp lại

22. Domain 20: Manual dispatch trước, automatic dispatch sau

Timefold và Salesforce đều cho thấy field service scheduling có thể rất phức tạp: time windows, skills, travel, fairness, priority, route optimization, real-time planning.[^timefold-routing][^salesforce-policy]

Kinh nghiệm thực tế cho EcoClean:

  • Đừng cố làm auto dispatch hoàn chỉnh trước khi có đơn thật.
  • Hãy làm manual dispatch tốt trước.
  • Manual dispatch tốt nghĩa là admin nhìn được lịch, thấy conflict, gán được người, đổi được người, và hệ thống log lại.
  • Auto dispatch sau này chỉ là một lớp đề xuất trên data đã đúng.

MVP dispatch:

Customer đặt lịch

-> Order pending_confirmation

-> Admin thấy trong schedule board

-> Admin chọn staff

-> Hệ thống kiểm tra conflict đơn giản

-> Order assigned/confirmed

-> Customer nhận thông báo

Sau này:

System đề xuất 3 cleaner tốt nhất

Admin chọn hoặc auto assign

23. Những "bẫy" kinh nghiệm khi làm booking system

23.1. Bẫy 1: chỉ lưu ngày và giờ, không lưu interval

Sai:

booking_date = 2026-06-01

booking_time = 09:00

Đúng hơn:

scheduled_start_at = 2026-06-01T09:00:00+07:00

scheduled_end_at = 2026-06-01T12:00:00+07:00

timezone = Asia/Ho_Chi_Minh

Vẫn có thể giữ booking_date/booking_time để hiển thị, nhưng logic overlap phải dựa vào interval.

23.2. Bẫy 2: buffer chỉ cộng vào UI

Buffer phải chặn slot thật. Nếu không, hệ thống sẽ cho đặt hai đơn sát nhau, nhân viên không kịp di chuyển.

23.3. Bẫy 3: option chỉ cộng tiền

Dịch vụ thêm thường cộng cả thời gian, skill, vật tư, rủi ro vận hành.

23.4. Bẫy 4: favorite staff chỉ là staff_id

Cleaner yêu thích kéo theo:

  • minimum notice dài hơn.
  • kiểm tra availability của đúng người.
  • nếu người đó không rảnh thì phải báo rõ.
  • không được tự đổi sang người khác mà không hỏi khách.

23.5. Bẫy 5: admin order list và customer order list dùng chung

List của khách là lịch sử cá nhân. List của admin là công cụ vận hành. Trộn hai thứ này sẽ tạo bug quyền và UX rất nhanh.

23.6. Bẫy 6: gói tháng là order dài

Gói tháng là series gồm nhiều visit. Phải nghĩ bằng recurrence và exception.

23.7. Bẫy 7: "đặt lịch thành công" nhưng chưa có người làm

Nếu chưa có availability engine, wording nên là:

EcoClean đã nhận yêu cầu đặt lịch và sẽ xác nhận nhân viên.

Không nên nói chắc:

Nhân viên sẽ đến lúc 09:00.

trừ khi đã thật sự gán được nhân viên.

23.8. Bẫy 8: không có trạng thái no-show

Dịch vụ tại nhà cần phân biệt:

  • Khách hủy.
  • EcoClean hủy.
  • Nhân viên không đến.
  • Khách không có mặt.
  • Không liên hệ được.

Những trạng thái này ảnh hưởng tiền, điểm, coupon, rating và KPI.

24. Mô hình domain đề xuất cho EcoClean

24.1. Service layer

Service

- name

- description

- active

- display_order

ServicePackage

- service

- name

- duration_minutes

- area_m2

- base_price

- staff_count_required

- display_order

- video_key

ServiceOption

- service

- name

- price_delta

- duration_delta_minutes

- required_skill_tags

- active

24.2. Policy layer

SchedulingPolicy

- service nullable

- slot_interval_minutes

- min_notice_minutes

- max_future_days

- min_cancel_notice_minutes

- min_reschedule_notice_minutes

- allow_customer_reschedule

- require_admin_confirmation

OpenHours

- scope: business/service/staff

- weekday

- start_time

- end_time

- timezone

AvailabilityOverride

- scope

- start_at

- end_at

- type: available/unavailable

- reason

24.3. Resource layer

Staff

- user/profile

- name

- avatar

- active

- rating

- home_location

- timezone

StaffSkill

- staff

- skill_tag

- level

StaffTerritory

- staff

- city/district/ward

StaffTimeOff

- staff

- start_at

- end_at

- reason

24.4. Booking layer

BookingQuote

- user

- service

- package

- selected_options_snapshot

- requested_start_at

- expected_end_at

- address_snapshot

- pricing_breakdown

- expires_at

Order

- user

- order_number

- financial totals

- payment status

- status

ServiceVisit

- order

- series nullable

- scheduled_start_at

- scheduled_end_at

- arrival_window_start

- arrival_window_end

- status

- address_snapshot

Assignment

- visit

- staff

- status

- assigned_by

- accepted_at

24.5. Recurring layer

BookingSeries

- order

- recurrence_rule

- start_date

- end_date

- locked_by_policy

- status

SeriesException

- series

- original_start_at

- new_start_at nullable

- type: cancelled/rescheduled/admin_adjusted

- reason

25. Roadmap đúng cho EcoClean

Giai đoạn 1: Booking một lần, manual dispatch

Mục tiêu: đặt lịch thật, admin gán được người, khách theo dõi được.

Làm:

  • Hỏi họ tên/SĐT/địa chỉ khi đặt lịch.
  • Auto-fill từ profile/address.
  • Chuẩn hóa start/end interval.
  • Booking rules từ server.
  • Quote/breakdown rõ.
  • Order detail đủ gói/option/duration/address.
  • Admin order tách khỏi customer order.
  • Admin gán staff và đổi trạng thái.

Chưa cần:

  • GPS.
  • Route optimization.
  • Auto dispatch.
  • Gói tháng phức tạp.

Giai đoạn 2: Availability cơ bản

Mục tiêu: không gán trùng người, không nhận slot quá vô lý.

Làm:

  • Staff open hours.
  • Staff time off.
  • Conflict check theo interval.
  • Buffer mặc định.
  • Favorite staff availability.
  • Service duration + option duration.
  • Available slots API cho một ngày.

Giai đoạn 3: Schedule board

Mục tiêu: admin vận hành bằng lịch, không phải bằng list.

Làm:

  • View theo ngày.
  • View theo staff.
  • Màu theo trạng thái.
  • Kéo/thả nếu sau này cần.
  • Cảnh báo conflict.
  • Gợi ý cleaner rảnh.

Giai đoạn 4: Gói tháng

Mục tiêu: xử lý series thay vì order đơn.

Làm:

  • BookingSeries.
  • Sinh ServiceVisit theo recurrence.
  • Exception date.
  • Không cho user đổi ngày giờ trên app nếu policy là vậy.
  • Admin có thể điều chỉnh nội bộ có log.

Giai đoạn 5: Field service nâng cao

Mục tiêu: tối ưu khi có nhiều đơn.

Làm:

  • Territory.
  • Geocode địa chỉ.
  • Travel buffer.
  • Skill matching.
  • Fairness/round robin.
  • Route optimization nhẹ.
  • KPI nhân viên.

26. Cách áp dụng ngay cho các yêu cầu hiện tại

26.1. Flow đặt lịch nhập thông tin lần đầu

Nên làm theo kinh nghiệm booking questions:

  • Không hỏi ở đăng ký.
  • Hỏi ở booking confirmation.
  • Auto-fill nếu có.
  • Snapshot vào order/visit.
  • Field phụ thuộc service có thể thêm sau.

26.2. Logic ngày giờ

Nên sửa theo interval:

  • Tính earliest_start = now + min_notice.
  • Slot hôm nay phải bắt đầu từ ceil_to_next_interval(earliest_start).
  • end_at = start_at + duration + option_duration.
  • Check working hours theo start_at/end_at.
  • Nếu có favorite staff, min_notice đổi thành 180 phút.

26.3. Cleaner yêu thích

Không chỉ lưu staff_id.

Cần:

  • Check staff active.
  • Check staff có skill/service.
  • Check staff rảnh.
  • Check min notice 3h.
  • Nếu không rảnh, báo rõ "Cleaner này không còn lịch trống".

26.4. Admin order

Nên chuyển dần thành schedule board:

  • List đơn chỉ là bước đầu.
  • Admin cần view theo ngày/staff.
  • Gán staff là assignment, không phải chỉ update order.

26.5. Gói tháng

Khoan code vội nếu chưa cần ra mắt.

Nhưng khi làm, phải làm dạng series:

Order thanh toán gói tháng

BookingSeries giữ rule 28 ngày/4 lần

ServiceVisit là từng buổi

27. Checklist domain đặc trưng của booking system

Nếu một hệ thống đặt lịch thiếu các câu hỏi dưới đây, sớm muộn sẽ gặp lỗi:

Câu hỏiEcoClean đã cần chưa
Dịch vụ này kéo dài bao lâu?
Option có cộng thời gian không?
Slot bắt đầu theo interval nào?
Đặt trước tối thiểu bao lâu?
Đặt tối đa bao xa?
Có buffer trước/sau không?Nên có
Staff có work hours không?Sắp cần
Staff có time off không?Sắp cần
Staff có skill/territory không?Sắp cần
Gói cần 1 hay 2 người?
Khách chọn staff cụ thể không?
Slot là confirmed hay pending confirmation?Cần chốt
Hủy/đổi lịch trước bao lâu?Cần chốt
No-show xử lý thế nào?Sau MVP
Gói tháng là series chưa?Chưa, cần thiết kế trước
Admin có schedule board chưa?Chưa

28. Kết luận riêng cho EcoClean

EcoClean không nên cố làm ngay một hệ thống như Grab. Nhưng EcoClean cũng không nên tiếp tục coi booking là một form tạo order.

Hướng đúng là:

Service rõ -> Policy rõ -> Availability vừa đủ -> Manual dispatch tốt -> Sau đó mới auto tối ưu

Ưu tiên gần nhất:

1. Chuẩn hóa flow đặt lịch một lần. 2. Tách booking policy từ server. 3. Tính lịch bằng interval start/end. 4. Thêm duration cho option. 5. Làm buffer cơ bản. 6. Tách assignment nhân viên. 7. Tạo nền cho schedule board.

Điểm đáng nhớ nhất: trong hệ thống đặt lịch, "còn slot" không có nghĩa là "giờ đó nằm trong giờ mở cửa". Còn slot nghĩa là đủ thời gian, đủ người, đúng kỹ năng, đúng khu vực, có buffer, không vướng lịch bận, không vi phạm notice/cancel policy, và nếu chưa chắc thì phải gọi nó là yêu cầu chờ xác nhận.

29. Nguồn tham khảo đúng phạm vi

[^ms-bookings-api]: Microsoft Graph, "Use the Microsoft Bookings API in Microsoft Graph for shared bookings": https://learn.microsoft.com/en-us/graph/api/resources/booking-api-overview [^ms-scheduling-policy]: Microsoft Bookings, "Set your Microsoft Bookings scheduling policies": https://learn.microsoft.com/en-us/microsoft-365/bookings/set-scheduling-policies [^ms-service-availability]: Microsoft Bookings, "Configure service availability": https://learn.microsoft.com/bs-latn-ba/microsoft-365/bookings/configure-service-availability [^ms-appointment]: Microsoft Graph, bookingAppointment resource type: https://learn.microsoft.com/en-us/graph/api/resources/bookingappointment [^cal-event-types]: Cal.com docs, "Event Types": https://calcom-cal-com.mintlify.app/features/event-types [^cal-buffer]: Cal.com Help, "Event buffers": https://cal.com/help/event-types/event-buffer [^nylas-availability]: Nylas Scheduler docs, "Managing availability in Nylas Scheduler": https://developer.nylas.com/docs/v3/scheduler/managing-availability/ [^google-freebusy]: Google Calendar API, "Freebusy: query": https://developers.google.com/workspace/calendar/api/v3/reference/freebusy/query [^rfc5545-freebusy]: RFC 5545 iCalendar, Free/Busy component: https://www.rfc-editor.org/rfc/rfc5545 [^rfc5545-recurrence]: RFC 5545 iCalendar, Recurrence Rule / RRULE / RDATE / EXDATE: https://www.rfc-editor.org/rfc/rfc5545 [^rfc7953]: RFC 7953, "Calendar Availability": https://www.rfc-editor.org/rfc/rfc7953 [^dynamics-resource]: Microsoft Dynamics 365 Field Service, "Set up bookable resources": https://learn.microsoft.com/en-us/dynamics365/field-service/set-up-bookable-resources [^salesforce-policy]: Salesforce Trailhead, "Examine Scheduling Policies": https://trailhead.salesforce.com/content/learn/modules/field-service-lightning-scheduling-basics/examine-scheduling-policies [^salesforce-appointment]: Salesforce Help, "Schedule Appointments from the Record Feed": https://help.salesforce.com/s/articleView?id=service.pfs_booking.htm&type=5 [^timefold-routing]: Timefold Field Service Routing model/docs: https://app.timefold.ai/models/field-service-routing [^calendly-team]: Calendly Help, "Multi-person scheduling options": https://help.calendly.com/hc/en-us/articles/14077508073111 [^calendly-buffer]: Calendly Help, "How to use buffers": https://help.calendly.com/hc/en-us/articles/14048208107287-How-to-use-buffers [^calendly-workflows]: Calendly Learn, "Automate Meeting Emails and Texts": https://calendly.com/learn/calendly-workflows