- Given the student record or the Team work screen is open
- When the manager chooses "+ Giao việc", enters title, assignees (one or many), due date, priority, description and an optional checklist
- Then the task is created in status Mới, linked to the student; each assignee receives an in-app notification and a push within 1 minute; the task appears in the assignee's My work and in the Công việc tab of the student record.
PRD 16 · Internal tasks, reminders and leave requests
One task entity shared by all staff, linkable to a customer, student, class, session, invoice or incident; recurring tasks for operational routines; system-generated tasks with dedupe rules; a notification centre and an 08:00 digest; staff and teacher leave requests approved per branch; incident reports from the teacher app. Today the backend only has customer follow-up tasks and the personal notification endpoints.
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 of 16/09/2026): the pricing page lists task assignment, notifications, leave requests and automatic class reminders in every tier; marketing stresses "smart notifications and reminders" in the mobile app; the teacher app has incident reporting. Their presence in the lowest tier shows customers treat them as baseline.
Limits
- Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
- Whether the competitor's tasks link to a student, class or invoice or are a free-standing to-do list; whether checklists and comments exist.
- Whether the competitor has recurring and system-generated tasks, and how duplicates are handled when the same condition repeats.
- Whether the competitor's leave requests are for staff, students or both; the leave types; who approves; whether an approved request automatically flags sessions for a substitute.
- Which types the competitor's "smart notifications and reminders" cover, whether a daily digest and quiet hours exist; whether incident reporting has photos, severity and routing.
Proposed design
All branch operations run through one Tasks area with five tabs (My work · Team work · Routines · Leave requests · Incidents); the most important thing to see is that every task row states its source (manual, routine, system) and its linked entity, while leave requests and incidents are simply two more ways of generating tasks into the same list.


session_unmarked trigger sits on top. Long-pressing a row opens a bottom sheet with exactly the three row actions (Hoàn thành, Hoãn đến ngày mai, Mở chi tiết).








1Purpose
Most of the work of running a centre belongs to no single module: calling a parent back, finding a substitute teacher, closing the cash drawer, counting stock on Monday, fixing a broken projector. Today these live in Zalo groups, notebooks or spreadsheets, and nobody knows which are done. This module introduces one task entity that any staff member can be assigned, that every other module can generate, and that everyone sees in a single "My work" screen. The same reminder and notification machinery serves staff leave requests and incident reports from the classroom.
In scope
- Generic tasks: assigned to one or many staff, optionally linked to a customer, student, class, session, invoice or incident; checklist, attachments, comments, priority, due date, five statuses.
- Daily or weekly recurring tasks (cash close, stock count); system-generated tasks from other modules (cold lead, student absent three sessions, overdue invoice, unmarked session, substitute needed) with dedupe rules.
- Per-task reminders, an 08:00 digest, the in-app notification centre (existing endpoints), push through the mobile app (PRD 17) and email.
- Leave requests for staff and teachers approved per branch; affected sessions are flagged for a substitute (PRD 03) and excluded from timesheets (PRD 06).
- Incident reports from the teacher app, routed to the branch manager; "My work" and "Team work" screens.
Out of scope
- Class reminders, fee reminders and any parent-facing notification: specified in PRD 07.
- Real-time internal chat; project management; statutory annual-leave balance (the module only counts days taken per type).
- Student excuses and pauses (PRD 02, PRD 04); technical incidents of the Trường Số platform (PRD 12).
2What exists today
| Capability | Endpoint | Keep? |
|---|---|---|
| Today plan: tasks, calls and appointments linked to customers, dashboard widget | GET /v3/crm/me/today-plan | existing keep; its data source widens to the new task entity |
| Customer follow-up tasks: an interaction type (Task) on the timeline with due date, assignee, status | GET /v3/crm/customers/{id}/interactions | extend migrate Task-type interactions into customer-linked tasks |
| Personal notification centre: list, unread count, read, read all, archive | GET /v3/crm/me/notifications, GET .../unread-count, POST .../{id}/read, POST .../read-all, POST .../{id}/archive | existing keep; new notification types added, contract unchanged |
| Class reminder to parents and send-time preview | POST /v3/teacher/class-schedules/{id}/remind, POST /v3/teacher/class-schedules/preview-remind-time | existing belongs to PRD 07; unchanged |
| Shared tasks, recurring tasks, system-generated tasks, leave requests, incident reports | none | new |
Proposed decision: separate tasks from customer interactions. Task-type interactions are migrated once into task with source = migrated and linked_type = customer, keeping assignee and due date; interactions keep only calls, meetings and notes; the customer timeline additionally reads from the task table.
3User stories and acceptance criteria
- Given a task with a three-item checklist and the "requires review" flag on
- When the assignee ticks the checklist items, attaches a photo and chooses "Hoàn thành"
- Then the status becomes Chờ xác nhận and the assigner is notified; the assigner chooses "Xác nhận" to move it to Hoàn thành, or "Trả lại" with a comment to return it to Đang làm; when the flag is off, "Hoàn thành" moves straight to Hoàn thành.
- Given a routine "Chốt quỹ chi nhánh": daily except Sunday, generation time 16:00, due 18:00, assignee the accountant role at the branch
- When the generation time arrives
- Then a new task is created for each active branch and assigned to the staff holding the accountant role there; if the previous period's task is not done, the new one is still created and the old one shows as overdue; the routine can be paused, and editing it only affects future generations.
- Given the student already has an open task "Liên hệ phụ huynh: vắng 3 buổi" triggered by PRD 04
- When the student is absent a fourth time before that task is done
- Then no new task is created; the existing task receives a system comment "Vắng thêm buổi ngày dd/MM, tổng 4 buổi", its priority rises one step and the assignee is reminded again; if the task was completed within the last 7 days, no new task is created either.
- Given a user has 2 overdue tasks, 3 due today, 1 awaiting their review and 1 leave request awaiting their approval
- When the clock reaches 08:00 in the centre's time zone
- Then a single digest notification is created in the notification centre and pushed, listing the four groups with counts and the first three items of each; a digest email is sent if the user enabled it; per-task reminders (default 09:00 on the due date) keep working independently; selecting a notification marks it read through the existing endpoint and opens the right task.
- Given the teacher opens Đơn xin nghỉ in the mobile app
- When they choose type Nghỉ phép, a date range and a reason, and submit
- Then the system lists the sessions where the teacher is main or substitute teacher in that range before submission; the request is created as Chờ duyệt, the approver is the branch manager of the teacher's primary branch and is notified; the teacher can cancel while it is still Chờ duyệt.
- Given a pending request affecting 4 sessions
- When the approver chooses "Duyệt" with an optional note
- Then the request becomes Đã duyệt; each affected session is flagged "Cần dạy thay" (PRD 03) and a task "Phân công dạy thay: [lớp] [ngày]" is created for the branch's academic admin; sessions in the leave period stay out of the absent teacher's timesheet (PRD 06); the teacher is notified of the outcome; on "Từ chối" a reason is mandatory and sessions are untouched.
- Given the teacher is in a session and opens "Báo cáo sự cố" from the session screen
- When they choose type Cơ sở vật chất, severity Trung bình, take two photos, type a short description and submit
- Then the incident is created with branch, room and session pre-filled; a task "Xử lý sự cố: [tiêu đề]" is created for the branch manager with matching priority; a critical incident pushes immediately to the branch manager and the owner; the teacher follows the status in Sự cố của tôi.
4Screens
Groups in order: Quá hạn · Hôm nay · Chờ xác nhận của tôi · Tuần này · Sau đó. Each row: done checkbox, title, linked entity (chip with icon and name), due, priority, source (Giao tay / Lặp / Hệ thống), unread comment count. Filters: branch, source, linked entity. Row actions: Hoàn thành, Hoãn đến ngày mai, open detail. Empty state: "Không có việc nào đến hạn. Việc được giao và việc hệ thống sinh ra sẽ xuất hiện tại đây." On mobile the teacher's tab bar gains a Việc item.
Table: title, assignees, assigner, linked entity, due, priority, status, source, last update. Filters: branch, assignee, status, priority, source, overdue. Side panel: per-staff summary (open, overdue, done in 7 days). Multi-select: Giao lại, Đổi hạn, Đóng with reason. Routines page: name, recurrence (daily, weekdays, weekly on day N), generation time, due after, assignee (staff or a role at the branch), branch scope, last generated, Đang chạy / Tạm dừng; a new tenant gets three suggested routines: Chốt quỹ (daily), Kiểm kho (Monday), Xác nhận lịch tuần tới (Friday). Empty state: "Chi nhánh chưa có công việc nào. Tạo mẫu công việc lặp để bắt đầu."
Header: inline-editable title, source, priority, due, status selector, assignees. Body: linked entity (link opens the right record), description, checklist, attachments, reminders (default 09:00 on the due date, + Nhắc button). Comments are a timeline of user comments (with @name mentions) and system comments (status changes, reassignment, escalation); the change log is reachable through "Lịch sử". Sticky action bar: Hoàn thành · Giao lại · Huỷ.
Two tabs: Đơn của tôi and Chờ tôi duyệt (only for users with approval rights). Table: requester, type (Nghỉ phép / Nghỉ ốm / Nghỉ không lương / Đổi ca), from and to, days, affected sessions, status, approver, submitted at. Form: type, period (full day or morning/afternoon/evening), reason, attachment (medical note); for a shift swap: the session and proposed receiving teacher. Before submission the affected sessions are listed. Approvers see Duyệt / Từ chối with a note field. Empty state: "Chưa có đơn xin nghỉ nào."
Desktop: table per branch with title, type (Cơ sở vật chất / An toàn học viên / Hành vi / Thiết bị / Khác), severity (Thấp / Trung bình / Cao / Nghiêm trọng), room, reporter, time, status (Mới / Đang xử lý / Đã xử lý / Đóng), linked task. Mobile: one screen with type and severity (large chips), photos (max 5, compressed client-side), description; branch, room and session pre-filled from context; can be submitted offline and synced later. Empty state: "Chưa có sự cố nào trong 30 ngày."
Panel opened from the bell, filter chips: Tất cả · Công việc · Đơn xin nghỉ · Sự cố · Hệ thống. Each item: type icon, title, subtitle, relative time, unread mark. Actions: select to open, Đọc tất cả, per-item Lưu trữ; a "Cài đặt thông báo" link opens channel preferences per type (in-app always on; push and email toggleable). Empty state: "Không có thông báo mới."
5Business rules
- Valid status transitions: new → in_progress; in_progress → pending_review (when requires review is on) or done; pending_review → done or in_progress; any → cancelled with a reason. Done or cancelled tasks are not reopened.
- A task has at least one assignee. With several assignees the task is done when any of them completes it (default) or when all do (flag
require_all). An assignee is a specific staff member or a role at a branch; for a role the system assigns every active staff member holding that role at creation time. - Priority shares the lead scale of PRD 01. Overdue is not a status but the condition
due_at < nowon a task that is not done or cancelled. - Recurring tasks are generated in the centre's time zone, skipping branch holidays (PRD 03) when the routine has that flag on. The pair (
routine_id,period_key) is unique, so a period is never generated twice; editing a routine only affects future generations. - System tasks carry
dedupe_key= trigger_code plus linked entity. When an open task with the same dedupe_key exists, the system creates none, adds a system comment, raises priority one step (up to urgent) and re-reminds. After completion the dedupe_key is held for 7 days (tenant setting, 0 to 30) before regeneration. - P1 triggers and default assignees:
lead_cold(no interaction for 7 days; the lead's assignee),student_absent(thresholds per PRD 04; the student-care owner at the branch),invoice_overdue(7 days overdue; branch accountant),session_unmarked(ended more than 2 hours ago; the session's teacher, escalating to the branch manager after 24 hours),substitute_needed(branch academic admin),incident_new(branch manager). Each trigger can be switched off per tenant. - Per-task reminders default to 09:00 on the due date, up to 3 more can be added; they go only to assignees of tasks that are not closed. The 08:00 digest is one
daily_digestnotification per user with at least one item, replaced if one already exists for the day. Push respects the quiet hours 21:30 to 07:00 (shared setting with PRD 04); in-app notifications are never held and auto-archive after 90 days. - The notification centre keeps the existing endpoint contract; new types are added to the
typeenum, and the payload always carriesentity_typeandentity_idso the UI opens the right context. - The requester may cancel a leave request only while pending; the approver may revoke an approved request before its start date with a reason, and substitute flags are removed if no substitute is assigned yet. A request overlapping another request of the same staff member is rejected at creation.
- The default approver is the branch manager of the requester's primary branch (PRD 06); when the requester is a branch manager the approver is the owner; the tenant may set an alternate approver per branch. A request untouched for 48 hours is escalated to the owner by notification; the system never auto-approves.
- On approval every session in the leave period where the requester is main or substitute teacher gets
needs_substitute = true; PRD 03 handles assignment; those sessions produce no timesheet line for the absent teacher (PRD 06) unless that teacher actually marks attendance (attendance wins over the leave and a warning is logged). An approved shift swap creates the substitute assignment for the receiving teacher immediately. - Each incident spawns exactly one handling task; closing the task moves the incident to resolved, the reporter confirms or it auto-closes after 3 days. Student-safety or critical incidents push immediately to the branch manager and owner, bypassing quiet hours.
- Every entity belongs to one centre (
center_id) and one branch (school_id); a task without an explicit branch takes the creator's branch. A user sees only tasks where they are assigner, assignee or mentioned, or that fall within a branch in their permission scope. Attachments use the CRM file service: at most 10 files per task, 20 MB each.
6Data model
| Entity | Fields | Notes |
|---|---|---|
| task | id uuid; center_id; school_id; title; description; source enum(manual, routine, system, migrated); trigger_code null; dedupe_key null (unique while open); routine_id null; period_key null; linked_type enum(customer, student, class, session, invoice, incident) null; linked_id null; created_by_staff_id; priority enum(low, medium, high, critical, urgent); status enum(new, in_progress, pending_review, done, cancelled); require_review bool; require_all bool; due_at; completed_at; cancelled_reason; version int | new; indexes (center_id, school_id, status, due_at) and (linked_type, linked_id); audit detail in task_history |
| task_assignee | task_id; staff_id; assigned_by_rule enum(direct, role); role_code null; state enum(open, done); done_at; last_reminded_at | primary key (task_id, staff_id) |
| task_checklist_item · task_attachment · task_reminder | checklist: id; task_id; position; label; checked; checked_by. attachment: id; task_id; file_id; uploaded_by. reminder: id; task_id; kind enum(relative, absolute); offset_minutes null; fire_at; fired_at null | max 30 items, 10 files, 4 reminders |
| task_comment | id uuid; task_id; author_staff_id null (null means system); body; kind enum(user, system); mentions staff_id[]; created_at | new |
| task_routine | id; center_id; school_ids[] (empty means all branches); name; title_template; checklist_template json; priority; require_review; recurrence enum(daily, weekdays, weekly); weekday null; generate_time; due_after_minutes; skip_holidays bool; assignee_rule {staff_ids[] | role_code}; is_paused; last_generated_at | new |
| notification (existing) · notification_preference | type enum extended: task_assigned, task_due, task_overdue, task_commented, task_review_requested, task_returned, task_completed, leave_submitted, leave_decided, leave_escalated, incident_reported, incident_updated, daily_digest; payload {entity_type, entity_id, title, body}. preference: staff_id; type; push bool; email bool | extend enum and payload only; preference is new |
| leave_request | id uuid; center_id; school_id; staff_id; type enum(annual, sick, unpaid, shift_swap); date_from; date_to; part_of_day enum(full, morning, afternoon, evening); session_id null; swap_with_staff_id null; reason; attachment_file_id null; status enum(pending, approved, rejected, cancelled); approver_staff_id; decided_at; decision_note; escalated_at null | new; leave_request_session (leave_request_id; session_id; substitute_task_id null) computed at submission, recomputed at decision |
| incident · incident_attachment | id uuid; center_id; school_id; room_id null; session_id null; class_id null; reported_by_staff_id; type enum(facility, student_safety, behaviour, equipment, other); severity enum(low, medium, high, critical); title; description; status enum(new, in_progress, resolved, closed); task_id; resolved_at; closed_at. attachment: id; incident_id; file_id | new; max 5 photos, compressed to 1,600 px on the long edge |
| task_policy (tenant settings) | dedupe_window_days (7); digest_time (08:00); default_reminder_time (09:00); triggers {lead_cold: {enabled, days}, student_absent, invoice_overdue: {enabled, days}, session_unmarked, substitute_needed, incident_new}; leave_approver_overrides [{school_id, staff_id}]; leave_escalation_hours (48); incident_autoclose_days (3) | stored in centre settings (PRD 10) |
7API contract
| Method | Path | Request / response | Status |
|---|---|---|---|
| GET | /v3/crm/me/today-plan | → adds a tasks array (overdue, today, awaiting review) next to the existing calls and appointments | existing |
| GET · POST | /v3/crm/me/notifications · /unread-count · /{id}/read · /read-all · /{id}/archive | contract unchanged; optional type? filter added | existing |
| POST | /v3/teacher/class-schedules/{id}/remind · /preview-remind-time | class reminders to parents, PRD 07; reference only | existing |
| GET | /v3/crm/customers/{id}/interactions | additionally returns customer-linked tasks as timeline items kind = task (read-only); task creation uses /v3/crm/tasks | extend |
| GET / POST | /v3/crm/tasks | view=my|team, school_id, assignee_id, status[], priority[], source, linked_type, linked_id, overdue, due_from, due_to, q; POST {title, description?, assignees: [{staff_id} | {role_code, school_id}], due_at?, priority?, require_review?, require_all?, linked?: {type, id}, checklist?, reminders?, attachments?} | new |
| GET / PATCH | /v3/crm/tasks/{id} | detail with checklist, attachments, reminders, comments, history; PATCH with version, 409 STALE_VERSION | new |
| POST | /v3/crm/tasks/{id}/transition · /reassign · /v3/crm/tasks/bulk | {to, reason?} validated per section 5; {assignees[], note?}; bulk {ids[], action: reassign|reschedule|cancel, payload} | new |
| PUT · GET/POST · POST/DELETE | /v3/crm/tasks/{id}/checklist · /comments · /reminders | checklist replaces the whole list; comment {body, mentions?} notifies mentioned staff; reminder {kind, offset_minutes | fire_at} | new |
| GET / POST / PATCH / DELETE | /v3/crm/task-routines · /{id} | fields per section 6; PATCH {is_paused}; POST /{id}/run-now generates one period immediately | new |
| GET / PUT | /v3/crm/me/notification-preferences | [{type, push, email}] | new |
| GET / POST | /v3/crm/leave-requests | view=mine|to_approve|all, school_id, staff_id, status[], from, to; POST {type, date_from, date_to, part_of_day, session_id?, swap_with_staff_id?, reason, attachment_file_id?} → with affected_sessions[]; POST /preview-impact returns affected_sessions[] before submission | new |
| POST | /v3/crm/leave-requests/{id}/decide · /cancel · /revoke | decide {decision: approved|rejected, note?} (note required when rejected) flags sessions and creates substitute tasks; cancel by requester while pending; revoke by approver before start date {reason} | new |
| GET / POST | /v3/crm/incidents | school_id, status[], severity[], type[], from, to; POST {type, severity, title, description, room_id?, session_id?, class_id?, attachments[], client_created_at?} idempotent by (reported_by, client_created_at) | new |
| GET / PATCH | /v3/crm/incidents/{id} | detail with task; PATCH {status, severity?, note?}; POST /{id}/confirm-resolved by the reporter | new |
| GET / PUT | /v3/teacher/school-config | task_policy block added | extend |
| GET | /v3/crm/tasks/report | school_id, from, to, group_by=assignee|source|trigger → created, done, on-time, median completion time; consumed by PRD 09 | new |
8Permissions
| Code | owner | branch_manager | admissions | academic_admin | accountant | teacher |
|---|---|---|---|---|---|---|
tasks.view | all | branch | own + mentioned | branch | own + invoice-linked in branch | own + linked to own classes |
tasks.create | yes | yes | yes (linked to own customers) | yes | yes | yes (linked to own classes, sessions) |
tasks.assign_others, tasks.edit_any, tasks.bulk, tasks.team_view | all | branch | no (self only) | branch | no | no |
tasks.routines | yes | branch | no | no | no | no |
leave.request, incidents.report | yes | yes | yes | yes | yes | yes |
leave.approve | all | branch | no | no | no | no |
leave.view | all | branch | own | branch (read, for substitutes) | branch (read, for pay) | own |
incidents.manage | all | branch | no | branch | no | own (confirm resolved) |
tasks.policy | yes | no | no | no | no | no |
tasks.report | all | branch | no | branch | no | no |
Parents have no permission in this module; parent requests go through PRD 07 and become front-desk tasks through a system trigger.
9Analytics events
| Event | Properties |
|---|---|
| task_created | source (manual|routine|system|migrated), trigger_code, linked_type, assignee_count, has_due, has_checklist, by_role, device |
| task_transitioned | from, to, hours_open, was_overdue, by_role, source |
| task_deduped | trigger_code, priority_before, priority_after |
| reminder_sent | kind (task|digest), channel (in_app|push|email), quiet_hours_deferred |
| notification_opened | type, minutes_after_send, device |
| routine_generated | routine_id, recurrence, schools_count, previous_period_incomplete |
| leave_submitted | type, days, part_of_day, affected_sessions, device |
| leave_decided | decision, hours_to_decide, escalated, affected_sessions |
| incident_reported | type, severity, photos, offline_queued, seconds_to_submit |
| incident_resolved | hours_to_resolve, confirmed_by_reporter, autoclosed |
10Edge cases
- An assignee is deactivated with open tasks: the tasks move to the branch manager with a system comment; role-based routines pick the replacement staff at the next period.
- The linked entity is archived or anonymised (PRD 02, PRD 12): the task keeps title and status, the link reads "Đối tượng đã lưu trữ"; an open system task is auto-cancelled.
- Two users edit the same task: the later save with a stale
versiongets 409 and sees the newer copy; comments are not locked by version. - A student transfers branch while a linked task is open: the task keeps its original branch and assignee; the new branch's manager sees it through the student link and may reassign.
- An approved leave, yet the teacher marks attendance for a session in the period: attendance is accepted, the session is paid to that teacher, the substitute flag is removed, and a warning comment is written on the request.
- The branch's only approver is on approved leave: new requests from that branch go straight to the owner during the leave period.
- An incident submitted twice offline through retries: deduplicated by (reported_by, client_created_at); photos are attached once.
- Trigger
lead_coldon a customer flagged Do not call (PRD 01): no task is generated. - A user belonging to several branches: My work merges all branches with a branch chip per row; the 08:00 digest is sent once.
11Open questions
- PRD 06 placed "leave request and approval" in phase 3; this PRD proposes pulling it into P2 because it depends directly on substitutes and timesheets. The product owner needs to decide and update the phasing section of PRD 00.
- Annual leave balance: only count days taken (proposed) or hold a manual opening balance on the staff record and warn when exceeded?
- Is the 08:00 digest emailed by default or only when the user enables it? Proposed: off by default for teachers, on for branch managers and accountants.
- Should teachers see colleagues' leave requests in their branch (to volunteer as substitutes), or only the "Cần dạy thay" sessions in PRD 03?
Trường Số (truongso.net) · FLYER internal