- Given the
curriculum.managepermission and a level from the catalogue (PRD 03) - When they create a curriculum with name, code, level, branch scope, then add units and lessons by typing or by pasting a list of lesson names (one per line)
- Then the curriculum is created with version 1 in
draft; units and lessons get consecutivepositionvalues; drag-and-drop reordering saves immediately; a duplicate code within the centre is rejected and the form offers to open the existing curriculum.
PRD 14 · Curriculum, lessons and digital resources
Curriculum as an ordered tree (curriculum, unit, lesson) versioned per centre and shared across branches; a course references one curriculum version and a class's sessions are pre-mapped to lessons in order; a per-session teaching log for teachers and a progress report per class; a digital resource library with folders, storage quota and permissions. Files live on FLYER's existing file service.
Key facts, origin and limits
Drafted by an AI assistant in a single session on 16/09/2026, extending the PRD set under the original request quoted verbatim on the hub page. A proposal for team discussion, not a decision of the product team.
Sources
- Public competitor research (PSE ONE, pse4.one), the
center-crm-frontendcodebase and the staging/v3OpenAPI specification (182 paths), as of 10/09/2026. - The existing / extend / new tags on this page come from a comparison with the staging
/v3specification at that date. - A second scan of PSE ONE's public sources on 16/09/2026 (features page, pricing, user-guide articles).
- Competitor reference (pse4.one, public scan on 16/09/2026): the competitor offers a "Quản lý giáo trình" module inside its database administration area; curricula are organised as folders in a digital-resources tab, lessons are created from a "Bài học" resource template with application type HTML and can embed YouTube videos; online teaching is only in the Enterprise tier.
Limits
- To verify in a hands-on survey of the application, because automated login to the reference product was not authorised in the drafting session:
- Whether the competitor has curriculum versioning, or every edit applies immediately to running classes.
- Whether their lessons are bound to specific timetable sessions or only sit in folders for teachers to look up.
- Whether a per-session teaching log exists, and whether teachers write it on mobile or only on the web.
- Storage quota per tier and allowed file types; resource templates other than "Bài học" and application types other than "HTML".
- Whether the student app shows "tài liệu học tập" per session, per class or for the whole curriculum, and whether parents can see untaught lessons.
- Whether the competitor's "Quản trị dữ liệu" right is granted for the whole module or per folder.
Proposed design
The curriculum is an ordered, versioned tree; lessons are pre-mapped to each class session so progress is derived automatically, and the teacher only confirms "Taught" on the phone after each session. The screens below show the state that matters most: a class three lessons behind because a cancelled session has not been shifted and two sessions are unlogged.










1Purpose
A centre runs smoothly when every class at the same level teaches the same content at the same pace, whoever is in front of it. Today most centres keep their curriculum in Word files, Google Drive folders and teachers' memory; a substitute teacher does not know where the class has got to, and academic staff cannot tell whether a class is behind plan. This module brings the curriculum into the system as an ordered tree, binds lessons to sessions the moment a class is created, lets the teacher log the lesson in one minute after each session, and gives managers a per-class progress report.
In scope
- Curriculum as an ordered tree: curriculum → unit → lesson; versioned per centre; shared with all branches or restricted to selected branches.
- Lesson: name, objectives, expected duration, content (rich text, HTML embed, YouTube video, file attachments), teacher material separated from student material, suggested homework linked to existing FLYER assignments.
- Binding a curriculum to a course (PRD 03); on class creation session N is pre-mapped to lesson N; manual re-mapping, skip, catch-up.
- Per-session teaching log for teachers and per-class progress report.
- Digital resource library: folders, file types, storage quota per centre (tied to the storage quota in PRD 19), edit and view permissions, search.
- Parent and student visibility flag per lesson; materials exposed in the parent portal (PRD 07).
Out of scope
- Online learning: online courses, exam preparation, educational games, student accounts attached to online courses. That belongs to PRD 18; this PRD only leaves the hooks (visibility flag, FLYER assignments).
- Authoring and grading tests (already covered by FLYER assignments).
- Printed book stock and issuing physical materials (PRD 08).
- A curriculum marketplace shared between centres (P3 candidate).
2Today
center-crm-frontend has no screen for curriculum or resources. FLYER /v3 already has a "lesson" concept attached to ClassSchedule (the FLYER app study path, used for homework reminders) and a file-upload pattern for CRM customer files.
| Existing capability | Endpoint | Keep? |
|---|---|---|
| FLYER study-path lesson attached to a class schedule (study_unit, book, remind status) | DELETE /v3/teacher/class-schedules/{id}/lessons/{lesson_id} | existing keep; the FLYER lesson is a content source a curriculum lesson may reference, not a replacement entity |
| Assignments given to a class (filter by status, level, skill, date) | GET /v3/teacher/classes/{class_id}/assign-tests | existing keep; source for suggested homework and for homework given in the teaching log |
| File upload for customer records (file_id, file_name, file_type; file stored on the FLYER file service) | GET/POST /v3/crm/customer-files, DELETE /v3/crm/customer-files/{id} | existing upload pattern reused; the resource record is a new entity |
| Curriculum, unit, lesson, version, session-lesson mapping, teaching log, resource library, storage quota | none | new |
Trường Số takes its own direction on two points: the curriculum is a versioned, ordered entity rather than a folder tree, and lessons are bound directly to class sessions so progress is derived without the teacher re-entering it.
3User stories and acceptance criteria
- Given a curriculum version in
draft - When they open a lesson and enter objectives, expected duration, content blocks (rich text, HTML embed, YouTube video, file attachment), mark which blocks are teacher-only, and pick suggested homework from FLYER assignments
- Then content is saved as ordered blocks; files are uploaded to the FLYER file service following the customer-files pattern and a resource record is created in the curriculum's system folder; the YouTube URL is validated; HTML embeds are sanitised per rule 13.
- Given version 1 is published and used by three classes
- When they choose Create new version, edit content, then Publish
- Then version 2 is copied from version 1, becomes
publishedand content-locked; the three running classes stay on version 1; the course has the option "New classes use the latest version"; academic staff can move one class to version 2 with a lesson-code reconciliation, unmatched lessons listed for manual mapping.
- Given a course (PRD 03) referencing a published curriculum version with 24 lessons
- When they create a class from that course with 24 sessions generated from the schedule pattern
- Then session N is mapped to lesson N; the class Curriculum tab shows the session-to-lesson table; if session and lesson counts differ, the remainder is flagged "Chưa gắn bài" or "Bài chưa có buổi" and staff are warned in the class-creation preview. A course without a curriculum still creates the class normally; the Curriculum tab offers "Gắn giáo trình".
- Given session 5 is cancelled for a holiday and a make-up session is appended at the end
- When they open the class Curriculum tab and choose "Dồn bài học xuống" (shift lessons down)
- Then lesson 5 and all later lessons move one session forward, the final make-up session receives lesson 24; the mapping records the action and the actor.
- Given session 12 covered only half of lesson 12, or the teacher finds lesson 9 unsuitable
- When they log "Dạy một phần" (partially taught), or mark lesson 9 as Skipped with a reason
- Then the system offers to attach the rest of lesson 12 to session 13 (session 13 then has two lessons) or to shift everything; a skipped lesson leaves the progress denominator and its session takes the next lesson if shifted, or stays empty for free content.
- Given the session has just ended and the teacher has saved attendance (PRD 04)
- When the post-attendance summary shows the Teaching log section with the mapped lesson pre-filled
- Then the teacher confirms "Đã dạy" with one tap, or switches to "Dạy một phần" / "Chưa dạy" / "Dạy bài khác" (picking a lesson from the curriculum), adds a note and the homework given; the record saves in one request; offline it is queued with the attendance sheet; sessions unlogged for over 24 h are reminded at 21:00 and appear in the academic staff's "Chưa ghi sổ" list.
- Given classes with a curriculum and teaching logs in the branch
- When they open Đào tạo → Tiến độ
- Then each class shows lessons taught over effective lessons, lessons planned to date, the gap, and a chip Đúng tiến độ / Chậm 1–2 bài / Chậm từ 3 bài; filters by branch, curriculum, teacher; xlsx export.
- Given the
resources.managepermission - When they create folders, upload several files at once, and set folder permissions (who edits, who views: by role or named staff, by branch)
- Then files are stored on the FLYER file service; the resource record stores size, type, uploader, folder; the centre's used storage is updated; above 90% of quota the storage bar turns to warning colour, at 100% uploads are refused with a clear message and a link to the storage plan (PRD 19).
- Given a lesson flagged "Hiển thị cho phụ huynh" with student material, and the session's teaching log says "Đã dạy"
- When the guardian opens that session in the parent portal (PRD 07)
- Then they see the lesson name, objectives, student material, the note for parents and the homework given; never teacher material or internal notes; untaught lessons are hidden unless the centre enables "Cho xem trước lộ trình" (preview the path).
4Screens
Tabs: Đang dùng (N) · Bản nháp (N) · Lưu trữ. Filters: search, level, branch. Columns: name and code, level, latest published version (with "nháp v3" if a draft exists), units / lessons / total duration, branches, courses and classes using it, actions (Sửa, Tạo phiên bản mới, Nhân bản, Lưu trữ). Actions: + Tạo giáo trình, Nhập từ Excel (columns: unit, lesson, objectives, duration). Empty state: "Chưa có giáo trình. Tạo giáo trình đầu tiên hoặc nhập từ Excel để gắn bài học vào buổi học của lớp."
Left column is the drag-and-drop tree; right column is the editor for the selected lesson, each block with a "teacher only" toggle. Top bar: version status, classes using it, Lưu, Phát hành, Xem trước như phụ huynh (preview as parent). Empty state for a lesson: "Bài học chưa có nội dung."
Header: bound curriculum and version, progress (taught / effective, gap to plan), buttons Đổi phiên bản and Dồn bài học xuống. Table: session (number, date, time), mapped lesson (pick another from the list), teaching-log status (Chưa ghi / Đã dạy / Một phần / Bỏ qua / Bài khác), logging teacher, short note. Cancelled sessions are struck through and their lesson flagged "Cần dời". Empty state: "Lớp chưa gắn giáo trình" with the action Gắn giáo trình.
Shown directly under the post-attendance summary (PRD 04) and openable on its own. Fields: mapped lesson, completion (Đã dạy / Dạy một phần / Chưa dạy / Dạy bài khác), replacement lesson, internal note, note for parents (optional), homework given (pick from the class's assignments or free text). Sticky Lưu button; editable for 7 days after saving, then teaching_log.edit_any is required.
Filters: branch, curriculum, teacher, class status. Table: class, curriculum and version, main teacher, taught / effective, planned to date, gap, unlogged sessions, progress chip. Selecting a row opens the class Curriculum tab. xlsx export. Empty state: "Chưa có lớp nào gắn giáo trình trong phạm vi lọc."
Left column: folder tree (the system folder "Giáo trình" cannot be deleted; user-created folders). Right column: file table with name, type, size, uploader, date, referencing lessons, actions (Xem trước, Tải xuống, Đổi tên, Di chuyển, Xoá). Storage bar used / quota at the top; a single search box over files and lessons within the caller's view scope; drag and drop to upload. Empty state: "Thư mục trống. Kéo thả tệp hoặc chọn Tải lên."
5Business rules
- A curriculum belongs to the centre. Branch scope is a list of school_id or empty (all branches); a branch's course may only pick a curriculum whose scope includes that branch.
- A curriculum has many versions; at most one
draftat a time. Apublishedversion is immutable; changing content requires a new version. Typo fixes in lesson names are allowed on a published version and audited. - A course references exactly one
curriculum_version_idor the flaguse_latest_published; a class references exactly one version at creation and never switches automatically; new classes never use a draft. - Default mapping on class creation: session N takes lesson N by curriculum-wide
position(ignoring unit boundaries), sessions ordered by start time. The lesson of a cancelled session is flagged "Cần dời" until staff shift or re-map manually. - A session may carry 0, 1 or several lessons; a lesson may span several sessions. Progress is counted in lessons, not sessions: rate = lessons "Đã dạy" (a "Dạy một phần" counts 0.5) / effective lessons (total minus skipped); planned to date = lessons mapped to non-cancelled sessions dated today or earlier; gap = taught minus planned.
- The teaching log may be written only by the session's main or substitute teacher, or by a holder of
teaching_log.edit_any; one record per session, edits keep history; after the 7-day window a reason is required. - Teacher material is never returned by any parent- or student-facing endpoint; enforced at the API layer, not only in the UI.
- The parent visibility flag is off by default; when on, content appears only after the session's teaching log says "Đã dạy" or "Dạy một phần", unless the centre enables "Cho xem trước lộ trình" (PRD 10).
- Allowed files: pdf, doc/docx, ppt/pptx, xls/xlsx, png, jpg, mp3, mp4, zip; 200 MB per file; the centre-wide quota comes from the storage plan (PRD 19), default 5 GB; uploads at quota are refused with
STORAGE_QUOTA_EXCEEDED. - Deleting a file referenced by lessons requires confirmation and lists those lessons; deleted files sit in a 30-day trash before physical deletion and still count towards quota until then.
- Folder permissions inherit from the parent unless overridden; files belonging to a curriculum follow the curriculum's permissions.
- Archiving a curriculum is allowed only when no active class is bound to any of its versions; finished classes keep their reference for history.
- HTML embeds are sanitised with an allowlist: no script, no form, iframes only from domains in the centre's configured list (default youtube.com, docs.google.com, canva.com).
6Data model
| Entity | Fields | Notes |
|---|---|---|
| curriculum | id uuid; center_id; code (unique per centre); name; level_code; description; school_ids uuid[] null (null = all branches); status enum(active, archived); latest_published_version_id null; created_by; created_at | new; tenant scope |
| curriculum_version | id uuid; curriculum_id; version_no int; status enum(draft, published, retired); published_at null; published_by null; source_version_id null; lesson_count; total_duration_min; change_note | unique (curriculum_id, version_no); at most one draft |
| curriculum_unit | id uuid; version_id; position int; name; description | unique (version_id, position) |
| lesson | id uuid; version_id; unit_id; position int (version-wide); lesson_code (stable across versions, used for reconciliation); name; objectives; duration_min; parent_visible bool; parent_note null; suggested_assign_test_ids int64[]; flyer_book_lesson_id null; tags text[] | unique (version_id, position) |
| lesson_block | id uuid; lesson_id; position int; kind enum(rich_text, html_embed, youtube, file); audience enum(all, teacher_only, student); body null; url null; resource_file_id null | audience=teacher_only is never returned to parents |
| course (PRD 03) | + curriculum_id null; curriculum_version_id null; use_latest_published bool | extend |
| class_syllabus | class_id PK; school_id; curriculum_version_id; bound_at; bound_by; effective_lesson_count; taught_count; planned_to_date_count | new; branch scope; counts are cached |
| session_lesson_map | id uuid; class_id; session_id; lesson_id; position_in_session int; status enum(planned, taught, partial, skipped, moved); skip_reason null; source enum(auto, manual, shift); updated_by; updated_at | unique (session_id, lesson_id) |
| teaching_log | id uuid; session_id (unique); class_id; school_id; staff_id; completion enum(taught, partial, not_taught, other_lesson); actual_lesson_id null; note; parent_note null; homework_text null; homework_assign_test_ids int64[]; logged_at; version int | new; history in teaching_log_history (before, after, actor_id, reason) |
| resource_folder | id uuid; center_id; parent_id null; name; kind enum(user, curriculum_system); curriculum_id null; school_ids uuid[] null; permissions jsonb [{principal_type: role|staff, principal_id, access: view|edit}]; created_by | new; system folder bound to a curriculum |
| resource_file | id uuid; center_id; folder_id; flyer_file_id; file_name; mime_type; size_bytes; tags text[]; uploaded_by; uploaded_at; deleted_at null (trash); ref_count int | new; bytes live on the FLYER file service |
| storage_usage | center_id PK; used_bytes; quota_bytes (synced from the PRD 19 storage plan); warned_at null; recalculated_at | cache, recalculated nightly |
| curriculum_policy (tenant settings) | allow_preview_for_parents bool; log_edit_window_days (7); log_reminder_hour (21); max_file_mb (200); allowed_embed_domains text[]; behind_schedule_threshold (3) | stored in centre config (PRD 10) |
7API contract
| Method | Path | Purpose | Status |
|---|---|---|---|
| GET/POST · GET/PATCH/DELETE | /v3/crm/curricula · /v3/crm/curricula/{id} | list filtered by level_code, school_id, status; create with version 1 draft; detail with versions; DELETE = archive, blocked while active classes exist | new |
| POST | /v3/crm/curricula/{id}/versions | new draft copied from the given version; 409 if a draft exists | new |
| GET · POST · PUT | /v3/crm/curriculum-versions/{id}/tree · /publish · /import · /order | full tree (audience filtered by caller role); draft → published; xlsx import via the PRD 11 wizard; {units: [{id, lessons: [id]}]} reorders the whole tree | new |
| POST/PATCH/DELETE | /v3/crm/curriculum-units/{id} · /v3/crm/lessons/{id} | add, edit, delete units and lessons (draft only); lesson PATCH takes the full ordered blocks array | new |
| GET | /v3/teacher/classes/{class_id}/assign-tests | source for suggested homework and homework given in the teaching log | existing |
| DELETE | /v3/teacher/class-schedules/{id}/lessons/{lesson_id} | detach a FLYER lesson from the class schedule when the centre moves to its own curriculum; unchanged | existing |
| GET/PUT | /v3/crm/classes/{class_id}/syllabus | {curriculum_version_id, strategy: sequential | manual} → class_syllabus + session_lesson_map[]; dry_run=true returns a preview with count-mismatch warnings | new |
| PATCH · POST | /v3/crm/classes/{class_id}/syllabus/map · /shift · /rebind | {changes: [{session_id, lesson_ids[]}], skips: [{lesson_id, reason}]}; {from_session_id} shifts lessons; {curriculum_version_id} switches version, reconciles by lesson_code, returns unmatched lessons | new |
| GET/PUT | /v3/crm/sessions/{id}/teaching-log | read and write the teaching log; PUT idempotent by version; 409 STALE_VERSION; reason required after the edit window | new |
| GET | /v3/crm/teaching-logs?school_id&from&to&missing=true | unlogged sessions by teacher | new |
| GET | /v3/crm/curriculum/progress | school_id, curriculum_id, staff_id → progress per class; format=xlsx | new |
| GET/POST · DELETE | /v3/crm/customer-files · /v3/crm/customer-files/{id} | two-step upload pattern (file_id from the file service, then record) reused for resource-files; unchanged | existing |
| GET/POST/PATCH/DELETE | /v3/crm/resource-folders · /v3/crm/resource-folders/{id} | folder tree; folder permissions via PATCH {permissions[]} | new |
| GET/POST · PATCH/DELETE | /v3/crm/resource-files · /v3/crm/resource-files/{id} | list by folder_id, search q; create {folder_id, flyer_file_id, file_name, mime_type, size_bytes, tags}, 413 STORAGE_QUOTA_EXCEEDED; rename, move, trash, restore=true; GET .../download-url returns a 15-minute signed URL | new |
| GET | /v3/crm/storage-usage | used_bytes, quota_bytes, breakdown by folder | new |
| GET | /v3/parent/learner/{id}/sessions/{session_id}/lesson | lesson content for guardians: only blocks with audience all or student, only when parent_visible and taught | new |
| GET/PUT | /v3/teacher/school-config | curriculum_policy block added | extend |
8Permissions
| Permission | owner | branch_manager | academic_admin | teacher | accountant | parent |
|---|---|---|---|---|---|---|
curriculum.view | all | branch | branch | curricula of assigned classes | no | taught lessons of own child (parent-facing blocks only) |
curriculum.manage (create, edit draft, reorder) | yes | branch (curricula scoped to that branch only) | branch | only when granted per curriculum | no | no |
curriculum.publish, curriculum.archive | yes | publish only, branch | no | no | no | no |
syllabus.bind (bind curriculum to course, class; switch version) | yes | branch | branch | no | no | no |
syllabus.remap (re-map, shift, skip) | yes | branch | branch | assigned classes | no | no |
teaching_log.write | yes | branch | branch | assigned sessions | no | no |
teaching_log.edit_any | yes | branch | branch | no | no | no |
curriculum.progress, curriculum.export | yes | branch | branch | assigned classes | no | no |
resources.view | all | branch | branch | granted folders | granted folders | no |
resources.manage (folders, upload, permissions) | yes | branch | branch | folders with edit grant | no | no |
resources.delete | yes | branch | branch | own uploads within 24 h | no | no |
curriculum.policy | yes | no | no | no | no | no |
9Analytics events
| Event | Properties |
|---|---|
| curriculum_created | level_code, branch_scope (all|subset), source (manual|import|duplicate) |
| curriculum_published | version_no, unit_count, lesson_count, total_duration_min, days_in_draft |
| lesson_saved | block_kinds[], has_teacher_material, has_student_material, parent_visible, suggested_assign_test_count |
| syllabus_bound | class_id, strategy, session_count, lesson_count, mismatch |
| syllabus_remapped | action (manual|shift|skip|rebind), affected_sessions, by_role |
| teaching_log_saved | completion, minutes_after_session_end, device, offline_queued, has_parent_note, homework_count |
| teaching_log_edited | days_after_log, by_role, reason_given |
| progress_report_viewed | filters, behind_count, on_track_count, exported |
| resource_uploaded | mime_type, size_bytes, folder_kind, quota_ratio_after |
| resource_quota_warning | ratio (0.9|1.0), used_bytes, quota_bytes |
| parent_lesson_viewed | learner_id, lesson_code, has_student_material, homework_count |
10Edge cases
- A class has 30 sessions but the curriculum has 24 lessons: the last six sessions are "Chưa gắn bài", used for revision or tests; progress is still computed over 24 lessons. The opposite case (20 sessions, 24 lessons): the last four lessons are "Bài chưa có buổi" and the class-creation preview suggests adding sessions or choosing another curriculum.
- A substitute teacher logs "Dạy bài khác": only lessons from the class's own curriculum version can be selected; content outside the curriculum goes in the note.
- Two sessions of one class on the same day: mapping follows start time, not date.
- The curriculum's branch scope is narrowed after a class in the removed branch was bound: the class keeps its reference; that branch can no longer pick this curriculum for new classes.
- A file referenced by lessons in several curricula: one resource_file record with ref_count; deletion is blocked until references are removed or "Xoá và gỡ khỏi N bài học" is chosen.
- A YouTube video is made private after embedding: the block remains, the preview shows "Video không khả dụng"; a weekly check flags broken blocks for staff review.
- A session is cancelled after a teaching log exists: cancelling requires deleting the log first (same rule as the attendance sheet in PRD 04).
- A teaching log saved offline twice: the later
client_saved_atwins, as in the attendance queue.
11Open questions
- Should teachers be able to create personal curricula (not published to the centre) for private tutoring classes? Proposal: not in P1; teachers use free content through the teaching-log note.
- Should a unit carry its own objectives and end-of-unit test, or stay at lesson level? Proposal: a unit is only a named group in P1.
- Is counting "Dạy một phần" as 0.5 of a lesson acceptable to managers, or is a percentage needed?
- Should FLYER lessons (flyer_book_lesson_id) be auto-suggested by level while authoring, and who maintains that mapping?
Trường Số (truongso.net) · FLYER internal