SDD-034 — Buổi học cùng Dolphin: khung giờ, nguyện vọng, đăng ký
Chỉ đạo chủ dự án 2026-09-15: "Learner cần có thể chọn các slot thời gian yêu thích nếu học IELTS cùng Teacher. Cần chọn slot thời gian yêu thích nhất và slot yêu thích nhì. Chỉ được chọn trong các slot mà hệ thống đã setup từ trước. Được đăng ký tham gia các slot trước 48 giờ, để các teacher còn chuẩn bị. Trong các buổi đó, learner có link Google Meet."
Bốn khung mặc định do chủ dự án chốt: 10:00–12:00 chủ nhật · 15:00–17:00 chủ nhật · 20:00–22:00 tối thứ hai · 20:00–22:00 tối thứ tư, lặp hàng tuần.
1. Nguyên tắc
- Nguyện vọng và đăng ký là hai việc, không phải một. "Con rảnh khung nào" là lời khai dùng để xếp lớp; "con đi buổi chủ nhật này" là cam kết một buổi. Gộp chúng vào một nút là cách chắc chắn nhất để learner tưởng mình đã có lịch trong khi phòng học trống — nên chúng nằm ở hai bảng, hai endpoint, hai khối trên màn hình.
- Buổi cụ thể được TÍNH, không được sinh sẵn. Khung lặp vô hạn về tương lai; sinh sẵn thì phải chọn một chân trời và ngày nào đó chân trời ấy hết. Buổi chỉ hoá thành dòng dữ liệu khi có người ghi tên.
- Luật hạn đăng ký đo tới giờ BẮT ĐẦU buổi. Từ 28.09.2026 (SRC-1124) hạn là 2 giờ trước giờ bắt đầu, thay cho 48 giờ của chỉ đạo 2026-09-15 ở trên: chủ dự án muốn learner đăng ký được mọi event tới sát 2 giờ trước., không tới ngày của buổi. Tính theo ngày thì người đăng ký 23:00 và người đăng ký 06:00 hôm sau được đối xử như nhau, trong khi Teacher mất bảy tiếng chuẩn bị.
- Client không tự trừ hạn đăng ký. Đồng hồ máy learner có thể sai; một đồng hồ sai được phép mở nút "ghi tên" là cách để thầy cô chuẩn bị mà học sinh không tới. Server tính, client chỉ hiển thị cờ
bookable. - Link phòng chỉ tới tay người đã ghi tên. Cùng luật với
meeting_urlcủacourse_sessions(SDD-031 §4).
2. Dữ liệu (migration 0223_mentor_slots.sql)
| Bảng | Trả lời câu hỏi | Vòng đời |
|---|---|---|
mentor_slots | Teacher mở những khung giờ nào | Lặp hàng tuần; đổi vài tháng một lần |
slot_preferences | Learner thích khung nào nhất, khung nào nhì | Một dòng mỗi hạng; đổi khi thời khoá biểu ở trường đổi |
slot_bookings | Buổi ngày cụ thể ấy ai đã ghi tên | Một dòng một buổi |
weekdaytheo quy ước SQLite (strftime('%w')): 0 = chủ nhật. Chọn quy ước của chính CSDL để câu lọc không phải cộng trừ ở tầng ứng dụng.- Giờ lưu dạng
HH:MMgiờ Việt Nam, không offset: mọi buổi diễn ra ở Việt Nam và người đọc lịch cũng ở Việt Nam. PRIMARY KEY (learner_id, rank)ép mỗi hạng đúng một khung;UNIQUE (learner_id, slot_id)chặn khai cùng một khung cho cả hai hạng.idx_slot_booking_once … WHERE status <> 'cancelled': chặn ghi tên trùng nhưng vẫn cho huỷ rồi đăng ký lại. Đây là chỗ duy nhất phân xử được hai người bấm cùng lúc.slot_bookings.meeting_urlchép lại giá trị lúc đăng ký: đổi link của khung vào tuần sau thì buổi tuần này vẫn giữ link learner đã nhận.
3. Phép tính lịch (modules/slots/occurrences.ts)
Tách khỏi CSDL để luật nghiệp vụ test được bằng mười dòng. Bẫy đã suýt lọt và nay có test giữ: weekdayOf('2026-09-20') phải parse ở UTC, không phải +07:00 — một ngày lịch gắn offset rồi hỏi getUTCDay() là hỏi về mốc 17:00 hôm trước, và mọi khung buổi sáng bị đọc lệch một ngày mà không lỗi nào nổ.
occurrencesOf() trả cả buổi không còn kịp đăng ký kèm bookable: false. Giấu chúng đi làm learner tưởng tuần này Teacher không mở buổi nào, trong khi sự thật là họ vào muộn mấy tiếng — hai việc phải làm khác hẳn nhau.
4. API (modules/slots/routes.ts)
| Route | Vai | Việc |
|---|---|---|
GET /v1/slots | learner · phụ huynh | Khung đang mở + buổi sắp tới, kèm chỗ còn lại và cờ bookable |
GET·PUT /v1/learners/{id}/slot-preferences | learner · phụ huynh | Khai/đọc nguyện vọng nhất và nhì |
GET·POST /v1/learners/{id}/slot-bookings | learner · phụ huynh | Ghi tên một buổi; 400 nếu còn dưới 2 giờ tới giờ bắt đầu (SRC-1124), 409 nếu trùng hoặc hết chỗ |
DELETE /v1/slot-bookings/{id} | learner · phụ huynh | Huỷ ghi tên |
GET·POST·PATCH /v1/mentor/slots | mentor · staff · admin | Mở khung, gắn link Google Meet, đổi sức chứa |
GET /v1/mentor/slot-roster | mentor · staff · admin | Ai ghi tên buổi nào |
GET /v1/mentor/slot-preferences | mentor · staff · admin | Ai mong khung nào — gồm cả learner chưa ghi tên buổi nào |
Quyền phía learner đi qua requireLearnerAccess (QG-008): client có gửi learner_id vì một phụ huynh hai con phải chọn được đang xếp lịch cho đứa nào, nhưng gửi id con nhà khác thì nhận 401.
PATCH /v1/mentor/slots/{id} chỉ ghi đè cột có mặt trong body: gán cả bảng thì một client gửi thiếu một trường sẽ âm thầm xoá link phòng, và không ai biết cho tới buổi học kế tiếp.
5. Giao diện
apps/learn/src/SlotsPage.tsx— mục Học cùng thầy cô trong phần IELTS: buổi sắp tới của con · biểu mẫu nguyện vọng nhất/nhì · từng khung kèm các buổi ghi tên được.apps/mentors/src/Slots.tsx— tab Slots của cổng Dolphin: khung nào chưa có link phòng (cảnh báo đỏ), ai đã ghi tên buổi nào, và danh sách "đã khai giờ rảnh nhưng chưa ghi tên buổi nào" — chính là danh sách để quyết định mở lớp.
6. Phần CỐ Ý chưa làm
- Điểm danh.
slot_bookings.statusđã cóattended/absentnhưng chưa có đường ghi. Điểm danh thuộc REQ-MEN-04 và cần một vòng chỉ đạo riêng. - Nhắc trước buổi học (email/thông báo). Hệ thư đã có, nhưng trần thư và luật gửi cho learner phải do chủ dự án chốt trước.
- Gộp buổi slot vào lịch hợp nhất (SDD-031).
service.upcomingFor()đã trả đúng hình dạng cần; nối vàocalendar/service.tslà một vòng sau, để lịch không đổi hành vi trong cùng một đợt.
Trace
| REQ | Section |
|---|---|
| REQ-MEN-17 | §1, §2, §3, §4, §5 |