- Given
targets.managewith scope all - When Chỉ tiêu → + Tạo chỉ tiêu, period Tháng 10/2026, metrics picked from the catalogue (new leads 300, trials 120, conversions 60, signed revenue 900,000,000, collected 850,000,000), Lưu nháp
- Then A
targetin statusdraftis created with one center-scope line per metric; beside each input the actuals of the previous period and of the same period last year are shown; the draft is visible only to users who can manage targets.
PRD 15 · Targets and performance
Monthly and quarterly targets for the center, each branch and each individual; actual values are computed automatically from existing admissions, tuition and attendance data, never typed in. The target board shows goal, actual, percentage, forecast to finish, pace alerts and a revision history with reasons. Bonus rules are formula-only; payout belongs to PRD 06.
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: the pse4.one home page (public scan on 16/09/2026) lists a "Quản lý chỉ tiêu" (target management) module; its reporting module offers "Thống kê doanh số tư vấn", "Thống kê giáo viên", "Thống kê buổi học còn lại" among roughly 50 report types; teacher salary is described as computed automatically from teaching hours, coefficients, bonuses and penalties. How targets are set is not published; the design below is Trường Số's own.
Limits
- Verification pending in-app survey, because automated login to the reference product was not authorised in the drafting session:
- At which scope (center, branch, individual) and period (month, quarter, year) the competitor's "Quản lý chỉ tiêu" module sets targets.
- Whether their actuals are computed automatically or typed in, and whether any forecast exists.
- Whether "Thống kê doanh số tư vấn" compares against targets and includes a staff ranking.
- Whether bonus and penalty in the teacher salary formula are tied to targets or entered by hand on the payroll.
Proposed design
The target board puts goal and actual on the same row with a pace-based status chip; the three-step create-and-allocate flow blocks publishing while child lines do not add up to the parent; consultants see only their own targets and rank, on the phone as well.









1Purpose
A center owner runs the business on a handful of numbers: how many new students this month, how much revenue, which branch is behind, which consultant leads. Today those numbers live in Excel, while the application dashboard shows actuals with no goal to compare against. This module puts targets next to actuals on one screen: targets cascade from the center to branches and individuals, actuals are computed from records that already exist, and every change leaves a trace with a reason. Core principle: there is no manual field for an actual value and every value can be traced to its source records; a number that cannot be computed from data is not admitted to the metric catalogue.
In scope
- A fixed metric catalogue in four groups: admissions, revenue and receivables, academic operations, teachers; each metric has a defined formula and source.
- Monthly and quarterly periods; center, branch and individual scope (consultant, teacher, accountant).
- Top-down allocation with sum validation: branches add up to the center, individuals add up to the branch, or an explicit override with a reason.
- Target board (goal, actual, percentage, pace, forecast to finish), consultant ranking, a "My targets" tile.
- Alerts when pace falls below 80% from mid-period; a targets section in the PRD 09 weekly digest.
- Daily snapshots, period close, revision history with actor and reason.
- P2: formula-based bonus rules; the output is an estimated amount, paid through the PRD 06 timesheet.
Out of scope
- Manual or Excel entry of actual values.
- Weekly or daily targets.
- Salary, commission and bonus payout (PRD 06).
- OKRs, qualitative goals, self-set personal goals.
- Statistical forecasting; the forecast here is a linear projection of the current pace.
2Today
No notion of a target exists in center-crm-frontend or the /v3 API; the existing dashboard endpoints return actuals per period and are the sources for this module, aggregated by a background job into daily snapshots.
| Existing capability | Endpoint | Keep? |
|---|---|---|
| Manager cards: won and lost count and value, conversion rate | GET /v3/crm/dashboard/manager-summary existing | yes; source for conversions and signed revenue (center, branch) |
| Funnel by stage; won/lost trend; won count and value per consultant | /funnel, /won-lost-trend, /top-sales existing | yes; source for new leads, the weekly pace line and individual consultant targets |
| Tuition figures: issued, paid, unpaid, overdue count | GET /v3/teacher/school-finance/tuitions/stats existing | yes; source for collected revenue and overdue collection |
| Consultant list; accessible branches | /v3/crm/sale-profiles, /lightweight, /v3/crm/me/accessible-schools existing | yes; used for allocation and filters |
| Target, target line, snapshot, revision history, bonus rule | none | new under /v3/crm/targets |
| Attendance rate, renewal, withdrawals, sessions taught, parent rating | new entities of PRD 02, 04, 05, 06, 07 | sources for the P2 group, available once the source module ships |
3User stories and acceptance criteria
- Given A center draft with conversions 60
- When The Phân bổ step is opened and "Chia theo tỷ trọng kỳ trước" is chosen or each branch is typed in
- Then Every branch in
accessible-schoolsgets a child line; the sum of children is compared to the parent in real time; on a mismatch the Xuất bản button is disabled and the gap is shown ("Thiếu 4"); "Ghi đè tổng" requires a reason. The branch manager repeats the step for consultants (list fromsale-profiles/lightweight); a consultant sees only their own target, and only after publishing.
- Given 18/10/2026, October targets published, the daily snapshot has run
- When Chỉ tiêu → Chi nhánh tab
- Then Each line shows goal, actual to the end of the previous day, percentage achieved, pace, end-of-period forecast and a chip Đã đạt / Đúng tiến độ / Chậm / Nguy cơ per the thresholds in section 5; the time of the latest snapshot is shown above the table.
- Given Mid-period has passed (day 15 of a month, day 45 of a quarter) and a line has pace below 0.80
- When The daily snapshot completes
- Then An in-app notification "Chỉ tiêu [tên] của [phạm vi] đang ở 62% tốc độ, dự báo đạt 71%" goes to the line owner and their direct manager; at most one alert per line per 7 days; when pace returns to 0.80 or above, a "back on track" notification is sent.
- Given A published target
- When A branch's conversions goal is lowered from 25 to 18 because the branch closes for two weeks of renovation, with a reason
- Then A
target_revisionstores the old and new value, actor, time and reason; child lines that no longer sum are flagged for reallocation; everyone who can see the line is notified; earlier snapshots keep the goal as it was when captured.
- Given The period has ended and the reconciliation grace has elapsed (default 5 days)
- When The background job runs the final snapshot
- Then The target moves to
closedand the final snapshot is flaggedis_final; late records do not change the closed figures; only the owner can "Mở lại kỳ" with a reason, which produces a new final snapshot and a revision entry.
- Given PRD 04 and 07 have shipped; the quarterly target has attendance rate 92%, renewal rate 75%, withdrawals at most 12
- When The board is opened on the Chi nhánh tab
- Then Rate lines show goal and actual as percentages with no linear forecast; the "withdrawals at most" line is lower-is-better and turns Nguy cơ when the actual exceeds 80% of the cap before mid-period; a teacher sees only their own line on the teacher home.
- Given
targets.bonus_rules - When A rule is created at
/settings/targets/bonus-rules: role consultant, metric signed revenue, tiers [100% to below 120%: 2% of signed revenue; from 120%: 3%], effective from January 2027 - Then Tiers must be contiguous and non-overlapping; from the final snapshot every individual line gets a
bonus_estimate; PRD 06 reads it to create the timesheet adjustment line "Thưởng chỉ tiêu tháng MM/YYYY"; approval and payout stay in PRD 06.
4Screens
Period picker (month, quarter; defaults to the current period), branch filter, tabs: Trung tâm · Chi nhánh · Cá nhân · Xếp hạng · Lịch sử. A line "Số liệu đến hết 17/10/2026 23:30" and a "Tính lại ngay" button for users who can manage. Actions: + Tạo chỉ tiêu, Xuất Excel.
Columns: metric, scope (center, branch, or individual with avatar), goal, actual (from the latest snapshot, with a drill-down link to the PRD 09 source report), % achieved (progress bar), pace, forecast (blank for rate metrics), status (chip Đã đạt / Đúng tiến độ / Chậm / Nguy cơ / Chưa đủ dữ liệu), actions (Sửa mục tiêu, Phân bổ, Xem lịch sử).
Selecting a line opens the detail page /targets/:id: the line tree (center → branch → individual), a cumulative daily actual chart against the linear goal line, a Lịch sử tab (time, actor, old value, new value, reason) and a Thưởng tab (P2). Empty state: "Chưa có chỉ tiêu cho kỳ này. Tạo chỉ tiêu để so sánh với số liệu thực tế." with Tạo chỉ tiêu and "Sao chép từ kỳ trước". For consultants and teachers the Trung tâm and Chi nhánh tabs are hidden.
Step 1 Period and metrics: choose period type and period, tick metrics by group; each metric has a center goal input and two reference columns (previous period, same period last year). Step 2 Branches: metric × branch grid; "Chia đều" and "Chia theo tỷ trọng kỳ trước" buttons; a totals row with the gap coloured by status token. Step 3 Individuals (optional, the branch manager can do it later): metric × person grid per branch. Lưu nháp and Xuất bản; publishing is blocked while an un-overridden sum mismatch remains.
Period, branch (or whole center) and ranking metric pickers (default signed revenue). Columns: rank, change since last week, consultant, branch, goal, actual, % achieved, pace; ordered by percentage achieved, not by absolute value. A consultant sees the whole branch table when ranking_visibility = branch (default) or only their own rank when self. Top three rows get a light background; the viewer's own row is pinned while scrolling. Empty state: "Chưa có chỉ tiêu cá nhân nào được xuất bản cho kỳ này."
Home tile: up to five progress bars with label, actual / goal, percentage, "còn thiếu N", days left; a last line "Hạng 3/8 trong chi nhánh" when ranking is visible. Full page: the same data with a weekly pace line (from won/lost data) and the list of alerts received. Empty state: "Chưa có chỉ tiêu được giao cho kỳ này."
5Business rules
| metric_key | Label (group) | Kind | Scope (C center, B branch) | Actual value and source | Phase |
|---|---|---|---|---|---|
leads_new | Lead mới (admissions) | count | C, B, individual | leads created in the period, duplicates excluded; source funnel | P1 |
trials_attended | Học thử (admissions) | count | C, B, individual | trial_booking in status attended within the period (PRD 01), attributed to the consultant who booked it | P1 |
conversions | Chuyển đổi (admissions) | count | C, B, individual | leads moved to won in the period; source manager-summary, top-sales | P1 |
revenue_signed | Doanh thu ký mới (revenue) | money | C, B, individual | sum of expected amount of leads won in the period; source manager-summary, top-sales | P1 |
revenue_collected | Doanh thu thực thu (revenue) | money | C, B | sum of valid payments recorded in the period; source tuitions/stats | P1 |
overdue_collected | Thu công nợ quá hạn (revenue) | money | C, B, individual (accountant) | payments in the period on invoices already overdue at period start; source tuitions/stats (extended) | P1 |
attendance_rate | Tỷ lệ chuyên cần (academic) | rate | C, B | (present + late) / marked over sessions in the period (PRD 04) | P2 |
renewal_rate | Tỷ lệ tái ghi danh (academic) | rate | C, B | students whose package ended in the period and who bought a new one within 30 days / students whose package ended in the period (PRD 02, 05) | P2 |
withdrawals_max | Học viên nghỉ học tối đa (academic) | count, lower is better | C, B | students moved to Đã nghỉ in the period (PRD 02) | P2 |
sessions_taught | Số buổi dạy (teacher) | count | individual (teacher) | held sessions as main or substitute teacher (PRD 04, 06) | P2 |
parent_rating | Đánh giá của phụ huynh (teacher) | score 1–5 | individual (teacher), B | average of parent ratings submitted in the period (PRD 07); minimum 5 ratings | P2 |
- The metric catalogue is fixed at product level; a tenant chooses which metrics to use but cannot define formulas; metrics whose source module has not shipped are hidden.
- A
targetis unique per (center_id, period_type, period_key); months are written2026-10, quarters2026-Q4. Quarterly and monthly targets are independent. - Sum validation applies to count and money metrics: children must equal the parent with zero tolerance. Rate and score metrics are not summed; the parent uses the aggregated actual. Overriding a sum requires
targets.overrideand a reason of at least 10 characters; the parent line carriessum_overridden. - Elapsed fraction = days elapsed / days in the period (Asia/Ho_Chi_Minh); a tenant may enable "working days" to exclude Sundays and the PRD 03 holidays. Pace = (actual / goal) / elapsed fraction; forecast = actual / elapsed fraction; both computed from day 3 of the period, before that the line shows Chưa đủ dữ liệu.
- Status thresholds: Đã đạt when actual is at or above goal; Đúng tiến độ when pace is 0.95 or more; Chậm from 0.80 to below 0.95; Nguy cơ below 0.80. For lower-is-better metrics, Nguy cơ when actual / goal exceeds the elapsed fraction plus 0.20. Rate metrics compare directly and show Chưa đủ dữ liệu when the denominator is below 10.
- Pace alerts are sent only from mid-period, at most once per 7 days per line; never for lines already achieved or edited in the last 24 hours.
- Snapshots run daily at 23:30 for every open period; a manual run (manage permission) is limited to 6 per hour; a snapshot stores the goal at capture time, so editing a goal does not rewrite history.
- A period closes automatically after the reconciliation grace (default 5 days, configurable 0–15). After close the figures do not change; reopening requires
targets.reopen. Editing a goal after publishing always requires a reason; deletion is allowed only in draft. - Individuals are attributed by profile (sale_profile_id, staff_id), not by login account; an individual's actual follows the primary assignee at the time of the event and is not recomputed on reassignment.
- Ranking is always by percentage achieved, ties broken by absolute value. Bonus rules (P2) only produce
bonus_estimateon the final snapshot; one person and one metric get at most one rule per period, the most recently effective rule wins.
6Data model
| Entity | Fields | Notes |
|---|---|---|
| target | id uuid; center_id; period_type enum(month, quarter); period_key text; status enum(draft, published, closed, archived); working_days bool; grace_days int; ranking_visibility enum(branch, self, all); created_by; published_at; closed_at; version int | new; unique (center_id, period_type, period_key) |
| target_line | id uuid; target_id; center_id; school_id null; scope enum(center, branch, individual); parent_line_id null; metric_key; owner_type enum(none, sale_profile, staff); owner_id null; goal numeric; direction enum(higher, lower); sum_overridden bool; override_reason null; inactive_owner bool; updated_by; updated_at | new; unique (target_id, metric_key, scope, school_id, owner_id) |
| target_snapshot | id uuid; line_id; center_id; school_id null; snapshot_date; goal_at_snapshot; actual; denominator null; pct; elapsed_pct; pace null; forecast null; status enum(achieved, on_track, behind, at_risk, no_data); is_final bool; computed_at | new; unique (line_id, snapshot_date); denominator for rate metrics |
| target_revision | id uuid; target_id; line_id null; action enum(created, published, goal_changed, allocated, overridden, closed, reopened); field; old_value; new_value; reason; actor_id; created_at | new; audit detail, Lịch sử tab |
| bonus_rule (P2) | id uuid; center_id; name; role enum(admissions, teacher, accountant); metric_key; bonus_type enum(pct_of_actual, fixed); tiers [{from_pct, to_pct null, value}]; effective_from period_key; active bool; created_by | new; a formula, not a payment |
| bonus_estimate (P2) | id uuid; line_id; snapshot_id (is_final); rule_id; achieved_pct; tier_index; amount; exported_to_timesheet_at null | new; read by PRD 06 to create the timesheet adjustment line |
The metric catalogue (metric_key, vi/en label, group, kind, scopes, direction, source, minimum denominator, phase) is fixed in code. Every table carries center_id (tenant) and school_id (branch) when the line belongs to a branch; branch-scoped roles are always filtered by accessible-schools; center-scope lines are visible only to roles with scope all.
7API contract
| Method | Path | Purpose | Status |
|---|---|---|---|
| GET | /v3/crm/dashboard/manager-summary | period → conversions, revenue_signed (center, branch) | existing |
| GET | /v3/crm/dashboard/top-sales | period → conversions, revenue_signed per consultant | existing |
| GET | /v3/crm/dashboard/funnel | period → leads_new; add school_id, assignee_id | extend |
| GET | /v3/crm/dashboard/won-lost-trend · /sale-summary | weekly pace line; existing cards next to the My targets tile | existing |
| GET | /v3/teacher/school-finance/tuitions/stats | revenue_collected; add overdue_collected_amount and from, to, school_id parameters | extend |
| GET | /v3/crm/sale-profiles/lightweight · /v3/crm/me/accessible-schools | people and branches for allocation | existing |
| GET · POST | /v3/crm/targets | GET period_type, period_key, status → Target[]; POST {period_type, period_key, working_days?, grace_days?, lines: [{metric_key, goal}]} → Target (draft); 409 PERIOD_EXISTS | new |
| GET | /v3/crm/targets/{id} | → Target with line tree and latest snapshots; as_of to view a past date | new |
| PATCH | /v3/crm/targets/{id} | {working_days?, grace_days?, ranking_visibility?, version}; 409 STALE_VERSION | new |
| PUT | /v3/crm/targets/{id}/lines | {lines: [{id?, metric_key, scope, school_id?, owner_type?, owner_id?, goal, parent_line_id?}], override?: {reason}, reason?} → saved lines; 422 SUM_MISMATCH with the gap per parent; reason required after publishing | new |
| POST | /v3/crm/targets/{id}/allocate | {line_id, method: even | last_period_share, children: [{school_id | owner_id}]} → proposed allocation, not saved | new |
| POST | /v3/crm/targets/{id}/publish · /close · /reopen | reopen requires {reason}; publish returns 422 SUM_MISMATCH while an un-overridden gap remains | new |
| GET · POST | /v3/crm/targets/{id}/snapshots · /revisions · /snapshot | → TargetSnapshot[] (line_id, from, to); TargetRevision[]; POST /snapshot runs a manual snapshot, 429 above 6 per hour | new |
| GET | /v3/crm/targets/board | period_type, period_key, scope, school_id?, metric_key? → lines with latest snapshot, filtered by permission; format=xlsx | new |
| GET | /v3/crm/targets/ranking | period_type, period_key, metric_key, school_id? → [{rank, rank_delta, owner, school, goal, actual, pct, pace}] | new |
| GET | /v3/crm/targets/me | → the current user's individual lines with rank | new |
| GET/POST/PATCH | /v3/crm/targets/bonus-rules · /{id} | {name, role, metric_key, bonus_type, tiers, effective_from, active}; 422 TIER_OVERLAP; GET /targets/{id}/bonus-estimates → BonusEstimate[] after close, read by PRD 06 | new |
| GET/PUT | /v3/school/digest-settings | add a targets: {enabled, metrics[]} block for the weekly digest (PRD 09) | extend |
8Permissions
| Permission | owner | branch_manager | admissions | academic_admin | accountant | teacher |
|---|---|---|---|---|---|---|
targets.view | all | branch | own | branch (academic group) | branch (revenue group) | own |
targets.manage (create, edit, publish) | yes | individual lines in branch | no | no | no | no |
targets.override, targets.reopen | yes | no | no | no | no | no |
targets.ranking.view | all | branch | branch or own (per setting) | no | no | no |
targets.export | all | branch | no | no | branch | no |
targets.bonus_rules (P2) | yes | no | no | no | view only | no |
targets.bonus_estimates.view (P2) | all | branch | own | no | all | own |
Parents have no permission in this module. The "own" scope returns only lines whose owner_id matches the user's profile plus the branch parent line read-only.
9Analytics events
| Event | Properties |
|---|---|
| target_created | period_type, metrics_count, copied_from_previous |
| target_allocated | level (branch|individual), method (manual|even|last_period_share), children_count, overridden |
| target_published | period_type, branch_lines, individual_lines, days_before_period_start |
| target_goal_changed | metric_key, scope, old, new, days_into_period, by_role |
| target_board_viewed | role, scope_tab, snapshot_age_hours |
| target_pace_alert_sent | metric_key, scope, pace, forecast_pct, kind (at_risk|recovered) |
| target_closed / target_reopened | grace_days, late_records_after_close |
| bonus_rule_saved / bonus_estimate_exported | role, metric_key, tiers_count / lines, total_amount |
10Edge cases
- A consultant moves branch mid-period: the line in the old branch stops on the transfer date and a new line is created in the new branch with a goal set by the new branch manager; each line counts only events within its date range.
- A consultant leaves mid-period: the line is flagged
inactive_owner, receives no alerts, still counts toward the branch total and stays in the ranking to period end. - A won lead is reopened in the same period: the actual drops at the next snapshot; after close the change does not affect the closed figures.
- A snapshot fails because a source errors: the board keeps showing the latest snapshot with its age; retry after 30 minutes, up to 3 times, then notify the owner.
- A late record after close (a payment on the 3rd of the next month for last month's invoice): counted in the period it was actually recorded; the count of late records is carried in the
target_closedevent so the accountant can decide whether to reopen.
11Open questions
- Should signed revenue use the expected amount of won leads (available now from the CRM) or the value of invoices issued for new enrollments (more accurate, but needs the lead-to-invoice link from PRD 01 and 05)? Proposal: expected amount in P1, invoice value in P2, with the definition change recorded in the Lịch sử tab.
- Should a quarterly target be bound to the sum of its three monthly targets? Proposal: no binding in P1, only a soft warning when the three months differ from the quarter.
- Should the ranking be visible to every consultant in the center? Proposal:
ranking_visibility = allas an option, branch by default.
Trường Số (truongso.net) · FLYER internal