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ồn | Vì sao liên quan |
|---|---|
| Microsoft Bookings / Microsoft Graph Bookings | Một mô hình đặt lịch dịch vụ chuẩn: business, service, staff, appointment, customer, scheduling policy |
| Cal.com / Calendly / Nylas Scheduler | Các sản phẩm scheduling hiện đại: event type, availability, buffer, minimum notice, round robin, collective booking |
| Google Calendar FreeBusy API | Cách tư duy availability theo khoảng busy/free trong một time range |
| RFC 5545 iCalendar và RFC 7953 Calendar Availability | Chuẩ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ớp | Câu hỏi chính | Ví dụ EcoClean |
|---|---|---|
| Service design | Khá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 |
| Availability | Có 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/operation | Sau 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 scheduling | EcoClean nên map thành |
|---|---|
| Event type / booking service | Dịch vụ: giúp việc theo giờ, giúp việc theo tháng, giặt ủi, dọn phòng |
| Duration | Thờ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 questions | Họ tên, SĐT, địa chỉ, ghi chú, có thú cưng không |
| Service-level overrides | Gó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ụ:
| Option | Nếu chỉ cộng tiền | Nế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 | +10k | Có 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 hours | Ví dụ |
|---|---|
| Business open hours | EcoClean nhận lịch 07:00-19:00 |
| Service open hours | Giặt ủi chỉ nhận 08:00-17:00 |
| Staff open hours | Cleaner A làm thứ 2-6, 08:00-16:00 |
| Date-specific open hours | Ngà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[].start và busy[].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 fieldbooking_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ống | Buffer đề xuất |
|---|---|
| Chưa có GPS/route | 30 phút sau mỗi đơn |
| Có cleaner yêu thích | 45-60 phút vì phải giữ đúng người |
| Gói dài hơn 4h | 30 phút trước và 30 phút sau |
| Khác phường/quận | Sau 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à:
| Resource | Ví dụ |
|---|---|
| Person | Cleaner |
| Crew | 2 cleaner đi cùng một đơn |
| Equipment | Máy hút bụi, máy khử khuẩn Nano bạc |
| Facility | Kho/văn phòng, nếu có nhận đồ giặt |
| Pool | Nhó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ại | Capacity nên hiểu |
|---|---|
| Cleaner | 1 đơn tại một thời điểm |
| Cleaner yêu thích | 1, không được auto đổi người |
| Gói cần 2 người | cần 2 cleaner rảnh cùng khoảng |
| Máy hút bụi | nế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 rules và service 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:
| Rule | Ví dụ |
|---|---|
| Availability | Cleaner rảnh trong khoảng start/end + buffer |
| Territory | Cleaner làm khu vực/quận/phường đó |
| Skill | Cleaner làm được gói có thú cưng, ủi đồ, máy khử khuẩn |
| Required resource | Khách chọn cleaner yêu thích thì phải đúng cleaner |
| Time window | Slot nằm trong giờ phục vụ |
| Capacity | Không nhận quá số đơn cùng lúc |
Scoring cho EcoClean:
| Objective | Khi nào dùng |
|---|---|
| Minimize travel | Khi có địa chỉ/toạ độ |
| Preferred cleaner | Khi khách chọn cleaner yêu thích |
| Rating | Khi muốn ưu tiên cleaner tốt |
| Fairness | Khi muốn chia đều đơn |
| Avoid overtime | Khi có lịch ca |
| Skill fit | Khi 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ểu | Nghĩa | EcoClean tương ứng |
|---|---|---|
| Round robin | Ai rảnh thì nhận, có thể chia đều | Đơn thường, admin/engine chọn cleaner |
| Collective | Tất cả người liên quan phải cùng rảnh | Gói cần 2 cleaner đi cùng |
| Selected staff | Khách chọn một người cụ thể | Cleaner yêu thích |
| Pool | Giữ chỗ cho một nhóm, chọn người sau | Giai đ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ạn | Cách làm location |
|---|---|
| MVP | Lưu địa chỉ text, phường/quận/thành phố |
| Giai đoạn 2 | Chuẩn hóa địa chỉ, chọn phường/quận từ danh sách |
| Giai đoạn 3 | Geocode địa chỉ, lưu lat/lng |
| Giai đoạn 4 | Tính travel buffer theo khoảng cách |
| Giai đoạn 5 | Route 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_snapshot và geo 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
RecurringPlanhoặcBookingSeries. - 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 field | Ví dụ |
|---|---|
| Profile field | Tên, email, avatar |
| Contact field cho booking | Họ tên người nhận, SĐT liên hệ |
| Address field | Địa chỉ phục vụ |
| Service-specific question | Có thú cưng không, có thang máy không, cần mang vật tư không |
| Internal note | Admin/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ái | Nghĩa |
|---|---|
| draft | User đang chọn |
| quoted | Server đã báo giá |
| requested | Khách gửi yêu cầu đặt |
| pending_confirmation | Chờ EcoClean xác nhận/gán nhân viên |
| confirmed | EcoClean đã xác nhận slot |
| assigned | Đã có nhân viên |
| in_progress | Đang làm |
| completed | Hoàn thành |
| cancelled | Hủy |
| no_show | Khá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:
| Event | Người nhận |
|---|---|
| Booking requested | Admin |
| Booking confirmed | Customer |
| Staff assigned | Customer, staff |
| Reminder trước giờ làm | Customer, staff |
| Staff on the way | Customer |
| Staff started | Customer/admin |
| Completed | Customer/admin |
| Cancelled/rescheduled | Các bên liên quan |
| Chat message | Ngườ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:
| View | Người dùng | Mục tiêu |
|---|---|---|
| My orders | Customer | Xem đơn đã đặt |
| My schedule | Staff | Xem việc được giao |
| Schedule board | Admin | Gá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ệm | Nghĩa |
|---|---|
| Appointment | Một lần hẹn cụ thể ở một thời điểm |
| Order | Giao dịch/đơn mua dịch vụ |
| Visit | Một lần nhân viên tới làm tại nhà |
| Series | Chuỗ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 monthlykhắ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ỏi | EcoClean đã cần chưa |
|---|---|
| Dịch vụ này kéo dài bao lâu? | Có |
| Option có cộng thời gian không? | Có |
| Slot bắt đầu theo interval nào? | Có |
| Đặt trước tối thiểu bao lâu? | Có |
| Đặt tối đa bao xa? | Có |
| 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? | Có |
| Khách chọn staff cụ thể không? | Có |
| 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