Trường Số · Product Requirements · 09/2026

PRD 03 · Classes and scheduling

Courses, classes, rooms, recurring schedules that generate physical sessions, timetable views, capacity, enrollments, teacher and substitute assignment, and make-up sessions. Extends the existing Class, ClassSchedule and Course objects with rooms and materialised sessions.

Key facts, origin and limits
Status
Draft v1
Owner module
Lớp học
Backend
EXTEND Class, ClassSchedule, Course · NEW room, session, enrollment
Phase
P1
Depends on
02 Students, 06 Staff

Drafted by an AI assistant in a single session on 10/09/2026, from the original request quoted verbatim on the hub page; the Proposed design section was added on 16/09/2026. A proposal for team discussion, not a decision of the product team.

Sources

  • Public competitor research (PSE ONE, pse4.one), the center-crm-frontend codebase and the staging /v3 OpenAPI specification (182 paths), as of 10/09/2026.
  • The existing / extend / new tags on this page come from a comparison with the staging /v3 specification at that date.

Limits

  • Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
    • Competitor's "lịch học tự động" inputs: whether sessions are generated as rows and how holidays are handled.
    • Whether rooms exist and conflicts are checked.
    • How "phân lớp giáo viên" and "xếp lịch giáo viên theo tuần" look (grid vs list).
    • Their handling of make-up ("học bù") and whether it consumes a package session.
    • Class capacity and waiting list behaviour.

Proposed design

The module revolves around three screens: the class list, the class detail with sessions materialised as rows, and the room timetable; the most important thing to see is that every room or teacher conflict is detected and shown in place before anything is saved.

1 · Class list

/classes · desktop

Class list
Five status tabs (Active · Starting soon · Full · Completed · All), filters for branch, course, level, teacher, weekday, time slot and Seats available; the table carries all ten columns of section 5: code, name with a level chip, schedule, room, teacher and assistant, seats with a progress bar, session progress, start · end, status, actions. Movers 2B is full at 12/12, shown in red with "2 waiting" (CLS-07); the per-row menu offers View schedule · Add student · Today's attendance.

2 · Class detail · Sessions tab

/classes/:id (tab Buổi học) · desktop

Class detail, Sessions tab
Layout follows the Cloud theme lop-hoc frame: page head, seven tabs and a right rail (class info with a copyable code, fill rate, next session, progress). The sessions table has all nine columns plus the Create session · Regenerate · Holidays toolbar. Session 5 was cancelled late and a make-up added at the end of the course (CLS-05), session 7 has a substitute teacher (CLS-06), session 8 changed room and is flagged "Adjusted" (CLS-04); Edit · Substitute · Cancel appear per row.

3 · Class detail · Students tab and waiting list

/classes/:id (tab Học viên) · desktop

Class detail, Students tab
Roster table with name, parent phone, start date, status, sessions attended, balance and flags; Transfer · End · Message actions per row. The class is full at 12/12, so the callout states that new students go to the waiting list (CLS-07); the Waiting list section shows two students ordered by date added; one trial seat is held separately (capacity rule, section 6); a student with unpaid tuition carries a red flag.

4 · Create class form · step 1

/classes/new · desktop

Create class drawer, step 1
Two-step drawer with every field of section 5: branch, course (auto-fills level, duration, capacity, session count), class name, auto-generated code <LEVEL>-<YYMM>-##, main teacher, assistant, room, capacity, trial seats, weekday chips with per-day times, start date, end by session count or by date, holidays preloaded from the centre list, FLYER programme. The class name field shows the duplicate-name-in-branch error (CLS-02).

5 · Create class form · step 2 preview

/classes/new (dry_run) · desktop

Create class drawer, step 2 session preview
The 30 sessions to be generated with four summary tiles; two rows carry Room conflict and Teacher conflict chips because the API returned 409 CONFLICT with the clashing sessions (CLS-03). "Save anyway (override)" only appears for classes.override_conflict; the primary action is "Create class and 30 sessions".

6 · Timetable by room

/timetable?view=room&week=2026-W38 · desktop

Timetable by room
By room · By teacher · By class switch, week and day pickers, branch and level filters, Print (A4 landscape per room) and Export iCal (CLS-09). The grid runs in 30-minute rows with rooms as columns, block colour by level with a text legend. Flyers 1C and IELTS Junior 3 (moved from P.203, under maintenance) overlap by 30 minutes in P.201, so both are drawn in red and called out in the banner with the person who overrode the conflict (CLS-03, room-conflict rule in section 6).

7 · Rooms (settings)

/settings/rooms · desktop

Room settings
Room table with all five columns (name, branch, capacity, equipment tags, active). The edit drawer for P.203 shows the unique-name-per-branch constraint and refuses to deactivate the room while 14 future sessions are still scheduled there (ROOM_IN_USE), with a shortcut to move them in bulk to another room.

8 · Transfer class (modal)

/classes/:id (tab Học viên) · desktop

Transfer class modal
Transfer modal for Phạm Khánh Ngọc: target classes filtered by level and seats available, level-fit chips, a full class shown as 12/12 in red; the effective date ends the old enrolment and starts the new one on the same day (CLS-07); reason is required; options to move the remaining 20 package sessions, notify the parent and offer the freed seat to a waiting student. The secondary button "End enrolment, do not transfer" covers ending before the class finishes.

9 · Teacher's teaching schedule (mobile)

/timetable (chế độ xem theo ngày) · mobile

Teacher schedule on mobile
Day view for mobile (section 5): today's sessions for Lê Thu Trang with a button to open attendance, the substitute session carries a Substitute chip (CLS-06), a confirmed-substitution notice and an iCal week export button (CLS-09).

1Purpose

A center lives by its timetable. Classes must be created quickly from a course template, placed in rooms without collisions, taught by an assigned teacher with a substitute mechanism, and produce a list of concrete sessions that attendance, tuition packages and teacher pay can hang off. The FLYER backend has the class (name, code, type), a recurrence template (ClassSchedule with days_of_week, time_local, start and end date) used to auto-assign FLYER lessons, and a course catalog with pricing. This PRD adds what a physical center needs: rooms, sessions as rows, capacity, enrollments with dates, make-ups and timetable views.

In scope

Out of scope

2What exists today

ObjectFieldsGap
Class / SchoolClass (GET /v3/teacher/classes)id int64, name, code, class_type, student_count, courses[{id, name, price}], created_atno level, room, capacity, dates, teacher, status
ClassSchedule (POST/GET/PUT/DELETE /v3/teacher/class-schedules)class_id, days_of_week[], time_local, timezone, start_date, end_date, is_active, program_type STUDY|BOOK, program ids, delivery_time, deadline_days, lessons[] (rollup with remind status)one active schedule per class; no duration, room, teacher; lessons are content units, not physical sessions
Course (/v3/teacher/school-finance/course/courses)name, description, level, number_of_lessons, price, currency, pricing_method ONE_TIME|MONTHLY, split_payment_count, statusno session duration, no age band, no default capacity
POST .../classes/{class_id}/coursesattach course to classkeep
POST /v3/teacher/class-schedules/{id}/remind, preview-remind-timeFLYER lesson reminderskeep
Exam sessions (SessionInput with check_point_id)exam-onlynot reused for class sessions

3Personas and roles

PersonaNeeds
Academic adminCreate classes, generate and adjust sessions, fill classes, handle transfers and make-ups, print timetables.
Branch managerSee fill rate and teacher load, approve class opening and closing.
TeacherOwn weekly timetable, roster with notes, request substitute.
AdmissionsFind classes with seats by level and time for trials and conversion.
GuardianSee the child's timetable and changes (PRD 07).

4User stories and acceptance criteria

CLS-01Maintain the course catalog
As an admin I define courses once.
  • Given Settings → Khoá học or Classes → Khoá học
  • When I create a course with name, level, age band, number of lessons, session duration, default capacity, price and pricing method
  • Then it is saved through the existing course endpoint plus the extension fields, and appears in the class form.
CLS-02Create a class with a schedule
As an admin I open a class.
  • Given a course
  • When I enter class name (auto-suggested "Movers 2 · T3/T5 18:00"), branch, course, level, teacher, assistant (optional), room, capacity, trial seats, start date, end date or number of sessions, weekday pattern with time per weekday, and holidays to skip
  • Then the class is created (existing endpoint) with the extension record, a ClassSchedule is created for FLYER lessons if a program is chosen, and N sessions are generated as rows with date, start, end, room, teacher; the form shows conflicts before saving.
CLS-03Detect conflicts
As an admin I never double-book a room or a teacher.
  • Given a session being created or moved
  • When another session overlaps in the same room, or with the same teacher, in the same branch (teacher conflicts checked across branches)
  • Then the API returns 409 CONFLICT with the conflicting sessions and the UI shows them inline with an "Vẫn lưu" override for classes.override_conflict.
CLS-04Edit one session or the series
As an admin I move a session for a holiday.
  • Given a generated session
  • When I change its date, time, room or teacher
  • Then only that session changes and is marked "Đã điều chỉnh"; choosing "Áp dụng cho các buổi sau" regenerates future planned sessions (held sessions untouched); guardians of enrolled students receive a change notice (PRD 07) if enabled.
CLS-05Cancel a session
As an admin I cancel a session because the teacher is ill.
  • Given a planned session
  • When I cancel with reason and choose "Thêm buổi bù cuối khoá" or "Không bù"
  • Then status is cancelled (or cancelled_late when within 24 h, relevant to PRD 06 pay); a make-up session is appended after the last session if chosen; no session is consumed from student packages; guardians are notified.
CLS-06Assign a substitute
As an admin I put a substitute on a session.
  • Given a session
  • When I set substitute teacher
  • Then the session shows both main and substitute; the substitute sees it in their timetable and may mark attendance; PRD 06 pays the substitute.
CLS-07Enroll, transfer, end
As an admin I manage the roster.
  • Given a class with capacity 12 and 11 active enrollments
  • When I add a student with start date
  • Then the enrollment is created; at 12 the class shows "Đầy" and further adds go to the waiting list; transfer to another class ends the enrollment at date D and starts a new one at D (same day allowed); ending an enrollment before the class end asks for reason (PRD 02 lifecycle).
CLS-08Make-up session for a student
As an admin I book a make-up for a student who was absent.
  • Given an absence with excuse (PRD 04) and tenant policy "make-up allowed"
  • When I pick a session of another class at the same level with free seats, or create a one-off make-up session
  • Then a makeup_booking is created; the student appears on that session roster with a Học bù chip; attending it does not consume a package session if the tenant policy says so.
CLS-09View timetables
As a manager I see the week at a glance.
  • Given the timetable page
  • When I switch between By room, By teacher, By class, and a date range
  • Then a week grid (07:00 to 22:00, 30-minute rows) shows session blocks with class name, teacher, room and enrolled/capacity; clicking opens the session drawer; print produces an A4 landscape sheet per room.
CLS-10Close a class
As an admin I end a class.
  • Given the last session is held or the admin chooses Kết thúc lớp
  • When confirmed
  • Then class status becomes completed, enrollments end, students with no other enrollment become Completed (PRD 02), and the class list moves it to Đã kết thúc.

5Screens

Class list /classes

Tabs: Đang học · Sắp khai giảng · Đầy · Đã kết thúc · Tất cả. Filters: branch, course, level, teacher, weekday, time band (sáng/chiều/tối), has seats. Buttons: + Tạo lớp, Thời khoá biểu.

ColumnNotes
Mã lớpclass.code
Tên lớplink; level chip
Lịch"T3, T5 · 18:00–19:30"
Phòngroom name
Giáo viênavatar + name; assistant small
Sĩ số"11/12" with bar; red when full; waiting count
Tiến độheld / total sessions
Khai giảng · Kết thúcdates
Trạng tháiSắp mở (exc) · Đang học (ok) · Đầy (late) · Đã kết thúc (grey)
Thao tácXem lịch · Thêm học viên · Điểm danh hôm nay

Empty state: "Chưa có lớp. Tạo khoá học trước, rồi mở lớp từ khoá học."

Class detail /classes/:id

Header per Cloud theme lop-hoc frame: name, subtitle "T3 & T5, 18:00 · 12 học viên · Movers", buttons Sửa · Giao bài (FLYER) · kebab (Nhân bản lớp, Kết thúc lớp). Tabs: Tổng quan · Học viên · Buổi học · Điểm danh · Bài tập (existing FLYER assign tests) · Học phí · Cài đặt. Right rail: Thông tin lớp (code with copy, teacher, room, capacity, course, program), fill rate, next session.

  • Học viên: roster table (name, guardian phone, start date, status, sessions attended/total, balance, flags) with actions Chuyển lớp, Kết thúc, Gửi tin; waiting list section; + Thêm học viên (search students of branch, shows level match).
  • Buổi học: sessions table (No., date, time, room, teacher, substitute, status, attendance marked, note) with per-row edit, cancel, substitute; toolbar Tạo buổi, Tạo lại lịch, Nghỉ lễ.
  • Cài đặt: capacity, trial seats, allow make-up, FLYER program (existing ClassSchedule fields).
Class form (drawer, 2 steps) /classes/new
FieldTypeValidation
Chi nhánhselectrequired
Khoá họcselectrequired; fills level, duration, capacity, lessons
Tên lớptextrequired; unique per branch among open classes
Mã lớptextauto <LEVEL>-<YYMM>-##, editable
Giáo viên chính, Trợ giảngstaff picker (teaching, branch)main required before first session
Phòngselect rooms of branchrequired
Sĩ số tối đa, Chỗ học thửint1–60; trial ≤ 5
Lịch họcweekday chips; per weekday time from/to; duration default from course≥ 1 weekday; from < to
Khai giảngdaterequired
Kết thúcradio: theo số buổi (default course lessons) / theo ngày
Ngày nghỉmulti-date; preloaded from tenant holiday list
Chương trình FLYERexisting program picker (optional)

Step 2 preview: generated sessions list with conflict chips; button Tạo lớp và N buổi.

Timetable /timetable?view=room|teacher|class&week=2026-W37

Week grid, columns = rooms (or teachers, or classes), rows = 30 min. Block colour by level. Drag to move (desktop) with conflict check on drop. Filters: branch, level. Buttons: In, Xuất iCal (teacher view). Day view for mobile.

Rooms (settings) /settings/rooms

Table: name, branch, capacity, equipment tags, active. Form: name (unique per branch), capacity, note.

6Business rules

  1. Sessions are generated from the pattern at class creation and whenever the pattern changes, only for sessions with status planned. Held or cancelled sessions are never regenerated.
  2. Session numbering is by date order among non-cancelled sessions; renumbered on regeneration.
  3. Room conflict: same room, overlapping time, both sessions not cancelled. Teacher conflict: same staff as main or substitute, overlapping time, any branch. Buffer between sessions is 0 by default (tenant setting up to 30 min).
  4. Capacity counts enrollments active on the session date plus trial and make-up bookings for that session; trial seats are reserved separately.
  5. An enrollment has start and end dates; a student is on the roster of sessions within [start, end].
  6. Class status: draft (no sessions), scheduled (start date in future), active, completed, cancelled; derived from dates and sessions except cancelled.
  7. Late cancellation threshold 24 h (tenant setting) marks cancelled_late for pay rules.
  8. Level mapping for placement suggestion: tenant table level_code → score band; default seeded from CEFR (Pre-A1, A1, A2, B1, B2, C1) and Cambridge YLE (Starters, Movers, Flyers, KET, PET).

7Data model

EntityFieldsNotes
course (existing Course)+ session_duration_min int; age_from; age_to; default_capacity; level_code; is_templateextend
level (catalog)id; tenant_id; code; name; framework enum(cefr, cambridge_yle, ielts, custom); score_from; score_to; positionnew
roomid uuid; school_id; name; capacity; equipment text[]; is_activenew; unique (school_id, name)
class_profile (extends Class)class_id int64 PK; school_id; course_id; level_code; main_staff_id; assistant_staff_id; room_id; capacity; trial_seats; start_date; end_date; planned_sessions; status enum; allow_makeup bool; pattern jsonb [{weekday, start, end}]; holidays date[]new
class_sessionid uuid; class_id; school_id; seq int; date; start_at; end_at (timestamptz); room_id; main_staff_id; substitute_staff_id null; status enum(planned, held, cancelled, cancelled_late); cancel_reason; is_makeup bool; is_adjusted bool; note; attendance_marked_at; created_at; updated_atnew; index (school_id, date), (room_id, start_at), (main_staff_id, start_at)
enrollmentid uuid; student_id; class_id; start_date; end_date null; status enum(active, ended, waiting); end_reason; fee_plan_id null; created_by; created_atnew; one active enrollment per (student, class)
makeup_bookingid; student_id; source_session_id (absence); target_session_id; consumes_package bool; status enum(booked, attended, no_show, cancelled)new
holidayid; tenant_id; date; namenew

8API contract

MethodPathRequest / responseStatus
GET/POST/PUT/DELETE/v3/teacher/school-finance/course/courses[/{id}]as today + extension fieldsextend
GET/v3/teacher/classesas today; response adds profile summaryextend
POST/v3/school/classes{school_id, course_id, name, code?, level_code, main_staff_id?, assistant_staff_id?, room_id, capacity, trial_seats, start_date, end_by: {sessions: n} | {date}, pattern[], holidays[], program?: CreateClassScheduleRequest fields} → {class, sessions[], conflicts[]}; dry_run=true returns preview onlynew (creates Class + ClassSchedule internally)
GET/PATCH/v3/school/classes/{id}detail with profile, roster counts, next sessionnew
POST/v3/school/classes/{id}/regenerate{pattern, holidays, from_date} planned sessions onlynew
POST/v3/school/classes/{id}/close | /cancel{reason}new
GET/v3/school/sessionsschool_id, from, to, class_id, room_id, staff_id, status → Session[] with class, room, teacher, countsnew
POST/v3/school/classes/{id}/sessions{date, start, end, room_id, staff_id, is_makeup} one-offnew
PATCH/v3/school/sessions/{id}{date?, start?, end?, room_id?, main_staff_id?, substitute_staff_id?, note?, apply_to_following?: bool, override_conflict?: bool} → 409 CONFLICT {conflicts[]}new
POST/v3/school/sessions/{id}/cancel{reason, add_makeup: bool, notify: bool}new
GET/POST/v3/school/classes/{id}/enrollments{student_id, start_date, fee_plan_id?} → 409 CLASS_FULL (offers waiting)new
PATCH/v3/school/enrollments/{id}{end_date, end_reason} or {transfer_to_class_id, effective_date}new
GET/v3/school/sessions/{id}/rosterenrolled + trial + makeup with flags and balancenew
POST/v3/school/makeups{student_id, source_session_id, target_session_id | new_session: {...}, consumes_package}new
GET/POST/PATCH/v3/school/roomsnew
GET/POST/DELETE/v3/school/holidaysnew
GET/POST/PATCH/v3/school/levelslevel catalog with score bandsnew
GET/v3/school/timetableschool_id, week, view=room|teacher|class → grouped sessionsnew
GET/v3/school/staff/{id}/timetable.icssigned linknew
POST/GET/PUT/v3/teacher/class-schedules[...]FLYER program lessons; unchangedexisting

9Permissions

Codeownerbranch_manageracademic_adminadmissionsteacher
classes.viewallbranchbranchbranch readown
classes.manage (create, edit, close)yesyesyesnono
sessions.manage (edit, cancel, substitute)yesyesyesnorequest only
classes.override_conflictyesyesnonono
enrollments.manageyesyesyesvia convertno
makeups.manageyesyesyesnono
rooms.manage, courses.manage, levels.manageyesroomscourses, roomsnono
timetable.viewallbranchbranchbranchown

10Analytics events

EventProperties
class_createdlevel, weekdays, sessions, capacity, with_program
session_conflict_shown / session_conflict_overriddentype (room|teacher)
session_edited / session_cancelledlate (bool), makeup_added, notified
substitute_assignedhours_before
enrollment_created / ended / transferredreason, fill_rate_after
makeup_bookedconsumes_package
timetable_viewedview, printed
class_closedsessions_held, sessions_cancelled

11Edge cases

12Open questions

Trường Số (truongso.net) · FLYER internal