- Given I open Điểm danh on my phone at or after session start (or up to 30 min before)
- When the session sheet loads with every rostered student defaulted to Có mặt
- Then I tap only the exceptions (Muộn asks minutes with presets 5/10/15/30; Vắng; Có phép when an excuse already exists is preselected), add an optional class note, and press Lưu; the sheet saves with one request; if offline, the sheet is queued locally and synced when online, with a visible "Chưa đồng bộ" banner.
PRD 04 · Attendance
Session attendance with four statuses (present, late, absent, excused), a phone-first teacher flow, admin correction with audit, guardian notification through Zalo, and the link from attendance to package balance and teacher pay. No attendance entity exists in the backend today.
Key facts, origin and limits
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-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.
Limits
- Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
- Competitor's attendance statuses and whether "muộn" exists.
- Whether absence deducts a package session by default and whether the setting is per center.
- How teachers mark on mobile (app or web) and whether an edit window exists.
- What the parent receives (app push, Zalo, SMS) and its template text.
- Whether "đánh dấu ghi chú học sinh nghỉ học" is free text or reasons.
Proposed design
Three mobile screens for the teacher (today's sessions, the attendance sheet, the saved sheet) and five desktop screens for administrators. The one thing to see is the four-state control Có mặt · Muộn · Vắng · Phép on every student row, defaulting to present with 44 px targets: the teacher only taps the exceptions and presses Save.








1Purpose
Attendance is the transaction that connects the timetable to money and to parents. Each held session produces one record per rostered student; those records reduce session packages, prove teacher hours for pay, and trigger the message a guardian expects within minutes of the class starting. The flow must take a teacher under one minute on a phone in a classroom with poor connectivity.
In scope
- Marking attendance per session for enrolled, trial and make-up students.
- Statuses present, late, absent, excused; minutes late; session note per class and per student.
- Teacher mobile flow (PWA) and admin desktop flow; corrections with audit.
- Automatic guardian notification per status (PRD 07) and absence follow-up tasks.
- Attendance reports: by class, by student, by branch; export.
Out of scope
- Biometric or QR check-in devices (P3 candidate).
- Homework completion (FLYER assignments already track it).
2Statuses
| Code | VI label | Chip | Consumes package session | Notifies guardian | Counts in attendance rate |
|---|---|---|---|---|---|
present | Có mặt | Có mặt | yes | optional (tenant setting, default off) | present |
late | Muộn | Muộn | yes | yes, with minutes | present |
absent | Vắng | Vắng | yes (tenant setting "vắng không phép vẫn trừ buổi", default on) | yes, immediately | absent |
excused | Có phép | Có phép | no (default) ; make-up offered | no (guardian informed in advance) | excluded |
unmarked | Chưa điểm danh | grey | no | no | excluded until marked |
Colours follow the Cloud status tokens: ok #16A34A on #DCFCE7, late #D97706 on #FEF3C7, abs #DC2626 on #FEE2E2, exc #2563EB on #DBEAFE. Every chip carries its text; colour is never the only signal.
3User stories and acceptance criteria
- Given a guardian informs the desk (call, Zalo, portal request)
- When I add an excuse on the student for a session or a date range with reason
- Then those sessions show Có phép pre-marked on the teacher sheet; the guardian receives confirmation; a make-up suggestion appears if policy allows.
- Given attendance saved with a student marked absent and the guardian has a Zalo consent
- When the save completes
- Then within 2 minutes a ZNS message "Học viên [tên] vắng buổi [lớp] ngày [dd/MM] lúc [HH:mm]. Vui lòng liên hệ [chi nhánh] [SĐT]." is sent; if Zalo fails, in-app then email fallback; delivery status is visible on the session sheet.
- Given a saved sheet
- When I change a student's status after the session day with a reason
- Then the record updates, an audit entry stores before/after, package balance recalculates, and if the change moves absent to present a "correction" message is optionally sent.
- Given sessions ended more than 2 hours ago with no sheet saved
- When I open Điểm danh → Chưa điểm danh
- Then they are listed by teacher with "Nhắc GV" (in-app push) and "Điểm danh thay"; at 21:00 daily the teacher gets a reminder for any unmarked session of the day; the branch manager gets a digest of sessions unmarked for more than 24 h.
- Given a student absent 2 consecutive sessions or 3 in 30 days (tenant thresholds)
- When the second/third absence is saved
- Then a task "Liên hệ phụ huynh: [tên] vắng N buổi" is created for the student's care owner (branch default), shown in the today plan; the student list tab "Cần quan tâm" includes them.
- Given a session with a trial booking and a make-up booking
- When I open the sheet
- Then those students appear at the bottom with Học thử / Học bù chips and a note field; marking present updates the booking (PRD 01, PRD 03) and, for trials, prompts a 3-option quick note (Phù hợp / Cần lớp thấp hơn / Cần lớp cao hơn) sent to the consultant.
- Given a date range and branch
- When I open the attendance report
- Then I see rate by class (present+late over marked), late minutes distribution, absences by reason, unmarked sessions by teacher, and a student list sortable by absences; exportable to xlsx.
4Screens
List of my sessions today with time, class, room, roster count and a status chip (Chưa điểm danh / Đã điểm danh 11/12 / Đã lưu). Tap opens the sheet. Yesterday's unmarked sessions are pinned on top in amber. Bottom tab bar: Hôm nay · Lịch tuần · Lớp của tôi · Tài khoản.
Rows: avatar, name, flags (Cần lưu ý, low balance, birthday today), segmented control of four statuses (44 px tall targets), per-student note icon. Header counters update live. Save button sticky. After save: summary sheet with delivery status of notifications and "Sửa" for 24 h without reason (teacher), later with reason (admin).
States: loading skeleton; empty roster "Lớp chưa có học viên"; offline banner; conflict "Buổi này đã được [tên] điểm danh lúc HH:mm" with option to view.
Tabs: Hôm nay · Chưa điểm danh · Theo lớp · Theo học viên · Báo cáo. Hôm nay: table of today's sessions (time, class, room, teacher, roster, marked, present/late/absent/excused counts, notifications sent) with action Mở phiếu. Filters: branch, teacher, class.
Month calendar with a chip per session, rate for 30/90 days, table of absences (date, class, status, reason, make-up booked), + Thêm phép.
Fields: session(s) (select from upcoming sessions or date range), reason (Ốm / Việc gia đình / Đi học trường / Khác + text), reported by (guardian name), channel (Zalo, gọi điện, trực tiếp, cổng PH), offer make-up toggle.
5Business rules
- A sheet exists per session; records exist only for rostered students (enrolled on that date, trial, make-up). Students enrolled after the date are not retro-added.
- Default status on open is present; saving with all-present is valid and common.
- Late requires minutes (1–120); late beyond a tenant threshold (default 45 min) is still late, not absent.
- Teachers (main or substitute of the session) may mark and edit within 24 h without a reason; after that, edits need
attendance.edit_anyand a reason. - Package consumption: present and late always consume; absent consumes per tenant setting; excused never consumes; make-up consumes per booking flag.
- Notification rules per status are tenant settings with defaults in section 2; quiet hours 21:30–07:00 hold messages until morning except absence during evening classes, which send immediately (parents expect it).
- Attendance rate = (present + late) / (present + late + absent) over marked sessions.
- A cancelled session has no sheet; cancelling a session with a saved sheet requires deleting the sheet first (admin only, audited).
6Data model
| Entity | Fields | Notes |
|---|---|---|
| attendance_sheet | id uuid; session_id (unique); school_id; class_id; marked_by_staff_id; marked_at; note; notify_guardians bool; present_count; late_count; absent_count; excused_count; version int | new |
| attendance_record | id uuid; sheet_id; session_id; student_id; roster_type enum(enrolled, trial, makeup); status enum(present, late, absent, excused); late_minutes int null; note; excuse_id null; consumed_session bool; notified_at null; notify_status enum(sent, failed, skipped) null; updated_by; updated_at | unique (session_id, student_id) |
| attendance_record_history | id; record_id; from_status; to_status; reason; actor_id; created_at | audit detail |
| excuse | id; student_id; session_id null; date_from; date_to; reason_code enum(sick, family, school, other); reason_text; reported_by; channel; offer_makeup bool; created_by; created_at | new |
| attendance_policy (tenant settings) | absent_consumes bool; late_threshold_min; edit_window_hours; consecutive_absent_threshold; monthly_absent_threshold; notify: {present, late, absent, excused, correction}; quiet_hours | stored in school config |
7API contract
| Method | Path | Request / response | Status |
|---|---|---|---|
| GET | /v3/school/sessions/{id}/attendance | → {session, sheet | null, roster: [{student, roster_type, flags, balance, prefilled_status, excuse}]} | new |
| PUT | /v3/school/sessions/{id}/attendance | {records: [{student_id, status, late_minutes?, note?}], note?, notify_guardians, version?, client_saved_at?} → sheet with records and notify statuses; 409 STALE_VERSION; idempotent by (session_id, client_saved_at) | new |
| PATCH | /v3/school/attendance/records/{id} | {status, late_minutes?, note?, reason} (reason required after edit window) | new |
| DELETE | /v3/school/sessions/{id}/attendance | admin, audited | new |
| GET | /v3/school/attendance/sessions | school_id, date_from, date_to, class_id, staff_id, marked=false → sessions with counts | new |
| POST | /v3/school/attendance/sessions/{id}/remind-teacher | in-app push | new |
| GET/POST/DELETE | /v3/school/students/{id}/excuses | {session_id | date_from, date_to, reason_code, reason_text, reported_by, channel, offer_makeup} | new |
| GET | /v3/school/students/{id}/attendance | from, to → records + rate | new |
| GET | /v3/school/attendance/report | school_id, from, to, group_by=class|student|teacher → aggregates; format=xlsx for export | new |
| GET | /v3/school/me/sessions?date= | teacher's sessions with sheet status | new |
| GET/PUT | /v3/teacher/school-config | attendance_policy block added | extend |
| GET | /v3/parent/learner/{id}/attendance | guardian view | new |
8Permissions
| Code | owner | branch_manager | academic_admin | teacher | accountant | parent |
|---|---|---|---|---|---|---|
attendance.view | all | branch | branch | own sessions | branch read | own children |
attendance.mark | yes | yes | yes | own sessions | no | no |
attendance.edit_any | yes | yes | yes | no | no | no |
attendance.delete_sheet | yes | yes | no | no | no | no |
excuses.manage | yes | yes | yes | no | no | request only |
attendance.report, attendance.export | yes | branch | branch | own classes | report | no |
attendance.policy | yes | no | no | no | no | no |
9Analytics events
| Event | Properties |
|---|---|
| attendance_sheet_opened | device (mobile|desktop), minutes_from_start, roster_size |
| attendance_saved | seconds_to_save, counts, offline_queued, notify |
| attendance_record_edited | hours_after_session, by_role, from, to |
| attendance_notification_sent | status, channel, outcome |
| excuse_created | reason_code, channel, offer_makeup |
| absence_followup_task_created | trigger (consecutive|monthly) |
| unmarked_reminder_sent | hours_late |
10Edge cases
- Two teachers (main and substitute) open the same sheet: second save with stale version gets 409 and sees the saved sheet; the substitute is the expected marker when assigned.
- Student transferred out on the session date: still on roster for that date if enrollment end ≥ date.
- Guardian without any consent: notification skipped with reason shown as "Chưa có kênh liên lạc"; desk sees it in the follow-up list.
- Sheet saved before session start (teacher pre-marks): allowed from 30 min before; records timestamp the save.
- Class note contains a student's name: fine, notes are staff-visible only; per-student notes are never sent to guardians automatically.
- Offline queue with two saves: last save wins by
client_saved_atordering. - Trial student absent: booking becomes no_show; consultant notified.
11Open questions
- Should present notifications be on by default for kindergarten-age centers (parents like "đã vào lớp")?
- Do teachers need a "leave early" status? Proposed: no; note field covers it.
- Is a printable paper sheet still needed for teachers without phones?
Trường Số (truongso.net) · FLYER internal