- Given a second successful login on a mobile browser where the app is not installed
- When the shell home screen renders
- Then a card "Thêm Trường Số vào màn hình chính" appears; on Android and desktop it opens the browser install dialog; on iOS it opens an illustrated three-step guide; the card can be hidden for 14 days; once installed, the app opens in standalone mode with a Cloud-theme icon and splash screen.
PRD 17 · Mobile applications
One codebase for the Trường Số mobile experience with four role-specific shells (manager, teacher, consultant, parent and student), shipped as an installable PWA in P1 and wrapped as native iOS/Android apps in P2. This document specifies the offline data scope, sync conflict rules, push notifications with deep links, biometric re-authentication, the device registry and performance budgets on 3G.
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: PSE ONE (pse4.one, public scan on 16/09/2026) markets its mobile app as the main differentiator, running on desktop, laptop, tablet, Android and iOS, with three separate apps for parents (schedule, attendance, results, tuition), students (notifications, learning materials, course reviews) and teachers (teaching schedule, attendance, assessments, incident reports), plus a monitoring app for the owner.
Limits
- Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
- Whether the competitor's mobile app is native on the stores or a wrapped web app; whether it works offline and what is cached.
- Whether the parent, student and teacher apps are three store listings or one app with role selection after login.
- What "thông báo và nhắc nhở thông minh" covers, which channels it uses (push, Zalo, SMS) and whether users can mute types.
- Which real-time numbers the owner monitoring app shows and whether they are split by branch.
- Whether the competitor limits signed-in devices, has a device-management screen or biometric authentication.
- Which fields the teacher "báo cáo sự cố" contains and who receives it.
Proposed design
Six 390 px mobile screens for the four role shells on one codebase and one desktop screen for the device registry; the key point is that each shell holds only that role's work for the day, while network state, the sync queue and conflicts are always spelled out in text on the screen.






deep_link to the attendance sheet, with no amount or full name (rule 7); the notification list is grouped by category with unread dots, the item just tapped is highlighted and marked read, and the dark toast offers undo (MOB-03).
1Purpose
Most of a centre's daily work does not happen at a desk: teachers mark attendance in the classroom, branch managers approve requests while moving between sites, consultants call from their phones, and parents open Trường Số after a Zalo message. Trường Số today is a responsive web application shared by every role; it cannot be installed on the home screen, does not work without a connection and cannot send push notifications. This document specifies a mobile experience that shares its codebase with the web, split into role-specific shells, each containing only what that role needs during the day.
Platform decision: PWA first in P1 (installable, service worker, background sync, Web Push on Android and desktop, Web Push on iOS 16.4+ once added to the home screen); Capacitor wrappers in P2 for store presence and reliable iOS push. The interface is not rewritten in native technology.
In scope
- Four role shells on one codebase: Manager (centre owner, branch manager), Teacher, Consultant, Parent and student; bottom tab bar with at most five items.
- Installable PWA: manifest, service worker, install prompt, iOS-specific instructions.
- Offline data scope, offline write queue and conflict rules on sync.
- Push notifications with deep links; device registry, device and session revocation; biometric re-authentication.
- Performance budgets on 3G, inheriting PRD 12.
- Native iOS/Android apps in P2: Capacitor packaging, push through APNs and FCM, store release.
Out of scope
- The business logic of each module (attendance, grades, lesson log, tasks, parent portal) is specified in its own PRD; this document only specifies how they appear and behave on mobile.
- The existing FLYER learner app and FLYER teacher app.
- Reception kiosk tablets and QR check-in (P3); two-way teacher and parent chat (P3, PRD 07).
Feature matrix by role and phase
| Feature | Manager | Teacher | Consultant | Parent | PRD |
|---|---|---|---|---|---|
| Today's numbers, alerts | P1 | no | P1 (own targets) | no | 09 |
| Approvals: discounts, leave requests, incidents | P1 | no | no | no | 05, 16 |
| My tasks; leave requests, incident reports | P1 | P1 | P1 | excuse notice P1 | 16, 04 |
| Today's teaching schedule / class schedule | P1 | P1 | P1 (trials) | P1 | 03, 07 |
| Offline attendance, background sync | mark on behalf P1 | P1 | no | view P1 | 04 |
| Grades and comments; lesson log | view P1 | P1 (offline) | no | results, materials P1 | 13, 14 |
| Call queue, in-app calling, cold leads | no | no | P1 | no | 01 |
| Tuition, QR payment | view receivables P1 | no | no | P1 | 05, 07 |
| Push: Web Push / store push | P1 / P2 | P1 / P2 | P1 / P2 | P1 / P2 | 17 |
| Biometrics: WebAuthn / native | P1 / P2 | P1 / P2 | P1 / P2 | P2 | 17, 12 |
| Device registry, revocation | P1 (whole tenant) | P1 (own) | P1 (own) | P1 (own) | 17 |
| Native apps on the stores | P2 | P2 | P2 | P2 | 17 |
2Today
Trường Số has no store app and no PWA configuration. PRD 04 and PRD 07 already assume a teacher PWA with an offline attendance queue and a parent portal with OTP login; this document turns those two assumptions into a shared platform.
| Existing capability | Endpoint | Keep? |
|---|---|---|
| Current user, permissions, accessible branches | GET /v3/crm/me, /v3/crm/me/permissions, /v3/crm/me/accessible-schools | existing · keep; decides which shell is shown |
| Today plan | GET /v3/crm/me/today-plan | existing · keep; PRD 09 and 16 add attendance, approval and assigned-task items |
| In-app notifications: list, unread count, read, read all, archive | GET /v3/crm/me/notifications, /unread-count, /read, /read-all, /archive | existing · keep; add deep_link, category |
| Learner report and weekly report for parents | GET /v3/parent/learner/{id}/weekly-report, /report | existing · keep; parent Results tab |
| Learner-side feature flags | GET /v3/learner/feature-configs | existing · keep; template for per-shell feature flags |
| Manifest, service worker, background sync, Web Push, device registry, native packaging | none | new |
3User stories and acceptance criteria
- Given the app was opened at least once today while online, so today's schedule and rosters are cached
- When the teacher opens the sheet offline, marks statuses and taps Lưu
- Then the sheet enters the offline queue with
client_saved_at; the session shows a "Chưa đồng bộ" chip and a "1 mục chờ đồng bộ" banner; when online, the service worker sends the queue through Background Sync or as soon as the app reopens; each item shows Đã đồng bộ or Xung đột; the success path needs no further action.
- Given a registered push device and an event that produces a notification (unmarked session, leave request pending approval, assigned task, new receipt)
- When the user taps the notification on the lock screen or in the tray
- Then the app opens the route in
deep_link(for example/teach/sessions/:id/attendance) even when not running; if the session has expired, the app re-authenticates and then continues to that route; the notification is marked read through/v3/crm/me/notifications/read; each notification belongs to a category that can be muted in notification settings.
- Given the owner or branch_manager role with a branch or "Tất cả chi nhánh" selected
- When the Hôm nay tab and then the Phê duyệt tab of the Manager shell are opened
- Then Hôm nay shows metric cards from PRD 09 (sessions marked x/y, absent students, cash received today, new leads, trials) and alerts (unmarked over 2 h, consecutive absences, overdue tuition), each opening a detail list and cached locally for instant reopening; Phê duyệt lists discount requests (PRD 05), leave requests and incident reports (PRD 16) with requester, subject, waiting time and two buttons Duyệt and Từ chối (rejection needs a reason), audited per PRD 12; requests above the role's approval limit show "Cần chủ trung tâm" and no approve button.
- Given the consultant's own call queue from PRD 01 (scheduled callbacks, new uncontacted leads, cold leads over 14 days)
- When Gọi is tapped on a row
- Then the phone dialer opens with the full number (the interface shows a partially masked number); on returning to the app, a call-result sheet appears with start time, estimated duration and the preset outcomes of PRD 01; saving creates a Call interaction on the lead; an unfinished sheet is re-prompted after 10 minutes; leads flagged Không gọi have no call button.
- Given OTP login per PRD 07; the student has lesson logs (PRD 14) and published grades (PRD 13)
- When the student's Học tập tab is opened
- Then the parent sees Tài liệu bài học (per session: title, content taught, homework, attachments the teacher marked as shared) and Kết quả (published grades and comments, weekly and learner reports from
/v3/parent/learner/{id}/weekly-reportand/report); attachments open in the in-app viewer and download only on tap; unpublished content never appears.
- Given a device with biometrics and "Mở khoá bằng vân tay hoặc khuôn mặt" enabled in Tài khoản
- When the app returns to the foreground after more than 5 minutes (staff) or 30 minutes (parents), or before a screen with financial data and parent contacts opens
- Then a lock screen covers the content and requests WebAuthn (P1) or native biometric (P2) authentication; success continues on the open screen; three failures require password or OTP login; the OS app-switcher preview is blanked.
- Given a first login on a device
- When the app starts after login
- Then the device is registered through
POST /v3/crm/me/devices(name, platform, version, push token if any); the audit log records "login from a new device" and the user's other devices are notified; in Tài khoản the user sees the device list and taps Thu hồi to sign out remotely; the owner sees devices across the tenant; a revoked device receives 401 withDEVICE_REVOKEDon its next API call and wipes all local offline data.
- Given a Capacitor build of the same web codebase published to both stores
- When a parent installs from the store and logs in with OTP
- Then the app behaves like the PWA; push goes through APNs or FCM;
https://truongso.net/m/...links in Zalo messages open the native app directly (Universal Links, App Links); web content updates from the server without a store release unless native plugins change; builds below the minimum version get an update-required screen on launch.
4Screens
After login the app reads /v3/crm/me and /v3/crm/me/permissions to pick the shell: branch-management permissions go to Manager, a TeacherProfile with assigned classes goes to Teacher, a SaleProfile goes to Consultant; multi-role users get a shell switcher in the Tài khoản tab and the last-used shell is remembered. Parents enter at /parent per PRD 07. Shared components: header with branch switcher, notification bell with unread count from /unread-count, network and sync banner (opens the queue list with states Chờ, Đang gửi, Đã đồng bộ, Xung đột, Lỗi and a retry-all button), biometric lock screen.
| Shell | Tab bar | Notes |
|---|---|---|
| Manager | Hôm nay · Cảnh báo · Phê duyệt · Việc của tôi · Tài khoản | branch switcher in the header |
| Teacher | Hôm nay · Lịch tuần · Lớp của tôi · Việc của tôi · Tài khoản | keeps the PRD 04 structure, adds Việc của tôi |
| Consultant | Hôm nay · Hàng đợi gọi · Khách hàng · Học thử · Tài khoản | Hôm nay holds targets and the today plan |
| Parent | Trang chủ · Lịch học · Học tập · Học phí · Thông báo | Học tập groups Điểm danh, Tài liệu, Kết quả |
Today's session list per PRD 04 with an added sync chip (Đã đồng bộ, Chờ đồng bộ, Xung đột). The session page has three segments: Điểm danh (PRD 04), Sổ đầu bài (PRD 14: content taught, homework, attachments, share-with-parents toggle), Điểm (PRD 13: grades and comments entered as a list, drafts saved offline, published when online). Đơn xin nghỉ and Báo cáo sự cố (PRD 16) sit in the overflow menu and in Việc của tôi. All three segments are readable offline when the session is inside the cached scope (section 5).
Four sections: Gọi lại đã hẹn (sorted by scheduled time, overdue shows Quá giờ), Chưa liên hệ (newly assigned leads, sorted by priority), Khách hàng nguội (no interaction for 14 days per tenant threshold, sorted by expected amount), Học thử hôm nay (with confirmation status and a Nhắn Zalo button). Each row: guardian name, prospective learner, source, Gọi and Ghi chú buttons; the call-result sheet appears as a bottom sheet. The queue is cached for offline reading; sheets written offline enter the sync queue. Each section has its own empty state.
Three segments: Điểm danh (month calendar per PRD 04), Tài liệu (per session: date, title, content taught, homework, attachments with size), Kết quả (published grade table by period, comments, button to the weekly report). Empty state: "Giáo viên chưa đăng tài liệu cho lớp này". Adult learners see the same screen for themselves.
Table: device (OS-provided or user-set name, "Thiết bị này" label), platform (chip Android PWA / iOS PWA / iOS native / Android native / Web), push (Đang bật / Đã tắt / Không hỗ trợ), last activity (time, city inferred from IP), actions Đổi tên · Thu hồi. /manage/devices for the owner: every staff device in the tenant, filterable by staff, branch and status; staff with status Đã nghỉ who still have active devices are pinned on top with Cần thu hồi.
5Business rules
- One codebase: mobile shells are routes in center-crm-frontend sharing components, vi/en message files and the API layer with desktop; the P2 native wrapper loads the same web bundle.
- The shell is chosen by permissions, not screen size; the full desktop interface is always reachable through "Mở bản đầy đủ" in Tài khoản.
- Offline scope by role and time: teachers cache schedule, rosters, attendance sheets, lesson logs and grade drafts for sessions from yesterday to the end of tomorrow; managers cache today's numbers and alerts; consultants cache the day's call queue; parents cache the current week's schedule and 30 days of notifications. Full guardian phone numbers, ID numbers, invoices and financial data are never cached.
- Offline data is encrypted with a key bound to the device session and wiped on logout, on device revocation, or after 7 days without opening the app.
- Every offline write enters the queue with
client_saved_atandidempotency_key; the server applies a batch in client-time order and returns a result per item; multiple writes to the same object in the queue collapse to the last one. - Conflicts by data type: attendance sheets use
versionper PRD 04, a later submission with a stale version gets 409 and the user chooses; lesson logs and comments apply last-write-wins byclient_saved_atwith history; published grades are never overwritten from the offline queue, only drafts sync; call results and leave requests are new records, deduplicated byidempotency_keyonly. - A push carries only a title, a one-line summary and
deep_link; no phone numbers, amounts or full student names on the lock screen (initials are used). - Parent push respects the PRD 07 quiet hours 21:30–07:00; staff have no quiet hours by default but can set their own; the PRD 04 21:00 attendance reminder still sends.
- A user may have at most 5 active devices; the oldest is revoked when exceeded; a push token is dropped after 3 consecutive "no longer registered" failures.
- Mobile sessions inherit PRD 12: expiry after 30 days of inactivity, password change revokes other sessions; biometric re-authentication does not replace the mandatory second factor for owner and accountant.
- Performance budgets on 3G (1.6 Mbps, 300 ms latency, mid-range 2022 Android): cold start to interactive ≤ 4.0 s; warm start from cache ≤ 1.0 s; opening a cached attendance sheet ≤ 500 ms; initial JS per shell ≤ 200 KB gzip; today-plan response ≤ 50 KB; images served as webp sized to the screen. PRD 12 API thresholds still apply.
- Per-shell feature flags are read from tenant configuration at launch; a disabled feature disappears from the tab bar rather than erroring. All strings live in message files (decision D9).
6Data model
| Entity | Fields | Notes |
|---|---|---|
| device | id uuid; tenant_id; user_id null; parent_account_id null; name; platform enum(web, android_pwa, ios_pwa, android_native, ios_native); app_version; os_version; install_mode enum(browser, standalone, native); push_provider enum(webpush, fcm, apns) null; push_token null; push_status enum(active, disabled, unsupported, failed); device_fingerprint; last_seen_at; registered_at; revoked_at null; revoked_by null | new; unique (user_id | parent_account_id, device_fingerprint) |
| notification (extended) | + category enum(attendance, approval, task, finance, admissions, parent, system); + deep_link; + push_sent_at null; + push_result enum(sent, failed, skipped) null; + collapse_key | extend |
| notification_preference | id; tenant_id; user_id | parent_account_id; category; push bool; in_app bool; quiet_from; quiet_to | new; all on by default |
| sync_batch | id uuid; tenant_id; school_id; user_id; device_id; received_at; item_count; ok_count; conflict_count; error_count | new; kept 30 days |
| sync_item | id; batch_id; idempotency_key (unique per tenant); type enum(attendance_sheet, lesson_log, grade_draft, call_result, leave_request, incident_report); target_id; client_saved_at; base_version null; payload jsonb; result enum(applied, merged, conflict, rejected); result_detail; server_version | new |
| offline_queue, offline_cache_manifest (on device, IndexedDB) | queue: key; type; target_id; payload; client_saved_at; idempotency_key; status enum(pending, sending, done, conflict, error); attempts. manifest: scope enum(teacher, manager, sales, parent); cached_at; expires_at; entries [{route, etag}] | not on the server; encrypted per section 5 |
| mobile_policy (tenant settings) | max_devices_per_user (5); biometric_relock_minutes_staff (5); biometric_relock_minutes_parent (30); offline_retention_days (7); cold_lead_days (14); finance_requires_biometric bool; shell_features {manager, teacher, sales, parent: [feature_key]} | stored in school config (PRD 10) |
Scoping: device and notification_preference belong to the tenant; sync_batch and sync_item carry the school_id of the branch that produced the data so a branch manager can review sync history for teachers in the branch; parent devices carry no school_id because one parent account may have children in several branches.
7API contract
| Method | Path | Purpose | Status |
|---|---|---|---|
| GET | /v3/crm/me · /v3/crm/me/permissions · /v3/crm/me/accessible-schools | shell selection, branch scope | existing |
| GET | /v3/crm/me/today-plan | Hôm nay tab of Manager and Consultant | existing |
| GET / POST | /v3/crm/me/notifications · /unread-count · /read · /read-all · /archive | notification centre; add category parameter and deep_link field | extend |
| GET | /v3/parent/learner/{id}/weekly-report · /report | parent Results tab | existing |
| GET | /v3/learner/feature-configs | learner-side feature flags; unchanged | existing |
| POST | /v3/crm/me/devices | {name, platform, app_version, os_version, install_mode, device_fingerprint, push_provider?, push_token?} → device; idempotent by fingerprint | new |
| GET / PATCH / DELETE | /v3/crm/me/devices[/{id}] | own devices; rename; revoke; PUT /{id}/push-token refreshes the token | new |
| GET / DELETE | /v3/school/devices[/{id}] | owner: devices across the tenant, filter staff_id, school_id, status; revoke | new |
| GET / PUT | /v3/crm/me/notification-preferences | per-category toggles, quiet hours; POST /v3/crm/push/test sends a test push | new |
| GET | /v3/crm/me/offline-bundle?scope=teacher|manager|sales&date= | one aggregated response with everything in the role's offline scope, with per-part etags | new |
| POST | /v3/crm/sync/batch | {device_id, items: [{idempotency_key, type, target_id, client_saved_at, payload, base_version?}]} → {batch_id, results: [{idempotency_key, result, server_version, detail}]}; max 50 items; each item is delegated to the business endpoint of PRD 04, 13, 14, 16, 01 | new |
| GET / POST / DELETE | /v3/parent/me/devices[/{id}] | parent devices, same contract as staff | new |
| GET | /v3/parent/learner/{id}/lesson-logs · /grades | shared lesson materials (PRD 14), published results (PRD 13) | new |
| GET / PUT | /v3/teacher/school-config | add the mobile_policy block and minimum app version per platform (P2) | extend |
Push is sent by a server-side worker: every new notification whose category is enabled in the recipient's preferences produces one delivery to each active device through Web Push (VAPID), FCM or APNs; the outcome is written to push_result. Deep links use the same paths for the PWA and the native app.
8Permissions
| Code | owner | branch_manager | admissions | academic_admin | accountant | teacher | parent |
|---|---|---|---|---|---|---|---|
mobile.shell.manager | all | branch | no | branch (no Phê duyệt) | branch (finance numbers) | no | no |
mobile.shell.teacher | when assigned classes | when assigned classes | no | no | no | yes | no |
mobile.shell.sales | yes | branch | own or team leads | no | no | no | no |
mobile.shell.parent | no | no | no | no | no | no | own children |
mobile.offline | yes | yes | yes | yes | no (financial data) | yes | yes |
mobile.sync.override | yes | branch | own leads | branch | no | own sessions within 24 h | no |
devices.view_own, devices.revoke_own | yes | yes | yes | yes | yes | yes | yes |
devices.view_all, devices.revoke_any, devices.sync_history | yes | branch | no | sync_history only, branch | no | own sync_history only | no |
mobile.policy | yes | no | no | no | no | no | no |
9Analytics events
| Event | Properties |
|---|---|
| app_launched / install_completed | shell, platform, install_mode, cold_start_ms, from_push, network (offline|3g|4g|wifi) |
| offline_write_queued | type, shell, queue_length |
| sync_batch_completed | item_count, ok_count, conflict_count, error_count, seconds_offline, trigger (background|foreground|manual) |
| sync_conflict_resolved | type, resolution (keep_server|override), by_role |
| push_permission_granted / push_permission_denied | shell, platform |
| push_delivered / push_opened | category, provider, seconds_to_open, deep_link_route |
| biometric_enabled / biometric_relock / biometric_failed | shell, method (webauthn|native), attempts |
| device_registered / device_revoked | platform, by (self|owner|limit), active_devices |
| mobile_approval_actioned | type (discount|leave|incident), action, seconds_waiting |
| parent_material_opened | kind (lesson_log|attachment|grade|weekly_report), file_size_bucket, network |
| perf_budget_exceeded | metric (cold_start|warm_start|sheet_open|bundle), value, budget, network |
Weekly metrics on PRD 09: share of teachers marking attendance from mobile, share of sheets saved offline, push open rate by category, active devices per staff member, p75 cold start by platform.
10Edge cases
- A teacher teaching at two branches on the same day: the offline bundle holds sessions from both branches; the branch switcher does not affect the Teacher shell.
- iOS clears PWA site data after 7 days of non-use: this matches the offline retention period; a queue unsent for more than 7 days is treated as lost and the user is told on reopening with a list of sessions still lacking attendance.
- A push arrives before the related data exists on the device: the deep link opens the screen in its loading state; if the object was deleted, "Mục này không còn tồn tại" is shown and the app returns to Hôm nay.
- Two teachers (main and substitute) mark the same session offline: the later batch gets 409 per PRD 04; the substitute wins when assigned, the other sees the conflict screen.
- Staff moved to Đã nghỉ (PRD 06): all of that person's devices are revoked automatically at the end of the end date and logged.
- Browser without Background Sync (Safari): the queue sends when the app is opened or foregrounded; the banner says "Mở ứng dụng để đồng bộ".
11Open questions
- PRD 07 places the parent portal in P2, while this document brings its mobile presentation into P1 because it shares the shell; either PRD 07's phase is revised or the P1 parent scope is narrowed to schedule and attendance.
- Should the P2 native app be a single store app with a shell switcher, or two apps (staff and parents)? Proposed: two apps from one codebase so store listings and OS permissions stay separate.
- Should the parent portal be folded into the existing FLYER parent app instead of a separate Trường Số parent app (already raised in PRD 07)?
- Does in-app calling need a PBX (SIP or VoIP provider) for recording and central billing, or is the phone dialer enough for P1?
Trường Số (truongso.net) · FLYER internal