- Given a new center created via onboarding
- When the owner lands on the dashboard
- Then a checklist appears: add branch details, invite staff, create courses and fee plans, add rooms, connect bank account, connect Zalo, import students; each item links to its settings page and ticks when done.
PRD 10 · Settings and administration
Tenant profile, branches, users and invitations, roles with a unified permission catalog, catalogs (tags, sources, channels, levels, rooms, fee plans, expense categories, job titles), integrations (Zalo, SePay, e-invoice), notification policies, and the audit log viewer.
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 settings tree, default roles ("quản lý, kế toán, giáo viên, tư vấn") and permission granularity ("phân quyền").
- Whether branches are separate logins or one login with a switcher.
- Which catalogs are configurable (sources, statuses, fee types, expense categories).
- Their email auto-send configuration fields.
Proposed design
Settings is its own area with a 220 px sub-navigation on the left; every page is a plain list or form in the Cloud theme, and the most important thing to see is the permission matrix with a per-cell scope (None / Own / Branch / All) together with the read-only audit log and its before/after diff.





<module>.<action> grouped by module (Admissions, Academic, Finance, Settings), columns are the eight roles; each cell is a checkbox with its scope Own / Branch / All and a four-level scope picker opened on the cell being edited. The Receptionist column is being edited: finance.payment.create just went from Branch to None (amber cell); the bar below reports one unsaved change written through PUT /roles/{id}/permissions, effective from the next request (SET-03).





role.permissions_changed row: an info block (time, actor, action, object, branch, IP and device) and a Field · Before · After table with old values struck through in red and new values in green. The callout links to the same-session event (role.created); the footer only offers Close, Open role and Copy JSON, with no edit button (SET-06).1Purpose
Settings is where the owner shapes the product to the center: which branches exist, who can do what, which lists the forms offer, and which external services are connected. The current app has settings pages for users, roles, sale permissions, tags, sources, channels and tenant, three of which do not persist. This PRD replaces the mocks, consolidates settings under one route with a left sub-navigation, and adds what the new modules need.
2What exists today
| Page | State | Action |
|---|---|---|
| /crm/settings/users (sale profiles list, role assign, school access modal calling a non-spec endpoint) | partly working | rebuild as Users with invitations and branch scope |
| /crm/settings/roles (list, drawer with matrix; create is a mock toast; update works) | partly working | implement create via POST /v3/crm/roles; unify scope vocabulary |
| /crm/settings/sale-permissions | working | merge into Users |
| /crm/settings/tags | working (API) | keep |
| /crm/settings/sources, /channels | localStorage mocks | switch to API (PRD 01) |
| /crm/settings/tenant (name, logo, locale, timezone, business hours) | not persisted | persist via school-config and center update |
| Custom fields (create dialog inside customer form) | working | surface under Settings → Trường tuỳ chỉnh |
| Packages catalog (CRM sales packages) | working in detail tab | surface under Settings → Gói tư vấn (distinct from fee plans) |
| Pipelines and stages editor (in pipeline selector) | working | also reachable from Settings → Quy trình |
3Settings navigation
Left sub-navigation 220 px inside the content area; each page is a plain list or form per the Cloud theme. /crm/settings/* redirects to the matching new route.
4Roles and permissions
Vocabulary
- Permission code:
<module>.<action>, lowercase, as listed in each PRD. The catalog endpoint returns code, module, label (vi/en via X-Locale), description. - Scope, per permission per role:
own(records the user owns or is assigned to),branch(records in the user's branches),all(tenant). Replaces the current mix of own/team/all and none/read/write/manage. Existingteammaps tobranch. - Ownership rules per module: leads by assignment; classes and sessions by main or substitute staff; students by class membership for teachers; invoices by nothing (branch scope only).
Default roles
| Role code | VI name | School role derived | Summary |
|---|---|---|---|
| owner | Chủ trung tâm | OWNER | everything, all branches |
| branch_manager | Quản lý chi nhánh | ADMIN | all modules within assigned branches; approvals |
| admissions | Tư vấn tuyển sinh | none (or TEACHER if also teaching) | leads own, trials, placements, convert |
| academic_admin | Giáo vụ | ADMIN | students, classes, attendance, communication, materials issue |
| accountant | Kế toán | ADMIN (finance) | finance, inventory manage, staff contracts and pay |
| teacher | Giáo viên | TEACHER | own classes, attendance, class notes, self timesheet |
| viewer | Chỉ xem | none | read-only dashboards and lists (investors, auditors) |
The consolidated matrix is the union of the per-PRD tables (01 to 09). Roles can be cloned and edited; default roles are locked for code and name but their permissions can be adjusted by owner; a "Khôi phục mặc định" resets.
Users and branch scope
- A user is a staff record (PRD 06) with an account; the Users page lists staff with access, their roles, branches, last login, status (Đã kích hoạt, Chờ mời, Vô hiệu).
- Invitation by email or phone; the invitee signs in with an existing FLYER account or registers; on acceptance the sale profile and school memberships are created for the assigned branches.
- Branch scope is a list of school ids per user; replaces the non-spec school-access endpoint.
- Deactivation revokes sessions, keeps history; reactivation possible.
5User stories and acceptance criteria
- Given permission
settings.users - When I invite name, email or phone, role, branches
- Then an invitation is sent with a 7-day link; the Users list shows Chờ mời; resend and revoke are available; on acceptance the user appears active with memberships created.
- Given the roles page
- When I clone Giáo vụ, rename it "Lễ tân", and untick finance.payment.create
- Then the role is created through
POST /v3/crm/rolesandPUT /roles/{id}/permissions, and users assigned to it see the effect on next request.
- Given a school under the center
- When I edit address, phone, map link, opening hours, default receiving account, cash drawer name, logo override
- Then the profile is saved and used on receipts, messages ({chi_nhanh}, {sdt}) and the parent portal.
- Given owner
- When I add a branch with name and code
- Then a FLYER school is created under the center, the owner is a member, and the branch appears in the switcher.
- Given permission
settings.audit - When I filter by actor, action, entity, date
- Then I see entries with before/after diff and can export CSV; entries are read-only.
- Given the integrations page
- When I connect SePay (existing link-token flow), Zalo OA (PRD 07), or an e-invoice provider (credentials form)
- Then each card shows status, last sync, errors, and a disconnect action; secrets are masked after save.
6Screens (field lists)
| Page | Fields / columns | Validation |
|---|---|---|
| Trung tâm | Tên trung tâm; Mã; Logo (upload); Pháp nhân (legal_name, type, tax_id or citizen_id, address, issued_date, issued_by from SchoolCompany); Ngôn ngữ mặc định; Múi giờ; Giờ làm việc per weekday; Ngày nghỉ (list) | tax id 10 or 13 digits; citizen id 12 digits |
| Chi nhánh list | Tên, Mã, Địa chỉ, SĐT, Học viên, Lớp, Người dùng, Trạng thái; + Thêm chi nhánh | code unique |
| Người dùng list | Họ tên, Email/SĐT, Vai trò (chips), Chi nhánh (chips), Đăng nhập gần nhất, Trạng thái; actions Sửa, Vô hiệu, Gửi lại lời mời | last owner cannot be removed |
| Vai trò | list: Tên, Mã, Số quyền, Người dùng, Mặc định; matrix: rows = permissions grouped by module, columns = scope radio (Không / Của mình / Chi nhánh / Tất cả) | at least one owner role with all |
| Nguồn, Kênh, Nhãn, Trình độ, Phòng, Chức danh, Danh mục chi | generic MasterDataPage: name, code (where applicable), colour, position (drag), active; inline create; deactivate instead of delete when in use | name unique per tenant |
| Tài chính | Receipt prefix and scope; Discount limits per role; Approval thresholds (discount, refund, expense, inventory adjust); VAT defaults per line type; Credit sessions on (create / full payment); Refund policy (admin fee percent, rounding); Bank accounts (existing list with SePay status) | percent 0–100 |
| Thông báo | Table: message type × channels checkboxes, quiet hours apply; Quiet hours; ZNS monthly cap; Digest settings | |
| Tích hợp | Cards: SePay (accounts linked), Zalo OA (name, templates approved x/y), E-invoice (provider select, credentials, test), Webhooks (outgoing URL and secret for events) | |
| Nhật ký | Thời gian, Người thực hiện, Hành động, Đối tượng, Chi nhánh, IP; expand diff |
7Data model
| Entity | Fields | Notes |
|---|---|---|
| center (existing Center) | + logo_file_id; default_locale; timezone; business_hours jsonb; settings jsonb | extend |
| branch_profile | school_id PK; center_id; address; phone; map_url; opening_hours jsonb; default_bank_account_id; cash_account_id; logo_file_id; is_active | new |
| invitation | id; center_id; email; phone; role_id; school_ids; token; expires_at; accepted_at; created_by | new |
| user_branch_scope | sale_profile_id; school_id | new (replaces implicit accessible_schools) |
| crm_role (existing) | + is_locked; cloned_from; scope per permission stored as {code, scope} | extend (permissions with scope) |
| audit_log | PRD 12 section 6 | new |
| integration | id; center_id; type enum(sepay, zalo, einvoice, webhook); config jsonb (encrypted secrets); status; last_sync_at; last_error | new |
| onboarding_checklist | center_id; items jsonb {key: done_at} | new |
8API contract
| Method | Path | Request / response | Status |
|---|---|---|---|
| GET | /v3/crm/permissions-catalog | extended with new module codes | extend |
| GET/POST | /v3/crm/roles; GET /roles/{id}; PUT /roles/{id}/permissions | permissions body becomes [{code, scope}]; POST /roles/{id}/clone; POST /roles/{id}/reset | extend |
| GET | /v3/crm/me/permissions | scope values own|branch|all; accessible_schools = branch scope | extend |
| GET/POST/PUT | /v3/crm/sale-profiles[...]; POST /{id}/roles | unchanged | existing |
| PUT | /v3/crm/sale-profiles/{id}/branches | {school_ids[]} replaces non-spec users/{id}/school-access | new |
| GET/POST/DELETE | /v3/crm/invitations; POST /{id}/resend; POST /v3/crm/invitations/accept | {email|phone, role_id, school_ids} / {token} | new |
| POST | /v3/crm/sale-profiles/{id}/deactivate | /reactivate | new | |
| GET/PATCH | /v3/crm/centers/{id} | tenant profile fields | extend (GET list exists) |
| GET/POST | /v3/school/branches; GET/PATCH /branches/{school_id} | creates FLYER school under center | new |
| GET/PUT | /v3/teacher/school-config | settings blocks: attendance, finance, notifications | extend |
| GET/PUT | /v3/teacher/school-finance/company | legal profile | existing |
| GET/POST/PATCH/DELETE | /v3/crm/sources, /channels, /tags, /custom-fields, /packages, /pipelines, /stages, /statuses (+reorder) | unchanged | existing |
| GET/POST/PATCH | /v3/school/levels, /rooms, /job-titles, /expense-categories, /fee-plans, /holidays | see module PRDs | new |
| GET | /v3/school/audit-logs | actor_id, action, entity_type, entity_id, school_id, from, to, page; format=csv | new (generalises customers/{id}/audit-logs) |
| GET/POST/DELETE | /v3/school/integrations[/{type}]; POST /{type}/test | new | |
| GET/PATCH | /v3/school/onboarding-checklist | new | |
| POST | /v3/school/exports/full | tenant export job (PRD 12) | new |
9Permissions
| Code | owner | branch_manager | accountant | academic_admin |
|---|---|---|---|---|
settings.tenant | yes | no | legal info | no |
settings.branches | yes | own branch profile | no | no |
settings.users | yes | branch (invite non-owner roles) | no | no |
settings.roles | yes | no | no | no |
settings.catalogs (sources, channels, tags, levels, rooms, job titles, courses) | yes | rooms | expense categories, fee plans | levels, rooms, courses |
settings.finance | yes | no | yes | no |
settings.notifications, settings.templates | yes | no | no | no |
settings.integrations | yes | no | sepay, einvoice | no |
settings.audit | yes | branch | finance actions | no |
settings.data (imports, exports, anonymise) | yes | imports | imports (finance) | imports (students, classes) |
10Analytics events and edge cases
| Event | Properties |
|---|---|
| onboarding_checklist_item_done | key, days_since_signup |
| user_invited / invitation_accepted | role, branches |
| role_created / role_permissions_changed | cloned_from, changes |
| branch_created | count_after |
| integration_connected / integration_failed | type |
| audit_log_viewed / exported | filters |
- Invitee already a member of the center: invitation converts to a role change.
- Removing the last branch from a user: refused; deactivate instead.
- Role deleted while assigned: refused; reassign first.
- Zalo OA connected to two centers: refused by Zalo; show guidance.
Trường Số (truongso.net) · FLYER internal