Tài liệu này dành cho bạn — dashboard owner. Cover toàn bộ vòng đời: thêm/đổi KPI, target, role, BU, period, đến deploy + rollback + bật GAS auth.
URL production: https://huynguyen-hdrc.github.io/superb-stroopwafel-69d951/
Source code: ~/Desktop/2026 Forecast I KPI Setting & Tracking/app/
Deploy repo: ~/Documents/GitHub/superb-stroopwafel-69d951/ (branch gh-pages)
Backup file gốc: ~/Desktop/index.html.backup-20260525.html
┌────────────────────────────────────────────────────────┐
│ BROWSER │
│ ┌──────────────────────────────────────────────────┐ │
│ │ index.html (22 KB) │ │
│ │ ├─ <script type="module" src="main.js"> │ │
│ │ │ │ │ │
│ │ │ ├─→ constants.js (data: MONTHS, GMV,...)│ │
│ │ │ ├─→ roles.js (14 roles, KPIs, PINs) │ │
│ │ │ ├─→ calc.js (P&L, score, bonus — pure) │ │
│ │ │ ├─→ format.js (fmt USD/%/x — pure) │ │
│ │ │ ├─→ sanitize.js (XSS escape) │ │
│ │ │ ├─→ perm.js (canSeeBU/Role) │ │
│ │ │ ├─→ storage.js (LS schema v2 + migrate)│ │
│ │ │ ├─→ i18n/{en,vi}.js (~55 strings) │ │
│ │ │ ├─→ api.js (fetchData) │ │
│ │ │ └─→ auth-gsi.js (Google Sign-In) │ │
│ │ │ │ │
│ │ ├─ <script src="legacy/00-core.js"> │ │
│ │ ├─ <script src="legacy/01-sync.js"> │ │
│ │ ├─ <script src="legacy/02-utils-modals.js"> │ │
│ │ ├─ <script src="legacy/03-render.js"> (1.3K) │ │
│ │ └─ <script src="legacy/04-import-pingate.js">│ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ fetch │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Google Sheets (gviz API, public sheet) │ │
│ │ Tab: gmv / kpi / pl_actual │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
| Loại | Folder | Đặc điểm | Khi nào sửa |
|---|---|---|---|
| ES6 module | src/*.js |
Có import / export. Pure, testable |
Sửa logic chính: KPI definitions, calculations, i18n |
| Classic script | public/legacy/*.js |
Window globals, không import. Cũ. | Sửa code render UI (KPI cards, P&L tables, charts) |
Quy tắc: nếu tác vụ liên quan đến data/logic → sửa src/. Nếu liên quan đến UI render → sửa public/legacy/.
Mọi tác vụ trong Cookbook đều theo workflow này:
# 1. Vào thư mục app
cd "/Users/huy.nguyen/Desktop/2026 Forecast I KPI Setting & Tracking/app"
# 2. Sửa file (xem cookbook bên dưới biết sửa file nào)
# ...
# 3. Test logic vẫn pass
npm test
# Phải thấy: Tests 131 passed (hoặc số cao hơn nếu thêm test mới)
# 4. Chạy dev server xem trực quan
npm run dev
# Browser tự mở http://localhost:5173 — kiểm tra UI
# 5. Khi OK, dừng dev server (Ctrl+C trong Terminal)
# 6. Build production với BASE đúng
BASE=/superb-stroopwafel-69d951/ npm run build
# Output ở dist/
# 7. Copy vào repo gh-pages
REPO="/Users/huy.nguyen/Documents/GitHub/superb-stroopwafel-69d951"
DIST="/Users/huy.nguyen/Desktop/2026 Forecast I KPI Setting & Tracking/app/dist"
rm -rf "$REPO/assets" "$REPO/legacy" # xoá bản cũ
cp -R "$DIST/." "$REPO/"
# 8. Mở GitHub Desktop → Commit + Push
# Hoặc qua terminal:
cd "$REPO"
git add -A
git commit -m "Update: <mô tả ngắn>"
# Push qua GitHub Desktop (terminal push thường fail auth)
# 9. Đợi ~1-2 phút, hard reload URL production
Lưu cheatsheet này — bạn sẽ lặp lại nó mỗi lần sửa.
Use case: Mid-year review, cần đổi target CM3 của CAR Manager từ 36.2% → 38%.
File sửa: src/roles.js
Tìm role bằng key (vd CAR-MGR), trong mảng kpis:, sửa tgt:
{ key: 'CAR-MGR', label: 'bu manager', bu: 'CAR', group: 'car', mgr: true, kpis: [
{ n: 'gmv', tgt: 467768, ... },
{ n: 'gross margin %', tgt: .50, ... },
{ n: 'cm3 %', tgt: .362, ... }, // ← đổi 0.362 → 0.38
], ...},
⚠️ Đơn vị: percent là decimal (
.38cho 38%). USD là số nguyên.xlà số (vd25).dayslà ngày.
Test → build → deploy theo Section 2.
Use case: CAR Buyer thêm 1 KPI "supplier on-time delivery" trọng số 15%.
Vấn đề: tổng trọng số hiện tại của CAR Buyer = 60+20+20 = 100%. Thêm 15% sẽ vượt → phải giảm các KPI khác.
File sửa: src/roles.js
{ key: 'CAR-BUYER', label: 'buyer', bu: 'CAR', group: 'car', mgr: false, kpis: [
{ n: 'gross margin (%gmv)', tgt: .60, u: '%', w: .50, dir: 'H', mock: .455, fmt: 'pct', ctx: '...' }, // 60 → 50
{ n: 'inv. turnover', tgt: .20, u: '%', w: .20, dir: 'H', mock: .200, fmt: 'pct', ctx: '...' },
{ n: 'oos rate (active skus)',tgt: .20, u: '%', w: .15, dir: 'L', mock: .038, fmt: 'pct', ctx: '...' }, // 20 → 15
{ n: 'supplier on-time delivery', tgt: .95, u: '%', w: .15, dir: 'H', mock: .90, fmt: 'pct',
ctx: 'Tỉ lệ supplier giao đúng hạn. ↑ cao hơn là tốt. Weekly.' }, // ← NEW
], context: { ... }},
| Field | Kiểu | Ví dụ | Ý nghĩa |
|---|---|---|---|
n |
string | 'gross margin %' |
Tên hiển thị |
tgt |
number | .50 hoặc 467768 |
Target. % là decimal |
u |
string | '%', 'USD', 'x', 'days', 'kws' |
Đơn vị |
w |
number | .40 |
Trọng số. Tổng các w trong role nên = 1.00 |
dir |
'H' hoặc 'L' |
'H' |
H = higher is better, L = lower (vd OOS rate) |
mock |
number | .448 |
Số giả lúc dashboard chưa có actual |
fmt |
'pct', 'vnd', 'num' |
'pct' |
Format khi hiển thị |
ctx |
string | 'Note giải thích…' |
Tooltip / context cho user |
Test: chạy npm test. Test sẽ pass vì weightedScore() normalize theo tổng trọng số.
Tương tự 3.2 — chỉ sửa field w. Nguyên tắc:
w trong 1 role không nhất thiết = 1.0. Code chuẩn hoá tự động (weightedScore chia cho tổng weight thực có).Use case: tăng base CAR Manager từ 7.5M → 8M VND/tháng.
File sửa: src/roles.js
export const BONUS_BASE = {
'CAR-MGR': 7500000, // ← đổi
'CAR-BUYER': 3000000,
// ...
};
→ Lưu vào localStorage browser, KHÔNG persist cho user khác.
File sửa: src/constants.js
export const GMV_FC = {
CAR: [504171, 471923, 421177, 399851, 467768, 480242, 561321, 467768, 467768, 498952, 561321, 629303],
// ^ Jun = index 5 — đổi tại đây
HELI: [...],
JET: [...],
};
12 phần tử = Jan..Dec.
Use case: COGS rate của CAR giảm từ 50% → 48% do supplier mới.
File sửa: src/constants.js
export const RATES = {
CAR: { ret: .045, cogs: .50, ship_in: .020, ship_out: .025, promo: .095, mkt: .04 },
// ^ đổi .50 → .48
HELI: { ... },
JET: { ... },
};
Tác động: Mọi P&L card trên dashboard tự cập nhật (forecast lẫn MTD).
Field nghĩa:
ret = return rate (% GMV)cogs = cost of goods (% GMV)ship_in = inbound shipping (% GMV)ship_out = outbound shipping (% GMV)promo = promo spend (% GMV)mkt = marketing spend (% GMV)Use case: Thêm role "CAR-CS" (customer support).
Files sửa:
src/roles.js — thêm vào mảng ROLESsrc/roles.js — thêm BONUS_BASE['CAR-CS'] = 2500000src/roles.js — thêm ROLE_PINS['CAR-CS'] = '1104'src/gas-perm.js (nếu role này thuộc BU CAR) — thêm vào BU_TO_ROLES['CAR-MGR'] và SPECIALIST_BUS['CAR-CS'] = ['CAR']// 1. Thêm vào ROLES (cuối nhóm CAR)
{ key: 'CAR-CS', label: 'customer support', bu: 'CAR', group: 'car', mgr: false, kpis: [
{ n: 'csat score', tgt: .90, u: '%', w: .50, dir: 'H', mock: .85, fmt: 'pct', ctx: '...' },
{ n: 'ticket resolution time', tgt: 24, u: 'days', w: .30, dir: 'L', mock: 28, fmt: 'num', ctx: '...' },
{ n: 'first response time', tgt: 2, u: 'days', w: .20, dir: 'L', mock: 3, fmt: 'num', ctx: '...' },
], context: { type: 'ops', bu: 'CAR', headline: 'your impact on car cx', impact: '...' }},
// 2. Thêm BONUS_BASE
'CAR-CS': 2500000,
// 3. Thêm ROLE_PINS
'CAR-CS': '1104',
// 4. Trong src/gas-perm.js:
const BU_TO_ROLES = {
'CAR-MGR': ['CAR-MGR', 'CAR-BUYER', 'CAR-OPS', 'CAR-CS'], // ← thêm CAR-CS
// ...
};
const SPECIALIST_BUS = {
// ...
'CAR-CS': ['CAR'], // ← thêm
};
Đồng bộ Code.gs (nếu dùng GAS auth): copy logic từ src/gas-perm.js sang gas/Code.gs các function getVisibleBUs_ / getVisibleRoleKeys_.
Update USER_GUIDE.md: thêm PIN mới vào bảng PIN ở section 2.
Test: npm test. Bài gas-perm.test.js sẽ có thể fail vì parity table cứng — sửa nó để match.
Use case rất hiếm — code hiện hardcode 3 BU. Cần đụng nhiều file:
| File | Thay đổi |
|---|---|
src/constants.js |
Thêm key trong GMV_FC, GMV_ACT, RATES |
src/roles.js |
Thêm các role thuộc BU mới |
src/perm.js |
Update buMap + grpBU mapping |
src/gas-perm.js |
Update BU_TO_ROLES, SPECIALIST_BUS, getVisibleBUs |
gas/Code.gs |
Mirror các thay đổi của gas-perm |
gas/README.md |
Update permission matrix |
public/legacy/02-utils-modals.js |
_renderH2FC — thêm BU vào dropdown |
public/legacy/03-render.js |
Render BU breakdown cards — sửa các grid layout |
tests/perm.test.js + tests/gas-perm.test.js |
Cập nhật assertions |
Nên consider scope kỹ trước khi làm. Nếu chỉ thêm role thì stick với 3.7.
Use case: Đổi label "weekly pulse" thành "weekly review" (EN).
File sửa: src/i18n/en.js
tab_weekly: 'weekly review', // đổi từ 'weekly pulse'
Tương ứng src/i18n/vi.js:
tab_weekly: 'tổng kết tuần', // tiếng Việt tương ứng
Để thêm string mới:
en.js VÀ vi.js (nếu skip 1 cái, sẽ fallback EN)<span data-i18n="my_new_key">fallback text</span>const _t = (k, fb) => window.__HD?.i18n?.t(k, fb) ?? fb; ... _t('my_new_key', 'fallback')Cách A — Qua UI (cho COO):
Lưu vào localStorage browser — chỉ active trên browser đó.
Cách B — Sửa code (default cho mọi user):
File sửa: src/roles.js
export const ROLE_PINS = {
'COO': '2026',
'CAR-MGR': '1101',
// ... đổi giá trị
};
⚠️ PIN ở client-side KHÔNG bảo mật thật. Bất kỳ ai mở DevTools đều thấy. Nếu data thực sự nhạy cảm, bật GAS auth (Section 7).
Use case: Setup email cho mỗi role để có thể gửi báo cáo KPI hàng tháng.
Cách: COO → ⚙ → Team Email Addresses → nhập email từng role → Save.
Lưu vào localStorage (per browser).
Sau khi setup, vào dashboard COO/Manager → click nút "Send Email" → mở modal với preview, click send → mở email client với mailto link đã pre-fill.
| Column | Required | Format | Example |
|---|---|---|---|
role_key |
✅ | String (xem ROLES) | CAR-BUYER |
kpi_index |
✅ | Number, 0-based | 0 (KPI đầu tiên của role) |
month |
✅ | Number 1-12 | 5 (May) |
value |
✅ | Number, theo đơn vị KPI | 0.482 (cho 48.2%) hoặc 467768 (USD) |
| Column | Required | Example |
|---|---|---|
bu |
✅ | CAR, HELI, JET |
month |
✅ | 5 |
value hoặc actual |
✅ | 478000 |
role hoặc role_key; kpi_idx hoặc kpi_index hoặc index; value / actual / val1,234,567 → 1234567Mở dashboard → COO → ⚙ → ... HOẶC click Import Actuals → Download Template CSV ngay trong modal.
Hiện tại dashboard fetch từ Google Sheet ID 1ffeWfYwb3mFzBlASFvJD_nAlkjDiX27qWN7jMkrwrfM (xem src/constants.js → SHEET_ID).
Sheet này phải:
gmv, kpi, pl_actualTab gmv:
| month | CAR | HELI | JET |
|---|---|---|---|
| 1 | 521000 | 1015000 | 133000 |
| ... | ... | ... | ... |
Tab kpi:
| role_key | role_name | kpi_0 | kpi_1 | kpi_2 | kpi_3 |
|---|---|---|---|---|---|
| CAR-MGR | bu manager | 467768 | 0.448 | 0.271 |
Tab pl_actual (company-level):
| month | returns | cogs | gross_margin | shipping | discount | marketing | ecom_fee | cm3 | opex |
|---|---|---|---|---|---|---|---|---|---|
| 1 | ... | ... | ... | ... | ... | ... | ... | ... | ... |
src/constants.js → AUTO_REFRESH_MS)Sửa src/constants.js → SHEET_ID = '...' → rebuild + deploy.
Theo Section 2.
Nếu deploy bản mới gặp lỗi:
cd /Users/huy.nguyen/Documents/GitHub/superb-stroopwafel-69d951
# Xem 5 commit gần nhất
git log --oneline -5
# Revert commit gần nhất (tạo commit revert, an toàn hơn reset)
git revert HEAD --no-edit
# Push qua GitHub Desktop
cp ~/Desktop/index.html.backup-20260525.html /Users/huy.nguyen/Documents/GitHub/superb-stroopwafel-69d951/index.html
cd /Users/huy.nguyen/Documents/GitHub/superb-stroopwafel-69d951
rm -rf assets legacy .nojekyll
git add -A
git commit -m "Revert: roll back to single-file monolith"
# Push qua GitHub Desktop
Đây là bước nâng cấp tuỳ chọn — chỉ làm khi muốn:
Hướng dẫn deploy đầy đủ ở app/gas/README.md (7 steps). Tóm tắt:
app/gas/Code.gs (~3 phút)SHEET_ID, ALLOWED_DOMAIN, OAUTH_CLIENT_ID, ROLE_MAP (~5 phút)src/config.js → dán GAS_URL + OAUTH_CLIENT_ID + flip USE_GAS_AUTH = trueVào Apps Script editor → Project Settings → Script Properties → edit ROLE_MAP JSON → save. Không cần re-deploy.
cd "/Users/huy.nguyen/Desktop/2026 Forecast I KPI Setting & Tracking/app"
npm test
Hiện có 131 tests trong 8 files:
calc.test.js — P&L, score, bonusformat.test.js — formatterssanitize.test.js — XSS escapeperm.test.js — client permissiongas-perm.test.js — server permission (mirror)storage.test.js — localStorage migrate + persisti18n.test.js — translation lookupapi.test.js — data fetch routingnpm run test:watch
Khi thêm KPI/role/feature mới, nếu liên quan đến pure logic → viết test. Pattern:
import { describe, it, expect } from 'vitest';
import { yourFunction } from '../src/yourModule.js';
describe('yourFunction', () => {
it('does the thing', () => {
expect(yourFunction(input)).toBe(expectedOutput);
});
});
app/
├── package.json ← deps (vite, vitest)
├── vite.config.js ← build config + test setup
├── index.html ← HTML shell + data-i18n attrs
├── README.md ← dev quickstart
├── .github/workflows/ ← CI (test + build, không tự deploy)
│
├── src/ ◄ MAIN — sửa logic ở đây
│ ├── main.js ← entry, wire i18n + auth
│ ├── styles.css ← CSS (~5K lines compact)
│ ├── constants.js ← MONTHS, GMV_FC, GMV_ACT, RATES, SHEET_ID
│ ├── roles.js ← ROLES[], BONUS_BASE, ROLE_PINS
│ ├── state.js ← runtime mutable state
│ ├── calc.js ← achScore, calcPL, bonusLevel — PURE
│ ├── format.js ← fmtN/P/X — PURE
│ ├── sanitize.js ← XSS escape — PURE
│ ├── perm.js ← client permission filter
│ ├── storage.js ← localStorage v2 schema + migrate
│ ├── api.js ← fetchData (gviz hoặc GAS)
│ ├── auth-gsi.js ← Google Identity Services
│ ├── gas-perm.js ← server permission mirror — PURE
│ ├── config.js ← USE_GAS_AUTH flag, GAS_URL
│ └── i18n/
│ ├── en.js ← English dictionary
│ ├── vi.js ← Vietnamese dictionary
│ └── index.js ← t(), setLang(), applyTranslations()
│
├── public/ ◄ Vite copies as-is into dist/
│ └── legacy/ ◄ Classic scripts (window globals)
│ ├── 00-core.js ← Chart defaults + esc()
│ ├── 01-sync.js ← fetchActuals (gviz)
│ ├── 02-utils-modals.js← config modal (PIN/email/H2 FC)
│ ├── 03-render.js ← all panel render functions (1.3K LOC)
│ └── 04-import-pingate.js ← CSV/XLSX import + PIN gates
│
├── gas/ ◄ Apps Script backend
│ ├── Code.gs ← server entry + auth + filter
│ └── README.md ← deploy guide
│
├── tests/ ◄ Vitest
│ ├── _setup.js ← localStorage shim
│ └── *.test.js ← 8 files, 131 tests
│
├── docs/ ◄ Tài liệu
│ ├── USER_GUIDE.md ← cho nhân viên
│ └── OWNER_GUIDE.md ← cho bạn (đây)
│
└── understand/ ◄ Knowledge graph (analysis)
├── 05-graph.json ← unified KG (168 nodes, 327 edges)
├── tours-index.md ← 3 pedagogical tours
└── tour-{newcomer,security,business}.md
| Vấn đề | Nguyên nhân + fix |
|---|---|
npm test fail "expected X to be Y" |
Logic thay đổi, test cũ outdated. Đọc kỹ assertion → sửa test |
npm run build fail "Could not resolve" |
Import path typo. Đọc error → fix path |
| Deploy xong nhưng URL vẫn hiển thị bản cũ | GH Pages cache. Cmd+Shift+R. Đợi 1-2 phút |
| Sửa code nhưng dashboard không update | Quên rebuild + push. Quay lại Section 2 |
Sửa src/roles.js xong, role không hiện |
Quên thêm vào perm filter (src/perm.js + src/gas-perm.js) |
Test gas-perm.test.js fail sau khi thêm role |
Parity table trong test cứng — update assertion |
| 1 user kêu không thấy data của role X | Permission filter đúng — họ không có quyền. Hỏi xem cần expand permission không |
Console error HD_esc is not defined |
Script load order sai. Verify legacy/00-core.js load TRƯỚC các file legacy khác |
| Bonus tự nhiên về 0 cho 1 role | Tổng score < 80%. Check achScore từng KPI |
| Mất data sau khi deploy bản mới | localStorage schema version mismatch. Đảm bảo SCHEMA_VERSION trong storage.js không tự bump khi không có migration logic |
npm test pass tất cảnpm run dev xem dashboard workBASE=/superb-stroopwafel-69d951/ npm run builddist/ vào repoBản v1.0 — 2026-05-25. Update khi codebase thay đổi lớn.