# Built-in `hrm` — Nhân sự & lương (built-in — thay hr + payroll)

> 08/7/2026 · **Built-in module — WS runtime** (sẵn lúc boot, không cài qua .ogapp) · Bản HTML: [hrm-builtin.html](hrm-builtin.html) · [Review tổng core](core-review-2026-07-07.md) · [Quan hệ module](../quan-he-module-2026-07-07.md)
>
> 🎯 **Thay 2 module kế hoạch v1.5 (`hr` + `payroll`).** Hiện trạng: 18 model đầy đủ nhưng Payroll tổng quát (chưa có nguồn giờ DAT), chưa nối teacher. Đích = hrm + 2 phần đặc thù, KHÔNG scaffold mới.
>
> ✏️ **Cập nhật 08/7 (chiều):** chuẩn hóa **quy ước đặt permission key** (§3.1) + **bảng rename** key hiện có → key đích (§3.2) — doc này là source of truth, code rename theo bảng §3.2.

## 1. Vai trò
Nhân sự + lương trọn gói: hồ sơ nhân viên, hợp đồng, chấm công, nghỉ phép, bảng lương (tùy chọn: tuyển dụng, đào tạo nội bộ, KPI, đánh giá). Đặc thù trường lái: **GV ↔ Employee 1-1** và **timesheet** — lương GV sinh từ giờ dạy thực tế (DAT + LMS), không khai tay.

## 2. Feature / BO / Action

| Feature (key) | BO chính | Action |
|---|---|---|
| `personnel.employee` | Nhân viên (1 phòng + 1 chức danh + user) | view·create·edit·import·export |
| `personnel.contract` | Hợp đồng lao động + hạn | view·create·edit·configure |
| `time.attendance` | Chấm công (văn phòng + nguồn ngoài) | view·import·edit·export·**checkin** |
| `time.leave` | Nghỉ phép (quota + đơn) | view·create·edit·**approve** |
| `payroll.timesheet` | Bảng công GV từ giờ dạy thực tế ★đặc thù (nguồn: DAT, LMS) | view·export·**generate** |
| `payroll.salary` | Bảng lương (công thức giờ/khoán) | view·export·**generate**·**approve**·**lock** |
| `recruitment.* · training.* · performance.* · analytics.*` | Tuyển dụng · Đào tạo nội bộ · KPI/đánh giá · Dashboard (tùy chọn — ẩn qua config per-WS, **key giữ trong manifest**) | như manifest §3.3 |

## 3. Permission key — quy ước, bảng rename & manifest

### 3.1. Quy ước đặt key (chuẩn hóa — áp toàn nền tảng, không riêng hrm)

Cấu trúc 4 cấp `module.group.feature.action` giữ nguyên. Key là **định danh máy** (như AWS `service:Action`, K8s `verb×resource`); label là tên hiển thị.

| # | Quy tắc | Ví dụ vi phạm → sửa |
|---|---|---|
| **K1** | Key **bất biến sau phát hành**; label đổi tự do. Muốn đổi key → qua **cơ chế rename có khai báo** (manifest `renames` + grant-rewrite + bump epoch khi upgrade), không sửa tay rời rạc | — |
| **K2** | **Không lặp ngữ cảnh cha**: feature không chứa lại tên module/group | `employment-contract` → `contract` · `training-program` → `program` |
| **K3** | **Không để `group.feature` trùng tên** — dấu hiệu nhóm sai hoặc tên quá chung | `attendance.attendance`, `leave.leave` → group `time` · `payroll.payroll` → `payroll.salary` |
| **K4** | Key **không mã hóa nguồn dữ liệu/công nghệ** — nguồn là chi tiết của action `generate` | `timesheet-dat` → `timesheet` (nguồn DAT *và* LMS ngay trong thiết kế §4) |
| **K5** | Danh từ **số ít**, kebab-case chỉ khi cần phân biệt/ghép nghĩa | `job-position` **giữ** (tránh nhầm `org.position` — chức danh) |
| **K6** | Group = section nghiệp vụ ổn định theo chuẩn HRIS: `personnel · time · payroll · recruitment · training · performance · analytics` | gộp group `attendance` + `leave` → **`time`** (Time & Attendance) |

### 3.2. Bảng rename — key hiện có (code hrm v1.0.0) → key đích (v2.0.0)

> **Tham chiếu khi rename code.** Grant `kind:'module'` (owner) không bị ảnh hưởng; grant theo feature/action phải rewrite.

| Key hiện tại | Key đích | Quy tắc | Action thay đổi kèm theo |
|---|---|---|---|
| `personnel.employee` | *(giữ)* | — | **bỏ `delete`** (nghỉ việc = đổi status — giữ audit); giữ `import` (kit migrate WS3) |
| `personnel.employment-contract` | **`personnel.contract`** | K2 | bỏ `delete`; `approve` → bỏ (ký HĐ là quy trình giấy — trạng thái đủ); thêm `configure` (nhắc hạn) |
| `attendance.attendance` | **`time.attendance`** | K3·K6 | `create` → `checkin`/`import`; bỏ `delete` (sửa bằng `edit` có audit); bỏ `report`/`approve` |
| `leave.leave` | **`time.leave`** | K3·K6 | bỏ `delete` (hủy đơn = trạng thái); giữ `edit` |
| `payroll.payroll` | **`payroll.salary`** | K3 | bỏ `create`/`edit`/`delete` tay → `generate` từ công thức; thêm `lock` |
| — *(mới)* | **`payroll.timesheet`** | K4 | view·generate·export |
| `recruitment.job-position` | *(giữ)* | K5 | — |
| `recruitment.candidate` | *(giữ)* | — | — |
| `training.training-program` | **`training.program`** | K2 | — |
| `training.training-session` | **`training.session`** | K2 | — |
| `training.enrollment` | *(giữ)* | — | — |
| `performance.kpi-okr` | *(giữ)* | — | — |
| `performance.appraisal` | *(giữ)* | — | — |
| `analytics.dashboard` | *(giữ)* | — | — |

**Full code cũ → mới (các chuỗi đang hardcode ở FE portal/AppGrid — grep đúng các chuỗi này):**

```
hrm.personnel.employment-contract.view  →  hrm.personnel.contract.view
hrm.attendance.attendance.view          →  hrm.time.attendance.view
hrm.leave.leave.view                    →  hrm.time.leave.view
hrm.payroll.payroll.view                →  hrm.payroll.salary.view
hrm.training.training-program.view     →  hrm.training.program.view
(giữ nguyên: hrm.personnel.employee.view · hrm.recruitment.job-position.view ·
 hrm.performance.kpi-okr.view · hrm.performance.appraisal.view)
```

**Checklist ảnh hưởng khi rename code (thứ tự):**
1. `apps/api/src/modules/hrm/manifest.json` — key mới + `customActions` + version `2.0.0`.
2. BE enforce: grep code cũ trong routes/controllers hrm → thay theo bảng.
3. **Grant-rewrite** `rt_role`/`rt_role_assignment` (JSONB) trong DB **từng ws** + bump epoch — nên làm thành cơ chế `renames` trong manifest chạy lúc upgrade/boot, không chạy SQL tay.
4. CP catalog (`opengate_global`): nếu đã seed Feature key cũ cho hrm → update kèm; mapping trong `rbacExportService` rà lại.
5. FE: AppGrid + `HRM_SECTIONS.permPrefix` (chuỗi ở khối trên) — dài hạn thay bằng constants **sinh từ manifest** (codegen) để rename sau này vỡ lúc compile, không vỡ âm thầm.
6. Docs khác có ghi code cũ: [plan data-scope](../giai-doan-1-mvp/plan-data-scope-2026-07-06.md), ma trận WS0.

### 3.3. Manifest đích (v2.0.0 — JSON hợp lệ)

> ⚠️ **Điều kiện schema:** manifest-schema hiện tại (module-sdk) đã có `customActions` nhưng **chưa có** `fields`/`scopeVars` (chờ manifest v1.1 — [plan data-scope](../giai-doan-1-mvp/plan-data-scope-2026-07-06.md)) và `renames` (cơ chế K1). Thứ tự làm: **nâng schema trước → chạy `npm run catalog:validate` với manifest này = phải pass** rồi mới code.

Permission key: `hrm.<group>.<feature>.<action>`; `fields`/`scopeVars` theo [plan data-scope](../giai-doan-1-mvp/plan-data-scope-2026-07-06.md) (manifest v1.1). Action ngoài core-12 (`checkin`, `generate`, `lock`) **bắt buộc** khai `customActions` — nghĩa thống nhất theo [danh sách action đặc thù toàn hệ](../so-do-ws-module-feature-2026-07-06.md#4-tổng-hợp-số-lượng-đề-xuất).

```json
{
  "key": "hrm",
  "name": "Nhân sự & Lương",
  "version": "2.0.0",
  "customActions": [
    { "key": "generate", "label": "Sinh bảng", "desc": "Sinh bảng công/bảng lương từ dữ liệu nguồn (DAT/LMS) — không nhập tay." },
    { "key": "checkin", "label": "Điểm danh", "desc": "Ghi nhận chấm công tại chỗ — sinh bản ghi từ sự kiện thiết bị/thao tác nhanh, khác create." },
    { "key": "lock", "label": "Khóa sổ", "desc": "Khóa kỳ lương đã duyệt — bất biến; sai thì điều chỉnh kỳ sau (quy tắc §6.3)." }
  ],
  "permissions": [
    { "group": "personnel", "feature": "employee", "label": "Nhân viên",
      "actions": ["view", "create", "edit", "import", "export"],
      "fields": ["phong_ban_id", "chuc_danh_id", "user_id"],
      "scopeVars": ["current_employee_id", "current_dept_id"] },
    { "group": "personnel", "feature": "contract", "label": "Hợp đồng lao động",
      "actions": ["view", "create", "edit", "configure"] },
    { "group": "time", "feature": "attendance", "label": "Chấm công",
      "actions": ["view", "import", "edit", "export", "checkin"] },
    { "group": "time", "feature": "leave", "label": "Nghỉ phép",
      "actions": ["view", "create", "edit", "approve"] },
    { "group": "payroll", "feature": "timesheet", "label": "Bảng công GV (giờ DAT/LMS)",
      "actions": ["view", "generate", "export"],
      "fields": ["ma_gv", "ky"],
      "scopeVars": ["current_giao_vien_id"] },
    { "group": "payroll", "feature": "salary", "label": "Bảng lương",
      "actions": ["view", "generate", "approve", "lock", "export"],
      "fields": ["ma_nv", "ky"],
      "scopeVars": ["current_employee_id"] },
    { "group": "recruitment", "feature": "job-position", "label": "Vị trí tuyển dụng",
      "actions": ["view", "create", "edit", "delete"] },
    { "group": "recruitment", "feature": "candidate", "label": "Ứng viên",
      "actions": ["view", "create", "edit", "delete"] },
    { "group": "training", "feature": "program", "label": "Chương trình đào tạo",
      "actions": ["view", "create", "edit", "delete"] },
    { "group": "training", "feature": "session", "label": "Lớp/buổi đào tạo",
      "actions": ["view", "create", "edit", "delete"] },
    { "group": "training", "feature": "enrollment", "label": "Ghi danh đào tạo",
      "actions": ["view", "create", "edit", "delete"] },
    { "group": "performance", "feature": "kpi-okr", "label": "KPI / OKR",
      "actions": ["view", "create", "edit", "delete", "report"] },
    { "group": "performance", "feature": "appraisal", "label": "Đánh giá",
      "actions": ["view", "create", "edit", "delete", "approve", "report"] },
    { "group": "analytics", "feature": "dashboard", "label": "Dashboard nhân sự",
      "actions": ["view", "export"] }
  ]
}
```

> `recruitment.* / training.* / performance.* / analytics.*` = **tùy chọn**: ẩn/hiện qua config per-WS (Vĩnh An: quyết ở §8), **không xoá key khỏi manifest** — training tái dùng được cho *tập huấn định kỳ GV*.

## 4. Dữ liệu chính

### Nhân viên (khớp WS0 §4.3)
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
| Mã nhân viên | text | ✔ | GV: đồng bộ với ma_gv (teacher) |
| Họ tên | text | ✔ |  |
| CCCD | text | ✔ | Đối chiếu hồ sơ GV với Sở |
| SĐT / email | text | ✔/— |  |
| Phòng ban | ref org | ✔ | Một phòng chính |
| Chức danh | ref org | ✔ | Một chức danh chính |
| Tài khoản đăng nhập | ref iam | — | user_id — NV có đăng nhập |
| Ngày vào làm | date | — |  |
| Trạng thái | bool | ✔ |  |

### Hợp đồng
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
| Nhân viên | ref | ✔ |  |
| Loại hợp đồng | enum | ✔ | Thử việc / xác định / không xác định thời hạn |
| Ngày hiệu lực / hết hạn | date | ✔ | Hết hạn → notify 30/15/7 ngày |
| File scan / phụ lục | file | — |  |

### Bảng công GV (`payroll.timesheet`) ★
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
| Kỳ | text | ✔ | Tháng hoặc theo khóa (config) |
| Giáo viên | ref (ma_gv) | ✔ |  |
| Σ giờ DAT hợp lệ | number (generate) | ✔ | Từ phiên hop_le theo ma_gv — không nhập tay |
| Giờ lý thuyết | number | — | Từ lms (v1.5) |
| Điều chỉnh | number + lý do + người duyệt | — | Bắt buộc lý do — audit |

### Bảng lương (`payroll.salary`)
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
| Kỳ × nhân viên | ref | ✔ |  |
| Đơn giá giờ / khoán khóa | money | ✔ | Công thức config theo trung tâm |
| Phụ cấp / khấu trừ | money | — | Model hiện có sẵn BHXH/BHYT/BHTN/PIT — **giữ** (NV văn phòng lương đầy đủ) |
| Thực lãnh | money | ✔ |  |
| Trạng thái | enum | ✔ | nháp → duyệt → **đã khóa** (không sửa) |

## 5. Ma trận quyền — vai trò × feature

| Feature | QTHT | GIAM_DOC | QL_DAO_TAO | TUYEN_SINH | GIAO_VU | GIAO_VIEN | KE_TOAN | NHAN_SU |
|---|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| `personnel.employee` | R | R | R | — | — | R² | R | W |
| `personnel.contract` | R | R | — | — | — | R² | — | W |
| `time.attendance` | R | — | R | — | — | R² | — | W |
| `payroll.timesheet` | R | R | R | — | — | R² | W | R |
| `time.leave` | R | — | A | — | — | W² | — | A |
| `payroll.salary` | R | R | R | — | — | R² | W·A | R |

² chỉ của chính mình · **W·A** = tạo/sửa + duyệt

## 6. Quy tắc nghiệp vụ
1. **GV = Employee**: teacher.ma_gv ↔ mã NV (1-1); teacher chỉ giữ hồ sơ nghiệp vụ dạy — không trùng dữ liệu
2. Nguồn giờ GV **duy nhất** = phiên DAT hợp lệ + buổi lms; điều chỉnh tay phải có lý do + duyệt
3. Kỳ lương đã `lock` không sửa — sai thì điều chỉnh kỳ sau
4. Hết hạn hợp đồng/chứng chỉ tập huấn → notify. **Ranh giới:** `hrm.personnel.contract` = HĐLĐ + giấy tờ *lao động*; `teacher.cert` = chứng chỉ *nghiệp vụ dạy* (đăng ký Sở) — notify rule tách theo đó, không nhắc trùng
5. Data scope: NV/GV xem hồ sơ + lương **của chính mình**; quản lý theo phòng (org). ⚠️ Cần thêm cột `user_id` + `cccd` vào model Employee hiện có (chưa có) — điều kiện của scopeVar `current_employee_id`
6. `payroll.timesheet` phụ thuộc module `dat` (.ogapp cài/gỡ được): mount check view `dat_v_phien_hop_le` tồn tại — vắng thì **ẩn feature (degrade)**, không crash (rule 5 [quan hệ module](../quan-he-module-2026-07-07.md#4-quy-tắc-cứng-bổ-sung-vào-huong_danmd-khi-code))

## 7. Liên kết chéo
- **teacher** — 1-1 Employee (mã NV)
- **dat** — nguồn giờ timesheet (degrade khi vắng — §6.6)
- **lms** — giờ dạy lý thuyết
- **org** — phòng/chức danh
- **iam** — user đăng nhập (`Employee.user_id`)
- **notify** — nhắc hạn HĐ
- **finance** — chi lương vào sổ

## 8. Câu hỏi mở
- Công thức lương GV Vĩnh An (giờ/khoán/hỗn hợp)? Kỳ theo tháng hay khóa?
- Bật recruitment/training/KPI/dashboard hay ẩn (config theo WS)?
- Quy tắc mã NV hiện dùng (đồng bộ ma_gv)?
- Nguồn chấm công văn phòng: máy chấm công riêng hay dùng chung face terminal của LMS? (quyết `time.attendance.import` nối với gì)
