← dashboard  ·  ← User guide

HELIDIRECT 2026 KPI Dashboard — Owner Playbook

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


Mục lục

  1. Kiến trúc 1 trang giấy
  2. Quy trình chuẩn: sửa code → deploy
  3. Cookbook — các tác vụ thường gặp
  4. Data import — CSV/XLSX format
  5. Sync với Google Sheet master
  6. Deploy / rollback
  7. Bật GAS auth (production hardening)
  8. Test workflow
  9. Cấu trúc thư mục — quick reference
  10. Troubleshooting cho owner

1. Kiến trúc 1 trang giấy

┌────────────────────────────────────────────────────────┐
│                    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                    │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────┘

Phân biệt 2 loại file JS

Loại Folder Đặc điểm Khi nào sửa
ES6 module src/*.js 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/.


2. Quy trình chuẩn: sửa code → deploy

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.


3. Cookbook — các tác vụ thường gặp

3.1 Đổi target 1 KPI

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 (.38 cho 38%). USD là số nguyên. x là số (vd 25). days là ngày.

Test → build → deploy theo Section 2.


3.2 Thêm KPI mới cho 1 role

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: { ... }},

Các field bắt buộc của 1 KPI

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ố.


3.3 Đổi trọng số (weight) các KPI của 1 role

Tương tự 3.2 — chỉ sửa field w. Nguyên tắc:


3.4 Đổi base salary bonus

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,
  // ...
};

3.5 Đổi GMV forecast H1 hoặc H2

Cách A — Qua UI (cho COO + BU manager)

  1. Đăng nhập COO hoặc BU manager
  2. Click ⚙ → mục H2 Forecast Targets
  3. Đổi từng tháng, click Save

→ Lưu vào localStorage browser, KHÔNG persist cho user khác.

Cách B — Sửa code (persistent cho mọi user)

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.


3.6 Đổi RATES (P&L assumptions) cho BU

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:


3.7 Thêm role mới

Use case: Thêm role "CAR-CS" (customer support).

Files sửa:

  1. src/roles.js — thêm vào mảng ROLES
  2. src/roles.js — thêm BONUS_BASE['CAR-CS'] = 2500000
  3. src/roles.js — thêm ROLE_PINS['CAR-CS'] = '1104'
  4. src/gas-perm.js (nếu role này thuộc BU CAR) — thêm vào BU_TO_ROLES['CAR-MGR']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.


3.8 Thêm BU mới

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.


3.9 Thêm/đổi 1 string i18n

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:

  1. Thêm key vào CẢ en.jsvi.js (nếu skip 1 cái, sẽ fallback EN)
  2. Trong HTML: <span data-i18n="my_new_key">fallback text</span>
  3. Trong JS legacy: const _t = (k, fb) => window.__HD?.i18n?.t(k, fb) ?? fb; ... _t('my_new_key', 'fallback')

3.10 Đổi PIN của role

Cách A — Qua UI (cho COO):

  1. Đăng nhập COO → ⚙ → Team PIN Management
  2. Sửa PIN từng role → Save

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).


3.11 Đổi email distribution

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.


4. Data import — CSV/XLSX format

Format chuẩn

KPI actuals

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)

GMV actuals

Column Required Example
bu CAR, HELI, JET
month 5
value hoặc actual 478000

Lưu ý

Lấy template

Mở dashboard → COO → ⚙ → ... HOẶC click Import ActualsDownload Template CSV ngay trong modal.


5. Sync với Google Sheet master

Hiện tại dashboard fetch từ Google Sheet ID 1ffeWfYwb3mFzBlASFvJD_nAlkjDiX27qWN7jMkrwrfM (xem src/constants.jsSHEET_ID).

Sheet này phải:

  1. Public ("Anyone with the link → Viewer")
  2. Có 3 tabs: gmv, kpi, pl_actual

Schema từng tab

Tab 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 ... ... ... ... ... ... ... ... ...

Quy trình sync

Đổi Sheet ID

Sửa src/constants.jsSHEET_ID = '...' → rebuild + deploy.


6. Deploy / rollback

Deploy bản mới

Theo Section 2.

Rollback nhanh (revert về bản trước)

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

Rollback hoàn toàn về file gốc (single-file 165KB)

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

7. Bật GAS auth (production hardening)

Đâ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:

  1. Tạo OAuth Client ID trên Google Cloud Console (~5 phút)
  2. Tạo Apps Script project, paste app/gas/Code.gs (~3 phút)
  3. Set 4 Script Properties: SHEET_ID, ALLOWED_DOMAIN, OAUTH_CLIENT_ID, ROLE_MAP (~5 phút)
  4. Deploy as Web App "Execute as: Me", "Who has access: Anyone" (~2 phút)
  5. Edit src/config.js → dán GAS_URL + OAUTH_CLIENT_ID + flip USE_GAS_AUTH = true
  6. Rebuild + deploy
  7. Test sign-in flow với 1 user trước khi rollout

Khi cần thêm/xoá user

Vào Apps Script editor → Project Settings → Script Properties → edit ROLE_MAP JSON → save. Không cần re-deploy.


8. Test workflow

Chạy test

cd "/Users/huy.nguyen/Desktop/2026 Forecast I KPI Setting & Tracking/app"
npm test

Hiện có 131 tests trong 8 files:

Watch mode (auto-run khi save)

npm run test:watch

Coverage

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);
  });
});

9. Cấu trúc thư mục — quick reference

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

10. Troubleshooting cho owner

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

Liên hệ hỗ trợ kỹ thuật


Phụ lục — Checklist trước mỗi lần deploy


Bản v1.0 — 2026-05-25. Update khi codebase thay đổi lớn.