# Tài liệu đặc tả kỹ thuật — CRM Quản trị Bán hàng (Mediworld)

**Phiên bản:** 1.2 — 27/09/2026 (rà soát cuối: thêm mục 1.4 vòng vận hành hằng ngày; thêm mục 7.6 Nhập Excel hàng loạt — tính năng có thật nhưng trước đó chưa được mô tả; sửa số bảng dữ liệu 48→49 và một dòng dễ hiểu nhầm ở Phụ lục A; bổ sung route còn thiếu ở Phụ lục B)
**Trạng thái yêu cầu gốc:** toàn bộ nội dung file "Tính năng CRM.xlsx" và phiếu hỏi-đáp bổ sung đã được triển khai hoặc có quyết định rõ ràng (xem Mục 17). Đối chiếu từng dòng nằm ở sheet "Đối chiếu Tính năng CRM" trong `trao-doi-khach/bo-test-case-2026-09-19.xlsx`.

## 0. Về tài liệu này

**Mục đích:** mô tả đầy đủ, có cấu trúc, những gì hệ thống làm và làm như thế nào — phục vụ ba việc: (1) xác nhận tính đầy đủ so với yêu cầu của khách hàng, (2) bàn giao cho người tiếp nhận vận hành/bảo trì, (3) làm nền để phát triển/mở rộng về sau mà không phải đọc lại toàn bộ mã nguồn.

**Đây không phải tài liệu duy nhất.** Bốn tài liệu khác đi kèm, mỗi tài liệu một vai trò riêng — đọc tài liệu này để biết **hệ thống làm gì**, đọc các tài liệu kia để biết **cách vận hành/mở rộng**:

| Tài liệu | Trả lời câu hỏi |
|---|---|
| `DAC-TA-KY-THUAT.md` (tài liệu này) | Hệ thống có những tính năng gì, quy tắc nghiệp vụ ra sao, dữ liệu tổ chức thế nào? |
| `CLAUDE.md` | Quy ước viết mã, các lỗi đã gặp và cách né, ghi chú kỹ thuật dày đặc cho người/AI trực tiếp sửa code |
| `HUONG-DAN-TRIEN-KHAI-HAWKHOST.md` | Các bước triển khai lên máy chủ HawkHost, cập nhật phiên bản, migration nào cần chạy |
| `TAI-LIEU-BAN-GIAO-VA-BAO-TRI.md` | Quyền sở hữu mã nguồn, sao lưu/khôi phục, giám sát, xử lý sự cố, phạm vi bảo trì |
| `THIRD-PARTY-LICENSES.md` | Danh sách giấy phép của 186 thư viện bên thứ ba |

**Quy ước đọc:** mỗi mục tính năng được đánh số theo đúng thứ tự trong file yêu cầu gốc của khách hàng ("Tính năng CRM.xlsx") để dễ đối chiếu ngược. Đường dẫn URL viết theo tiếng Việt không dấu, khớp menu thật của ứng dụng. Tên bảng/hàm trong dấu `` ` `` là tên thật trong mã nguồn — tìm bằng chính tên đó sẽ ra đúng chỗ.

---

## 1. Tổng quan kiến trúc

| Thành phần | Lựa chọn |
|---|---|
| Framework | Next.js 15 (App Router) + React 19, server actions cho mọi thao tác ghi |
| Cơ sở dữ liệu | PostgreSQL, truy vấn qua Drizzle ORM (SQL tường minh, không ORM ẩn logic) |
| Máy dev | PostgreSQL nhúng (PGlite) — không cần cài đặt gì thêm |
| Máy chủ thật | PostgreSQL thật (Neon/Supabase/VPS) — đổi 2 dòng cấu hình, không sửa code |
| Xác thực | JWT trong cookie `httpOnly` (thư viện `jose`) + băm mật khẩu `scrypt` (Node gốc) — không có bảng session riêng |
| Giao diện | Tailwind CSS v4, tối ưu cho điện thoại (menu di động, bảng chuyển thành thẻ dưới 1024px) |
| Biểu đồ | Recharts |
| Kiểm thử | Vitest (103 test tự động: 81 nhanh + 22 khoá quyền) + CI GitHub Actions |

### 1.1 Đa khách thuê (multi-tenant)

Một cơ sở dữ liệu phục vụ nhiều công ty (`tenants`). **Mọi bảng nghiệp vụ đều có cột `tenant_id`, và mọi truy vấn đều lọc theo cột này** — đây là ranh giới bảo mật quan trọng nhất của hệ thống: dữ liệu công ty A không bao giờ lẫn sang công ty B, kể cả khi có lỗi ở tầng khác. Bản demo cho Mediworld chạy trên một `tenant` (`Công ty TNHH Dược Mỹ phẩm Minh Anh`); thêm khách hàng mới chỉ là thêm một `tenant`, không cần cài đặt riêng.

### 1.2 Mô hình phân quyền dữ liệu (chi tiết ở Mục 2)

Mọi màn hình, mọi báo cáo, mọi thao tác ghi đều đi qua một hàm trung tâm `scopeFilter(session, cột)` (`src/lib/rbac.ts`) quyết định người xem thấy được những bản ghi nào. Đây là nguyên tắc thiết kế cốt lõi: **không có màn hình nào tự viết điều kiện lọc riêng** — tất cả gọi lại đúng một hàm, nên sửa quy tắc phân quyền ở một chỗ là có hiệu lực toàn hệ thống, và không có nguy cơ một màn hình quên lọc.

### 1.3 Bố cục mã nguồn

```
src/
├── db/schema.ts            Toàn bộ mô hình dữ liệu (49 bảng), chú thích theo từng mục yêu cầu
├── db/index.ts             Kết nối cơ sở dữ liệu (PGlite hoặc PostgreSQL theo biến môi trường)
├── lib/                    Toàn bộ logic nghiệp vụ dùng chung, tách khỏi giao diện
│   ├── auth.ts             Đăng nhập, phiên làm việc
│   ├── rbac.ts             scopeFilter() và mọi hàm phạm vi dữ liệu
│   ├── accounts.ts         Quy tắc quản lý tài khoản (ai quản được ai)
│   ├── classification.ts   Phân hạng đại lý A–E
│   ├── kpi.ts, reports.ts, sales-analysis.ts   Tính toán báo cáo/phân tích
│   ├── alerts.ts, forecast.ts   Cảnh báo tự động, dự báo dự trù
│   ├── audit.ts, audit-view.ts  Ghi và hiển thị nhật ký chỉnh sửa
│   └── ... (mỗi tính năng lớn một tệp riêng, xem Phụ lục A)
├── jobs/nightly.ts         Job chạy hằng đêm: phân hạng → KPI → cảnh báo → dự báo
├── app/(app)/              Từng thư mục là một trang, đường dẫn tiếng Việt
└── components/             Thành phần giao diện dùng chung
```

### 1.4 Vòng vận hành hằng ngày — bức tranh tổng thể trước khi đọc từng tính năng

Từ Mục 2 trở đi, tài liệu đi theo **từng tính năng** (đúng thứ tự file yêu cầu của khách, để đối chiếu dễ) — nhưng đọc rời từng mục sẽ khó thấy được **các phần nối với nhau thế nào**. Mục này cho bức tranh tổng thể trước.

**Nhịp một ngày–đêm:**

```
BAN NGÀY (người dùng thao tác)                    ĐÊM (job tự động, jobs/nightly.ts)
─────────────────────────────────                 ──────────────────────────────────
NVKD:  check-in, lên đơn, báo cáo ngày      ──┐
Quản lý/GĐKV: duyệt đơn, duyệt đề xuất        ├──►  1. Phân hạng đại lý A–E (từ đơn hàng)
Ban giám đốc: duyệt, đăng thông báo           │     2. Tính KPI tháng từng NVKD
                                              │     3. Quét 7 loại cảnh báo
                                              │     4. Tính dự báo dự trù 3 tháng tới
                                              └──►  5. Gửi cảnh báo qua email/Zalo (nếu cấu hình)
                                                            │
SÁNG HÔM SAU  ◄─────────────────────────────────────────────┘
Số liệu mới có mặt ở: Cảnh báo, Dự báo, Báo cáo, Phân tích, hồ sơ Khách hàng/Nhân viên
```

**Thứ tự 4 bước trong job đêm là bắt buộc, không đổi được:** cảnh báo và dự báo đọc số liệu đã phân hạng/KPI của CHÍNH đêm đó, đảo thứ tự sẽ cho kết quả cũ một nhịp. Chạy tay bằng `npm run jobs:nightly`, hoặc nút "Tính lại số liệu" ở Danh mục → Chỉ tiêu (dùng sau khi nhập dữ liệu lịch sử/sửa chỉ tiêu, không cần đợi qua đêm).

**Đơn hàng là gốc — gần như mọi số liệu khác trong hệ thống đều tính lại từ đó, không nhập tay:**

```
                         ┌──► Phân hạng đại lý (Mục 6.2)
                         ├──► KPI tháng, KPI sự kiện (Mục 5.1)
Đơn hàng (Mục 7) ────────┼──► Cảnh báo 7 loại (Mục 12)
(doanh thu/doanh số/     ├──► Dự báo dự trù (Mục 12)
 công nợ tính từ đây)    └──► Báo cáo & Phân tích, mọi bảng xếp hạng (Mục 11)

Nhật ký chỉnh sửa (Mục 14) ghi lại MỌI thay đổi trên MỌI bảng ở trên — không phụ thuộc luồng nào.
```

Hệ quả thực tế: sửa một đơn đã duyệt (chỉ Ban giám đốc làm được — Mục 14.2) sẽ kéo theo số liệu đổi ở phân hạng/KPI/cảnh báo/báo cáo của kỳ đó **sau lần chạy job đêm gần nhất**, không đổi ngay lập tức.

**Một ngày thực tế trông như thế nào, theo vai trò:**

| Vai trò | Mở lên mỗi ngày | Thỉnh thoảng |
|---|---|---|
| NVKD | Trang chủ (thông báo) → Công việc hôm nay (lộ trình, check-in) → Khách hàng / Đơn hàng khi cần lên đơn | Đề xuất (xin khuyến mãi/quà tặng), Chốt tuần |
| Quản lý / GĐKV | Trang chủ → **Đơn hàng chờ duyệt** → **Đề xuất chờ duyệt** → Cảnh báo (nhánh mình) | Báo cáo/Phân tích, Nhân viên (nhánh mình) |
| Ban giám đốc | Trang chủ (tổng quan cảnh báo nổi bật) → Đơn hàng/Đề xuất cần can thiệp → Cảnh báo & Dự báo | Nhật ký thao tác, Danh mục (chỉ tiêu, người dùng, cài đặt) |

**Nên đọc theo thứ tự nào nếu mới tiếp cận dự án:** Mục 1 (kiến trúc, mục này) → **Mục 2 (Phân quyền)** — vì mọi tính năng khác đều đứng trên nền phân quyền này, đọc Mục 2 trước sẽ thấy các mục sau logic hơn hẳn → sau đó đọc theo thứ tự 3–14 tuỳ nhu cầu, hoặc nhảy thẳng tới tính năng cần biết. Cách "giữ hệ thống chạy" (triển khai, sao lưu, giám sát) không nằm trong tài liệu này — xem `HUONG-DAN-TRIEN-KHAI-HAWKHOST.md` và `TAI-LIEU-BAN-GIAO-VA-BAO-TRI.md` (Phụ lục E).

---

## 2. Phân quyền (Mục 1 file yêu cầu)

### 2.1 Bốn vai trò cố định

| Vai trò | Phạm vi |
|---|---|
| **Ban giám đốc** (`DIRECTOR`) | Toàn bộ dữ liệu trong công ty, không giới hạn |
| **Giám đốc Khu vực** (`GDKV`) | Dữ liệu của cả miền mình phụ trách (gán qua bảng `user_territories`, cấp trên MANAGER) |
| **Quản lý** (`MANAGER`) | Dữ liệu của nhánh phòng ban/team mình phụ trách (và toàn bộ team con) |
| **Nhân viên kinh doanh** (`SALES`) | Chỉ dữ liệu do chính mình phụ trách |

Quy tắc cha thấy con, con không thấy cha/anh em: **cấp trên luôn thấy được số liệu, khách hàng, KPI, báo cáo của mọi người trong nhánh mình phụ trách; cấp dưới không bao giờ thấy dữ liệu ngoài nhánh của mình**, kể cả khi biết mã bản ghi (mọi API và trang chi tiết đều kiểm phạm vi trước khi trả dữ liệu, không chỉ ẩn ở giao diện).

### 2.2 Hai chiều tổ chức độc lập

Hệ thống có **hai cây tổ chức tách biệt**, dễ nhầm là một:

- **Phòng ban** (`departments`, nhiều cấp, có thể có Team con) — **là chiều quyết định phạm vi dữ liệu**. Quản lý thấy dữ liệu của toàn bộ nhánh phòng ban mình phụ trách.
- **Khu vực** (`territories`: Miền → Tỉnh/thành → Quận/huyện → Phường/xã) — chỉ dùng để phân tích thị trường (khách hàng ở đâu). Người dùng **chưa được gán phòng ban** thì hệ thống lùi về dùng khu vực làm phạm vi tạm thời, để không có ai bị "mù" hoàn toàn trong lúc chờ gán phòng ban.

Lý do tách riêng: một công ty thật có thể có phòng ban chia theo dòng sản phẩm chứ không theo miền (ví dụ "Team Miền Bắc" và "Team Chuyên môn Miền Bắc" là hai phòng ban khác nhau cùng phụ trách khu vực Miền Bắc) — nếu gộp chung một cây thì không diễn tả được tình huống này.

### 2.3 Cờ quyền đặc biệt (không xây ma trận phân quyền động)

Theo yêu cầu của khách: chức vụ ghi trên hồ sơ nhân viên (Trợ lý, HCNS, Kế toán trưởng…) chỉ là **tên gọi hiển thị**, không tự sinh ra quyền hạn. Quyền hạn theo đúng 4 vai trò ở trên; một số việc đặc thù được cấp bằng **cờ quyền riêng** do Ban giám đốc bật/tắt cho từng người:

| Cờ | Cho phép |
|---|---|
| `canUploadDocuments` | Tải văn bản công ty lên, đăng thông báo phạm vi toàn công ty |
| `canEditStaffInfo` | Sửa thông tin cá nhân của nhân viên khác (thường cấp cho HCNS) |
| `canPostAnnouncements` | Đăng thông báo trong phạm vi phòng/khu vực của chính người được cấp (ví dụ Trợ lý Giám đốc khu vực) |

**Khác biệt có chủ đích so với yêu cầu gốc:** khách hàng có nhắc tới mô hình "tick chọn quyền" kiểu Getfly (ma trận Truy cập/Thêm/Sửa/Xoá/Duyệt cho từng chức năng). Hệ thống **không xây ma trận này** — đã trao đổi và khách xác nhận chấp nhận mô hình 4 vai trò + cờ quyền lẻ, đơn giản hơn và đủ dùng.

### 2.4 Cha cấp quyền cho con

Ban giám đốc/GĐKV/Quản lý tạo, sửa, điều chuyển phòng ban, khoá, cấp lại mật khẩu cho người **trong nhánh mình phụ trách và có vai trò thấp hơn mình** — không cấp ngang hay cấp lên. Cha chỉ cấp lại được **cờ quyền mà chính mình đang có** (không tự tạo ra quyền cho người khác vượt quá quyền của mình). Toàn bộ quy tắc này nằm ở `src/lib/accounts.ts` (`canManageAccount`, `validateAssignment`).

---

## 3. Tài khoản (Mục 2)

### 3.1 Đăng nhập, mật khẩu

- Đăng nhập bằng email (duy nhất trên toàn hệ thống, không hỏi tên công ty) và mật khẩu.
- Sai mật khẩu **5 lần liên tiếp** → khoá tạm tài khoản 15 phút.
- Quên mật khẩu: gửi đường dẫn đặt lại qua email, chỉ đường dẫn **mới nhất** còn hiệu lực (yêu cầu lại thì đường dẫn cũ bị vô hiệu), tối đa 5 yêu cầu/giờ cho một tài khoản để chống spam.
- Đổi mật khẩu: mọi phiên đăng nhập cấp **trước** thời điểm đổi bị coi là hết hạn ngay — người biết mật khẩu cũ (hoặc phiên bị lộ) không dùng tiếp được.

### 3.2 Quản lý tài khoản (Ban giám đốc và cấp trên trong nhánh)

- Danh sách tài khoản có **cột "Đăng nhập gần nhất"**, tìm và lọc được.
- Thêm, sửa, phân quyền, cấp lại mật khẩu, điều chuyển phòng ban/team, khoá tài khoản.
- **Xoá hẳn** chỉ áp dụng cho tài khoản tạo nhầm (chưa từng đăng nhập, chưa có dữ liệu gắn vào) — tài khoản đã có hoạt động thì chỉ **khoá** để giữ lại lịch sử (đơn hàng, báo cáo cũ vẫn cần biết "ai" đã tạo).
- Khoá tài khoản có hiệu lực **ngay lập tức** ở request tiếp theo (không phải chờ hết phiên) vì mỗi request đều đối chiếu lại tình trạng tài khoản với cơ sở dữ liệu.
- Hệ thống chặn khoá tài khoản Ban giám đốc **đang hoạt động cuối cùng** — tránh tự khoá hết quyền truy cập.
- Mọi thao tác điều chuyển/khoá/cấp lại mật khẩu đều ghi vào nhật ký chỉnh sửa (Mục 14).

### 3.3 Hồ sơ cá nhân

Trường: avatar (ảnh nén trước khi lưu), họ tên, email, điện thoại, ngày sinh, giới tính, khu vực, phòng ban › team, chức vụ (tự do). Nhân viên tự sửa hồ sơ của mình; người có cờ `canEditStaffInfo` (thường là HCNS) sửa được cho người khác.

---

## 4. Trang chủ (Mục 3)

### 4.1 Thông tin & thông báo

Đăng theo 3 phạm vi, mỗi phạm vi một nhóm người có quyền đăng khác nhau:

| Phạm vi | Ai đăng được |
|---|---|
| Toàn công ty | Ban giám đốc, hoặc người có cờ `canUploadDocuments` |
| Khu vực / Phòng ban | Ban giám đốc, GĐKV/Quản lý của đúng nhánh đó, hoặc người có cờ `canPostAnnouncements` trong nhánh |

Người xem chỉ thấy thông báo của **công ty + phòng ban của mình + khu vực quản lý trực tiếp** — không thấy thông báo của phòng/khu vực khác. Có thể đính kèm ảnh hoặc tệp, ghim lên đầu, và **tự ẩn khi hết hạn** (tin kèm ngày sự kiện mà không đặt hạn riêng thì tự hết hạn đúng ngày diễn ra).

### 4.2 Chuông thông báo cá nhân

Gộp thông báo mới liên quan tới tài khoản đang đăng nhập, chia theo 5 nhóm: **Công ty – Khu vực – Phòng – Team – Cá nhân**. Chỉ lưu một mốc thời gian "lần cuối mở chuông" (`users.notificationsSeenAt`); số lượng thông báo mới được **suy ra** từ dữ liệu đã có (đơn chờ duyệt, đề xuất tới lượt duyệt, việc quá hạn…), không lưu một bảng thông báo trùng lặp.

---

## 5. Công việc (Mục 4)

### 5.1 KPI

Ba loại KPI riêng biệt, cùng chạy song song:

1. **KPI chỉ tiêu doanh số** — chỉ tiêu doanh thu/doanh số/số đơn/khách mới giao theo tháng (bảng `targets`), tính % hoàn thành **tự động từ đơn hàng**, không nhập tay. Kỳ quý/năm được cộng dồn từ các tháng bên trong, không có bảng chỉ tiêu quý riêng (tránh giao hai nơi rồi lệch nhau).
2. **KPI tự tạo** (`custom_kpis`) — quản lý tự đặt tên và đơn vị tính bất kỳ (ví dụ "Số buổi tập huấn — buổi"), giao chỉ tiêu cho từng nhân viên theo tháng; nhân viên **ghi nhận hằng ngày**, hệ thống cộng dồn vào kết quả tháng, có tiến độ so với thời gian đã trôi qua trong tháng.
3. **KPI sự kiện mời khách** (`inviteEvents`) — dành riêng cho loại sự kiện hiếm, có giao chỉ tiêu thật (trung bình 1 tháng/lần toàn công ty): Ban giám đốc tạo sự kiện và giao chỉ tiêu theo khu vực → Giám đốc khu vực giao lại cho từng nhân viên trong khu vực mình → nhân viên ghi nhận từng khách mời (tên, **loại khách** — ví dụ bác sĩ/chủ spa/chủ thẩm mỹ viện, ngày mời), cộng dồn tới hạn chót. Mỗi khách mời có cờ riêng **CME** và **Chứng nhận tham dự** để tra nhanh khi tới đợt cấp chứng nhận, cùng cờ **có tham dự thực tế hay không** (so sánh mời với tham dự thực tế). Nhân viên chỉ thấy chỉ tiêu và khách mời của chính mình, không thấy của đồng nghiệp.

**Khác biệt có chủ đích:** yêu cầu gốc có nhắc tới việc "gắn thẻ nhiều nhân viên vào một KPI" (mô hình Getfly/Slim). Hệ thống hiện giao KPI **trực tiếp cho từng người**, chưa có cơ chế gắn thẻ nhiều người dùng chung một KPI.

### 5.2 Danh sách công việc

Việc cần hoàn thành, có ngày bắt đầu và 6 trạng thái: **Chưa bắt đầu, Chờ phản hồi, Đã hoàn thành, Dời (kèm lý do), Chưa hoàn thành (kèm lý do), Huỷ (kèm lý do)**. Lý do được hỏi **ngay lúc đổi trạng thái**, không đợi tới cuối tuần (tránh quên lý do thật). Cấp trên giao việc được cho cấp dưới trong nhánh mình; chỉ người tạo sửa được nội dung, người thực hiện hoặc người giao đổi được trạng thái. Có lịch sử từng việc và chuông báo khi quá hạn/tới hạn.

### 5.3 Kế hoạch tuần, báo cáo ngày, check-in

- **Lộ trình tuần** (`weeklyPlans`/`planItems`): đăng ký lịch thăm khách theo ngày trong tuần.
- **Báo cáo ngày**: % thực hiện **tự tính** từ số việc trong lộ trình đã đánh dấu hoàn thành, không nhập tay (tránh làm đẹp số liệu).
- **Check-in GPS** tại điểm bán: khoảng cách tới địa chỉ khách được tính ở **máy chủ**, không tin số trình duyệt gửi lên; check-in ngoài bán kính cho phép vẫn được lưu nhưng gắn cờ để quản lý rà soát (không chặn cứng, vì GPS trong nhà có thể lệch hàng trăm mét). Ảnh kèm theo là tuỳ chọn và được nén trước khi lưu.
- **Lịch** (`/cong-viec/lich`): gom việc, lộ trình, sự kiện, hạn chót của chính người xem theo tháng.

---

## 6. Khách hàng (Mục 5)

### 6.1 Danh sách khách hàng

Phạm vi theo đúng Mục 2: khu vực/phòng nào chỉ thấy khách của khu vực/phòng đó, nhân viên chỉ thấy khách mình phụ trách. Cột hiển thị: tên cơ sở, người liên hệ chính, người phụ trách, khu vực (phường – tỉnh), điện thoại (bấm gọi trực tiếp), ngày lên đơn gần nhất, công nợ, cảnh báo.

Sắp xếp được theo 7 kỳ tính (tháng này/trước, quý này/trước, năm này/trước, 12 tháng gần nhất) trên 5 tiêu chí: **doanh số bình quân tháng, doanh thu bình quân tháng, công nợ bình quân tháng, công nợ hiện tại, số lần lên đơn, số sản phẩm đang ứng dụng** (ngưỡng >30/>20/>10/>5 sản phẩm).

### 6.2 Hồ sơ khách hàng chi tiết

Đầy đủ các trường yêu cầu: mã, loại (kênh bán), tên cơ sở, mã số thuế, người liên hệ chính (họ tên, ngày sinh, chức vụ, điện thoại), địa chỉ tách riêng phường/tỉnh, thông tin người nhận hàng mặc định, **nhiều người liên quan** (không giới hạn một người liên hệ), nhân viên phụ trách, thông tin trao đổi (ghi chú tự do, không giới hạn số lần), lịch sử lên đơn, lịch sử báo cáo + đi hỗ trợ (dòng thời gian check-in và lộ trình đã báo cáo), doanh số/doanh thu/công nợ theo tuần/tháng/quý/năm, số sản phẩm đang ứng dụng (kèm tên), các khoá chuyển giao đã tham dự, và **xếp hạng** đại lý.

**Xếp hạng đại lý:** hệ thống chạy **5 hạng A–E**, tự tính song song theo hai tiêu chí độc lập:
- Theo doanh thu bình quân tháng (12 tháng gần nhất): A ≥50 triệu · B ≥20 triệu · C ≥10 triệu · D ≥5 triệu · E <5 triệu.
- Theo số SKU đang ứng dụng (3 tháng gần nhất — cửa sổ ngắn hơn để phản ánh đúng "đang dùng", không tính sản phẩm đã bỏ từ lâu): A >30 · B >20 · C >10 · D >5 · E ≤5.

> ℹ️ **Giả định đang áp dụng:** file yêu cầu gốc ghi *"có thể xếp hạng theo hạng A, B, C"* (3 hạng), hệ thống đang chạy 5 hạng A–E (chi tiết hơn). Không tìm thấy xác nhận riêng bằng văn bản cho việc mở rộng này trong các file log hiện có, nhưng nhiều khả năng đã được thống nhất ở một trao đổi không còn lưu lại — **giữ nguyên 5 hạng theo giả định hiện tại**, chỉ điều chỉnh nếu khách hàng yêu cầu khác. Xem Mục 17.

Mọi thay đổi trên hồ sơ (kể cả người liên quan, ghi chú, tệp đính kèm, chuyển trạng thái) đều ghi vào nhật ký chỉnh sửa và chỉ thực hiện được trong đúng phạm vi của người thao tác.

### 6.3 Khách tiềm năng

Danh sách riêng (tab trong mục Khách hàng, không phải menu riêng) cho dữ liệu do Marketing thu thập, chưa chuyển thành khách chính thức. 7 cột: ngày thêm mới, họ tên người liên hệ, điện thoại, khu vực, loại, nhu cầu tư vấn, nguồn khách. Trạng thái chuyển đổi: **Chưa liên hệ → Đã liên hệ (bắt buộc ghi báo cáo) / Đã huỷ (bắt buộc ghi báo cáo) → Đã chốt** (chuyển thành khách chính thức — bắt buộc điền đủ hồ sơ theo Mục 6.2). Khách tiềm năng chưa được phân công người phụ trách chỉ Ban giám đốc thấy được; có chức năng giao hàng loạt cho nhân viên.

---

## 7. Bán hàng (Mục 6)

### 7.1 Đơn hàng

**Thông tin đơn:** mã khách hàng, tên cơ sở, người đại diện, mã số thuế, địa chỉ, điện thoại, email, số phiếu đặt hàng (mã đơn tự sinh dạng `DH` + năm-tháng + số thứ tự), cờ cần xuất VAT (hiển thị nổi bật kèm cảnh báo nếu thiếu mã số thuế/địa chỉ), thông tin người nhận hàng (tự điền sẵn theo mặc định của khách, sửa được), tên/điện thoại/địa chỉ nhà xe, phụ thu phí, ghi chú, các dòng hàng, tệp đính kèm.

**Quy trình duyệt:** người lập → **Giám đốc khu vực hoặc Ban giám đốc duyệt là đủ** (một cấp). TGĐ và Kế toán trưởng chỉ can thiệp khi đơn xin thêm khuyến mãi/chiết khấu ngoài chính sách chuẩn — trường hợp đó đi qua mục **Đề xuất** riêng, không làm phần của quy trình duyệt đơn.

> **Khác biệt có chủ đích:** file yêu cầu gốc mô tả luồng duyệt 4 cấp (Người đề xuất → Trưởng bộ phận → Trưởng bộ phận liên quan → TGĐ). Khách hàng đã xác nhận (19/09/2026) một cấp GĐKV là đủ cho đa số đơn; 4 cấp chỉ áp dụng cho phiếu Đề xuất khi xin thêm ngoài chính sách.

**Trạng thái:** Chờ duyệt, Điều chỉnh (người duyệt trả về yêu cầu sửa), Đã duyệt, Đã huỷ.

**Danh sách đơn:** ngày tạo, mã đơn, mã khách hàng, tên cơ sở, **người tạo đơn**, trạng thái, doanh số, doanh thu, đã thanh toán, còn lại — lọc theo trạng thái/VAT/người tạo, tìm kiếm, phân trang, xuất Excel đúng bộ lọc đang xem.

**Định nghĩa số liệu áp dụng nhất quán ở MỌI nơi trong hệ thống:**
- **Doanh thu** = tổng tiền hàng của đơn **đã duyệt**.
- **Doanh số** = tổng tiền hàng của đơn **chưa huỷ** (gồm cả đang chờ duyệt).
- **Công nợ / phải thu** = tiền hàng + phụ thu phí − đã thu (chỉ tính trên đơn đã duyệt).

### 7.2 Khoá thông tin đã duyệt

Xem chi tiết ở Mục 14 — đơn đã duyệt chỉ Ban giám đốc sửa/huỷ/xoá/gỡ tệp đính kèm được.

### 7.3 Sản phẩm

Hiển thị: mã, tên, nhóm, đơn vị tính, dung tích/trọng lượng, giá bán lẻ (có VAT), giá đại lý (có VAT), chiết khấu chuẩn (%, chỉ để tham khảo báo giá — đơn hàng vẫn ghi đơn giá tay theo dòng), ghi chú. Dữ liệu sản phẩm được dùng chung xuyên suốt: lên đơn, báo cáo, cảnh báo, dự báo.

### 7.4 Chương trình bán hàng / khuyến mãi

Khách hàng xác nhận **không có khái niệm ngân sách bằng tiền** — khuyến mãi là chính sách chiết khấu quy ra hàng theo bậc giá trị đơn (ví dụ đơn ≥5 triệu được thêm 15% hàng khuyến mãi) hoặc mua-nhiều-tặng-nhiều, không hoàn tiền mặt. Chương trình được **gắn tự động vào dòng hàng** theo bảng `programProducts` (không để người lên đơn tự chọn tay, để tránh sót và làm sai tiến độ chương trình). "Ngân sách đã sử dụng" trong báo cáo là **giá trị hàng khuyến mãi đã phát ra**, xem theo tuần/tháng/quý/năm, theo khu vực/phòng ban/nhân viên/sản phẩm.

### 7.5 Chương trình chuyển giao

Khoá học quy trình sử dụng sản phẩm (đại lý phải học xong mới được lấy hàng nhóm sản phẩm tương ứng). Hiển thị: tên khoá, thời gian, điều kiện, hình thức, có/không cấp chứng nhận. Hệ thống lưu **danh sách khách đã học khoá nào và ngày nào** để tra nhanh, không kiểm giấy tờ; đề xuất mở lớp đi qua mục Đề xuất.

### 7.6 Nhập Excel hàng loạt (`/danh-muc/nhap-excel`)

Không phải yêu cầu trong file gốc — công cụ nội bộ phục vụ nhập dữ liệu ban đầu khi bắt đầu dùng hệ thống và sửa/bổ sung hàng loạt về sau (sản phẩm, đơn hàng lịch sử...), thay vì phải thêm tay từng dòng. **Chỉ Ban giám đốc** dùng được.

Ba loại file, mỗi loại một bộ cột bắt buộc/tuỳ chọn riêng (hiển thị ngay trên màn hình để chuẩn bị file đúng khuôn): **sản phẩm, khách hàng, đơn hàng lịch sử**. Quy trình hai bước: (1) tải file lên → hệ thống đọc và kiểm tra từng dòng, hiển thị số dòng hợp lệ, danh sách lỗi kèm số dòng, và vài dòng đầu để đối chiếu — **chưa ghi gì vào cơ sở dữ liệu**; (2) người dùng xem xong bấm xác nhận mới thật sự ghi.

**Nguyên tắc không ghi một phần:** file còn dù chỉ một dòng lỗi thì không dòng nào được ghi — nhập nửa vời để lại dữ liệu rác khó lần hơn là bắt sửa file rồi nhập lại. Đơn hàng trùng mã đơn thì **bỏ qua** (không báo lỗi, không ghi đè) — để nhập lại đúng file cũ nhiều lần mà không nhân đôi doanh thu. Mỗi lần xác nhận nhập đều ghi vào nhật ký chỉnh sửa (Mục 14) kèm tên file, số dòng thêm mới/cập nhật.

**Khách hàng/sản phẩm trùng mã: cập nhật, nhưng khác nhau ở ô để trống — đã kiểm chứng bằng test tự động với file .xlsx thật (`tests/lib/import.test.ts`):**
- **Khách hàng:** cột nào có mặt trong file nhưng ô để trống, với khách **đã có sẵn**, thì **giữ nguyên giá trị cũ** (không xoá) — đúng cột nào đổi thì chỉ cần đưa đúng cột đó vào file, không phải xuất lại toàn bộ hồ sơ.
- **Sản phẩm:** không có quy tắc "giữ nguyên khi để trống" — cột nào có mặt trong file thì ghi đúng theo ô đó, **kể cả khi ô để trống** (xoá giá trị cũ về rỗng). Muốn cập nhật sản phẩm mà giữ nguyên Dung tích/Ghi chú cũ thì bỏ hẳn cột đó ra khỏi file, không để ô trống.

---

## 8. Đề xuất (Mục 7)

4 loại phiếu cố định, mỗi loại có vài trường riêng: **mở lớp chuyển giao, khuyến mãi, quà tặng, khác**. Bất kỳ ai cũng tạo được phiếu.

**Duyệt lần lượt theo bước:** Trưởng bộ phận (hệ thống tự tìm từ cây phòng ban của người đề xuất) → Trưởng bộ phận liên quan (người tạo tự chọn, nhiều bộ phận cùng bước thì duyệt song song) → người duyệt thêm tuỳ chọn → TGĐ (bắt buộc, trừ khi người đề xuất chính là TGĐ). Chỉ tới lượt khi mọi bước trước đã duyệt xong; một người từ chối là cả phiếu bị từ chối (phải ghi lý do); người đề xuất thu hồi được phiếu khi **chưa có ai** quyết định.

> ℹ️ **Giả định đang áp dụng:** hồ sơ test case từng ghi "CẦN XÁC NHẬN" về việc bắt buộc qua TGĐ và duyệt theo đúng thứ tự cố định. Khi rà lại lịch sử trao đổi (19/09/2026), có một xác nhận trước đó mô tả cơ chế **khác hẳn**: không có mẫu trường dữ liệu cố định, người tạo tự do gắn thẻ bất kỳ ai làm người duyệt — nhiều khả năng đã được thống nhất lại ở một trao đổi sau đó không còn lưu trong các file log hiện có. **Giữ nguyên cơ chế đang chạy** (4 loại cố định, duyệt theo thứ tự qua TGĐ) theo giả định hiện tại, chỉ điều chỉnh nếu khách hàng yêu cầu khác. Xem Mục 17.

---

## 9. Nhân viên (Mục 8) & Chuyên môn (Mục 9)

### 9.1 Nhân viên (kinh doanh)

Danh sách: họ tên, phòng ban, chức vụ, điện thoại, ngày sinh, cảnh báo — lọc theo khu vực/phòng ban/cảnh báo, bấm vào xem hồ sơ kèm báo cáo chi tiết đầy đủ (KPI, kế hoạch, khách hàng, đơn hàng, báo cáo ngày, check-in, và với cấp quản lý có thêm bảng nhân sự trực thuộc). Ô lọc chỉ liệt kê đúng cơ cấu trong phạm vi người xem.

### 9.2 Nhân viên chuyên môn (phụ trách đào tạo)

Được nhận diện bằng một quy tắc duy nhất: **chức vụ hoặc phòng ban có chứa chữ "chuyên môn"** (chỉ phụ trách vài lớp không đủ để tính là chuyên môn). Quyền hạn vẫn như nhân viên kinh doanh thường, chỉ khác: không có chỉ tiêu doanh thu (không bị tính vào KPI doanh số, không bị cảnh báo "không đạt chỉ tiêu"), hồ sơ xoay quanh **số lớp đã dạy** thay vì doanh số/doanh thu. Quản lý chuyên môn khu vực là vai trò Quản lý trong nhánh phòng ban chuyên môn — phạm vi xem theo đúng cây phòng ban như mọi vai trò khác.

---

## 10. Văn bản (Mục 10)

Đính kèm văn bản, thông báo, quy định, tài liệu công ty (thường là PDF đã duyệt). **Toàn công ty xem và tải về được**; chỉ Ban giám đốc hoặc người có cờ `canUploadDocuments` tải lên/sửa/xoá. Có phân loại (Văn bản / Thông báo / Quy định – Quy chế / Tài liệu / Khác), trích yếu nội dung, tìm kiếm, phân trang. Nhận PDF/PNG/JPEG/Word/Excel/PowerPoint tới 8MB; hệ thống **kiểm chữ ký nội dung tệp thật** (không tin đuôi file hay loại trình duyệt khai báo) để chặn tệp giả mạo đuôi. Xoá văn bản xoá luôn tệp trên đĩa.

---

## 11. Báo cáo & Phân tích (Mục 11)

### 11.1 Báo cáo bán hàng / Phân tích (`/phan-tich`, `/bao-cao`)

Sáu mục: **Tổng quan** (doanh số–doanh thu–công nợ theo tháng/quý/năm), **Kế hoạch** (thực hiện so với chỉ tiêu, theo khu vực/phòng ban/nhân viên), **Tỷ trọng** (đóng góp doanh thu của từng thị trường/phòng ban/nhân viên/sản phẩm), **Sản phẩm** (báo cáo doanh số từng sản phẩm), **Khuyến mãi** (ngân sách/hàng khuyến mãi đã dùng), **Định biên** (doanh thu bình quân trên mỗi nhân viên kinh doanh). Minh hoạ bằng biểu đồ. Có thể xem theo toàn công ty, khu vực, phòng, team hoặc từng nhân viên (bộ chọn phạm vi chỉ **thu hẹp thêm**, không bao giờ mở rộng vượt quá quyền gốc của người xem).

### 11.2 KPI nhân viên — Xếp hạng (`/phan-tich?muc=xep-hang`)

Xếp hạng doanh thu, doanh số, công nợ, và số khách mở mới của từng nhân viên theo tháng/quý/năm, từ cao xuống thấp (đồng hạng khi bằng điểm). Bấm tên mở đúng hồ sơ của đúng kỳ đang xem.

### 11.3 Báo cáo khách hàng (`/phan-tich?muc=khach-hang`)

Xếp hạng khách hàng theo doanh thu/doanh số/công nợ của kỳ; số lượng và **tỷ lệ** khách mở mới trên tổng khách; danh sách khách mới trong tuần/tháng/quý/năm. Xem theo toàn công ty/khu vực/phòng/team/nhân viên.

### 11.4 Báo cáo chuyển giao (`/bao-cao/chuyen-giao`)

Biểu đồ số lớp/chuyên đề theo từng loại, theo ngày/tuần/tháng/quý/năm, xem theo mọi cấp tổ chức. Gồm cả thống kê khách đã hoàn thành từng khoá chuyển giao.

---

## 12. Cảnh báo & Dự báo (Mục 12)

Bảy loại cảnh báo, quét tự động trong job hằng đêm trên **tháng đã đóng sổ** (tháng đang chạy dở không bao giờ được dùng để so sánh, tránh kết luận sai "mọi thứ đều sụt giảm" chỉ vì mới qua vài ngày đầu tháng):

| Loại | Điều kiện |
|---|---|
| Đại lý ngừng lấy hàng | ≥2 tháng (lưu ý) · ≥3 tháng (cảnh báo) · ≥6 tháng (nghiêm trọng), tính từ ngày bắt đầu làm đại lý |
| Đại lý giảm doanh thu | So với tháng trước / quý trước (3 tháng so 3 tháng liền trước) / năm trước (12 tháng so 12 tháng liền trước) — vượt ngưỡng % (mặc định 30%, Ban giám đốc tự chỉnh) |
| Sản phẩm không có doanh thu | Đã từng bán trong 12 tháng gần nhất nhưng tháng này không bán — báo lại mỗi tháng còn tiếp diễn |
| Sản phẩm giảm sản lượng | Cùng ba mốc so sánh như đại lý |
| Thị trường không có doanh thu / giảm | Cộng dồn từ cấp tỉnh lên cấp miền, cùng ba mốc so sánh |
| Nhân sự không đạt chỉ tiêu | Dưới ngưỡng % (mặc định 70%) trong **2 tháng liên tiếp** |

**Vòng đời cảnh báo:** quét lại nhiều lần trong ngày không sinh trùng; cảnh báo hết điều kiện tự chuyển "Hết hiệu lực"; điều kiện đúng trở lại thì tự mở lại; người dùng đánh dấu "Đã xử lý" thì giữ nguyên làm lịch sử, không bị hệ thống ghi đè.

**Dự báo dự trù sản phẩm** (3 tháng tới): bình quân trượt 3 tháng đã đóng sổ nhân hệ số mùa vụ (so cùng kỳ năm trước, chỉ áp dụng khi có đủ dữ liệu). Người dùng chỉnh tay được số dự trù; số đã chỉnh **không bị mất** khi hệ thống tính lại mỗi đêm — đây là dự báo cấp công ty, không lọc theo phạm vi người xem.

---

## 13. Nhắn tin (Mục 13)

**Không triển khai** — khách hàng đã xác nhận bỏ hẳn tính năng này (19/09/2026). Không có bảng dữ liệu, đường dẫn hay giao diện nào cho tính năng này trong mã nguồn.

---

## 14. Lịch sử chỉnh sửa & Khoá thông tin đã duyệt (Yêu cầu thêm)

### 14.1 Nhật ký chỉnh sửa

Mọi thao tác tạo/sửa/xoá dữ liệu nghiệp vụ hoặc cấu hình đều ghi lại: ai, lúc nào, làm gì — với thao tác sửa là **từng trường "cũ → mới"** (kể cả từng dòng hàng của đơn hàng); thao tác xoá ghi lại nội dung **trước khi xoá** (sau đó không còn gì để đối chiếu). Mật khẩu hay mã đặt lại mật khẩu **không bao giờ** được ghi vào nhật ký.

Chỉ Ban giám đốc xem được: trang **Danh mục → Nhật ký thao tác** (tìm kiếm theo tên/mã đơn/số điện thoại, lọc theo người/hành động/đối tượng/khoảng ngày, phân trang, xuất Excel), và khung "Lịch sử chỉnh sửa" ngay trong đơn hàng, hồ sơ khách hàng, hồ sơ nhân viên, từng phiếu đề xuất. Mã nội bộ trong nhật ký được đổi thành tên thật (người, khu vực, sản phẩm…) để đọc hiểu ngay, không cần tra cứu thêm.

### 14.2 Khoá thông tin đã duyệt

| Đối tượng | Người lập | Cấp quản lý (kể cả người vừa duyệt) | Ban giám đốc |
|---|---|---|---|
| Đơn hàng chờ duyệt | Sửa được | Duyệt hoặc trả về yêu cầu điều chỉnh | Sửa/duyệt/huỷ/xoá |
| **Đơn hàng đã duyệt** | Không sửa, không huỷ, không xoá, không gỡ tệp | Không sửa, không huỷ, không xoá, không gỡ tệp | Sửa/huỷ/xoá/gỡ tệp (có nhật ký) |
| Phiếu đề xuất | Thu hồi được khi chưa ai quyết định | Duyệt/từ chối đúng lượt | Không sửa/xoá |
| **Phiếu đề xuất đã có kết quả** | Không sửa/xoá | Không sửa/xoá | Sửa nội dung, xoá (chuỗi duyệt và kết quả duyệt giữ nguyên khi sửa; nội dung đầy đủ được ghi vào nhật ký trước khi xoá) |

**Hai điểm chưa khoá, giữ nguyên theo giả định hiện tại (27/09/2026 — xem Mục 17):** (1) quản lý vẫn xoá được một lần thu tiền ghi nhầm của đơn đã duyệt (có ghi nhật ký); (2) bất kỳ ai xem được đơn đã duyệt vẫn thêm được tệp đính kèm mới (ví dụ ảnh chuyển khoản) nhưng không gỡ được tệp đã có. Sẽ khoá thêm nếu khách hàng yêu cầu.

---

## 15. Yêu cầu phi chức năng

### 15.1 Hiệu năng & khả năng mở rộng

Đã kiểm tải với 3.077 khách hàng, 40.603 đơn hàng, 98.459 dòng hàng, 150.124 dòng nhật ký: đa số trang phản hồi 20–150 mili-giây (trên cơ sở dữ liệu nhúng — PostgreSQL thật còn nhanh hơn); job quét cảnh báo/phân hạng/dự báo hằng đêm chạy toàn bộ trong 2,3 giây. Đủ dùng cho gói máy chủ dùng chung chi phí thấp (đã chạy thật trên HawkHost Semi-Dedicated). Biến `PG_POOL_MAX` cho phép giới hạn số kết nối PostgreSQL đồng thời nếu nhà cung cấp hosting hạn chế.

### 15.2 Bảo mật

- Mật khẩu băm `scrypt`, không lưu dạng đọc được; phiên đăng nhập là JWT trong cookie `httpOnly`, `SameSite=Lax`, thêm `Secure` khi chạy thật.
- Toàn bộ header bảo mật tiêu chuẩn: chống nhúng khung (`X-Frame-Options: DENY`, CSP `frame-ancestors 'none'`), `nosniff`, `Strict-Transport-Security` khi chạy HTTPS, không lộ phiên bản framework.
- Tệp tải lên (ảnh check-in, văn bản, tệp đính kèm) nằm ngoài thư mục công khai, chỉ phục vụ qua đường dẫn có kiểm tra đăng nhập **và** kiểm phạm vi (người xem phải thấy được đối tượng chứa tệp đó).
- Tệp kiểm tra **chữ ký nội dung thật**, không tin đuôi file hay loại trình duyệt khai báo.
- Rà soát lỗ hổng thư viện định kỳ (`npm audit`); đã vá các lỗ hổng có ảnh hưởng thật, các lỗ hổng còn lại đã rà từng cái và xác nhận không chạm tới cách hệ thống đang dùng thư viện đó (chi tiết trong `CLAUDE.md`).

### 15.3 Vận hành & giám sát

Điểm kiểm tra sức khoẻ `/api/suc-khoe` (không cần đăng nhập, không lộ dữ liệu nghiệp vụ) để dịch vụ giám sát ngoài theo dõi. Hướng dẫn sao lưu/khôi phục, xử lý sự cố nhanh nằm trong `TAI-LIEU-BAN-GIAO-VA-BAO-TRI.md`.

### 15.4 Kiểm thử tự động

103 test tự động (Vitest), chạy bắt buộc trong CI trên mỗi lần đưa mã nguồn lên:
- **81 test nhanh** (dưới 2 giây): logic thuần (kỳ báo cáo, cảnh báo, dự báo, phân hạng), rò rỉ phạm vi dữ liệu giữa vai trò/công ty — lớp lỗi hay gặp nhất trong quá trình phát triển — và nhập Excel hàng loạt (file .xlsx thật, cột tuỳ chọn mới, quy tắc không xoá dữ liệu cũ khi cập nhật, xem Mục 7.6).
- **22 test khoá quyền** (10–20 giây, dựng máy chủ và cơ sở dữ liệu thật): xác nhận đơn hàng và phiếu đề xuất đã duyệt không sửa/huỷ/xoá được trừ Ban giám đốc.

Chưa có kiểm thử tự động cho toàn bộ các phân hệ còn lại (Cảnh báo, Báo cáo, quản lý Tài khoản…) — việc kiểm tra các phần đó hiện dựa vào bộ test case thủ công và các kịch bản đối chiếu SQL độc lập đã chạy trong quá trình xây dựng. Khuyến nghị mở rộng dần theo đúng khuôn mẫu đã có nếu dự án được bảo trì lâu dài.

---

## 16. Tổng hợp khác biệt có chủ đích so với yêu cầu gốc

Bốn điểm sau **đã trao đổi và có quyết định rõ ràng** với khách hàng, không phải thiếu sót:

1. **Ma trận tick quyền kiểu Getfly** — dùng 4 vai trò cố định + cờ quyền lẻ thay vì ma trận Truy cập/Thêm/Sửa/Xoá/Duyệt động cho từng chức năng.
2. **Duyệt đơn hàng 4 cấp** — chỉ 1 cấp (Giám đốc khu vực/Ban giám đốc) là đủ cho đa số đơn; TGĐ/Kế toán trưởng chỉ can thiệp qua mục Đề xuất khi xin thêm ngoài chính sách.
3. **KPI gắn thẻ nhiều nhân viên** — KPI hiện giao trực tiếp cho từng người, chưa có cơ chế gắn thẻ nhiều người vào chung một KPI.
4. **Nhắn tin nội bộ** — bỏ hẳn, không triển khai.

---

## 17. Điểm còn mở

Quyết định (27/09/2026): mục 1–7 **giữ nguyên theo giả định/hành vi đang chạy**, không coi là chặn tiến độ — chỉ điều chỉnh khi khách hàng chủ động báo yêu cầu khác, không cần hỏi trước.

| # | Nội dung | Trạng thái |
|---|---|---|
| 1 | Hạng đại lý: hệ thống dùng 5 hạng A–E, file yêu cầu gốc ghi "A, B, C" (3 hạng) | Giữ giả định hiện tại (5 hạng) |
| 2 | Phiếu đề xuất: cơ chế hiện tại (4 loại cố định, duyệt theo thứ tự cố định qua TGĐ) có khớp với một xác nhận trước đó mô tả cơ chế tự do gắn thẻ, không mẫu cố định | Giữ giả định hiện tại (4 loại, duyệt theo thứ tự) |
| 3 | "Không đạt 70% doanh số" hiện đo trên doanh thu đã duyệt (KPI), chưa rõ khách có muốn đo trên doanh số (đơn chưa huỷ) | Giữ giả định hiện tại (đo trên doanh thu KPI) |
| 4 | Sản phẩm/thị trường **chưa từng có doanh thu** hiện không bị cảnh báo (chỉ báo cái đã từng bán rồi ngừng) | Giữ giả định hiện tại (chỉ báo cái đã từng bán) |
| 5 | Dự báo dự trù là số liệu cấp công ty, chưa lọc theo khu vực/phòng ban | Giữ giả định hiện tại (cấp công ty) |
| 6 | Quản lý vẫn xoá được phiếu thu ghi nhầm của đơn đã duyệt | Giữ hành vi hiện tại (quản lý xoá được, có nhật ký) |
| 7 | Ai xem được đơn đã duyệt vẫn thêm được tệp đính kèm mới (không gỡ được) | Giữ hành vi hiện tại |
| 8 | Cam kết bảo trì 2 năm, hỗ trợ nhanh, chuyển quyền sở hữu mã nguồn | Là điều khoản hợp đồng, không phải điều phần mềm tự chứng minh — xem gợi ý phạm vi ở `TAI-LIEU-BAN-GIAO-VA-BAO-TRI.md` mục 10 |

---

## 18. Sơ đồ chuyển trạng thái các luồng nghiệp vụ chính

Năm luồng dưới đây là phần dễ hiểu sai nhất khi sửa code (nhiều trạng thái, nhiều vai trò cùng chạm vào) — đây là bảng tra đầy đủ mọi bước chuyển hợp lệ, không phải chỉ ví dụ.

### 18.1 Đơn hàng (`orders.status` + cờ `needsRevision`)

| Trạng thái hiện tại | Sự kiện | Trạng thái mới | Ai làm được |
|---|---|---|---|
| *(chưa có)* | Lập đơn mới (`createOrderAction`) | `DRAFT` | NVKD (khách trong phạm vi mình) |
| `DRAFT` | Sửa (`updateOrderAction`) | `DRAFT` (nếu đang `needsRevision` thì tự tắt cờ) | Người lập, quản lý/GĐKV trong phạm vi |
| `DRAFT` | Yêu cầu điều chỉnh (`requestOrderRevisionAction`) | `DRAFT` + `needsRevision = true` | GĐKV / Ban giám đốc |
| `DRAFT` | Duyệt (`approveOrderAction`, cập nhật có điều kiện `status = DRAFT` để chặn hai người duyệt cùng lúc) | `CONFIRMED` | GĐKV / Ban giám đốc |
| `DRAFT` | Từ chối / Huỷ (`cancelOrderAction`) | `CANCELLED` | Quản lý trở lên |
| `CONFIRMED` | Sửa (`updateOrderAction`) | `CONFIRMED` | **Chỉ Ban giám đốc** |
| `CONFIRMED` | Huỷ (`cancelOrderAction`) | `CANCELLED` | **Chỉ Ban giám đốc** |
| `CONFIRMED` | Xoá hẳn (`deleteOrderAction`) | *(xoá khỏi cơ sở dữ liệu)* | **Chỉ Ban giám đốc** |
| `CANCELLED` | — | *(trạng thái cuối, không quay lại được)* | — |

Ghi chú: `recordPaymentAction`/`deletePaymentAction` (ghi/xoá phiếu thu) chỉ thao tác được khi đơn ở `CONFIRMED`; không phải một bước chuyển trạng thái của chính đơn hàng.

### 18.2 Phiếu đề xuất (`proposals.status` + `proposal_approvers.decision` theo từng bước)

| Trạng thái phiếu | Sự kiện | Kết quả | Ai làm được |
|---|---|---|---|
| *(chưa có)* | Tạo phiếu (`createProposalAction`) | `PENDING`, mọi bước duyệt `PENDING` | Bất kỳ ai |
| `PENDING` | Người ở bước hiện tại duyệt (`decideProposalAction`, `decision=APPROVED`) | Bước đó `APPROVED`; nếu còn bước sau thì chuyển lượt, nếu là bước cuối thì cả phiếu → `APPROVED` | Người đúng lượt trong chuỗi duyệt |
| `PENDING` | Người ở bước hiện tại từ chối (`decision=REJECTED`, bắt buộc ghi lý do) | Cả phiếu → `REJECTED` ngay lập tức | Người đúng lượt trong chuỗi duyệt |
| `PENDING`, **chưa ai quyết định** | Người đề xuất thu hồi (`withdrawProposalAction`) | → `REJECTED` + đánh dấu "đã thu hồi" trong `details` | Người đề xuất |
| `APPROVED` / `REJECTED` | Sửa tiêu đề/nội dung/trường riêng — **không đổi chuỗi duyệt hay kết quả** (`updateProposalAction`) | Giữ nguyên `status` | **Chỉ Ban giám đốc** |
| `APPROVED` / `REJECTED` | Xoá hẳn (`deleteProposalAction`, nội dung + kết quả duyệt ghi vào nhật ký trước khi xoá) | *(xoá khỏi cơ sở dữ liệu)* | **Chỉ Ban giám đốc** |

### 18.3 Công việc (`tasks.status`, chuỗi ký tự — xem `TASK_STATUSES` trong `src/lib/tasks.ts`)

| Nhóm | Trạng thái | Bắt buộc lý do? |
|---|---|---|
| Đang mở (`OPEN_TASK_STATUSES`) | `NOT_STARTED` (Chưa bắt đầu) · `WAITING` (Chờ phản hồi) · `POSTPONED` (Dời) · `INCOMPLETE` (Chưa hoàn thành) | `POSTPONED`, `INCOMPLETE` bắt buộc |
| Kết thúc | `DONE` (Đã hoàn thành) · `CANCELLED` (Huỷ) | `CANCELLED` bắt buộc |

Không giới hạn thứ tự chuyển giữa các trạng thái (một việc "Chờ phản hồi" có thể chuyển thẳng sang "Đã hoàn thành" hay "Huỷ", không bắt buộc đi qua "Chưa bắt đầu" trước). Chỉ người thực hiện hoặc người giao việc đổi được trạng thái; chỉ người tạo sửa được nội dung/xoá được việc.

### 18.4 Cảnh báo (`alerts.status`)

| Trạng thái hiện tại | Sự kiện | Trạng thái mới |
|---|---|---|
| *(chưa có)* | Job hằng đêm phát hiện điều kiện đúng | `OPEN` |
| `OPEN` | Job quét lại, điều kiện vẫn đúng | `OPEN` (cập nhật nội dung mới nhất) |
| `OPEN` | Job quét lại, điều kiện hết đúng | `RESOLVED` (tự động) |
| `OPEN` | Người dùng bấm "Đã xử lý" (`acknowledgeAlertAction`) | `ACKNOWLEDGED` |
| `RESOLVED` | Job quét lại, điều kiện đúng trở lại | `OPEN` (mở lại) |
| `ACKNOWLEDGED` | Job quét lại (bất kỳ kết quả nào) | **Giữ nguyên `ACKNOWLEDGED`** — không bị job ghi đè, coi là lịch sử đã xử lý |

### 18.5 Khách hàng — Giai đoạn phễu (`customers.stage`) và Khách tiềm năng (`customers.lead_status`)

| Trạng thái hiện tại | Sự kiện | Trạng thái mới |
|---|---|---|
| `LEAD` (Sẽ) | Chuyển giai đoạn (`changeStageAction`) | `PROSPECT` (Đang) — yêu cầu hồ sơ đủ trường bắt buộc |
| `PROSPECT` (Đang) | Chuyển giai đoạn | `DEALER` (Đại lý) — yêu cầu hồ sơ đủ trường bắt buộc |
| `LEAD` | Chốt thành khách (`convertLeadAction`, bắt buộc báo cáo + hồ sơ đủ) | `PROSPECT` hoặc `DEALER` tuỳ người dùng chọn |
| bất kỳ | Đưa về "Sẽ" | `LEAD`, đồng thời `lead_status` reset về `NOT_CONTACTED` |

`lead_status` (chỉ có ý nghĩa khi `stage = LEAD`): `NOT_CONTACTED` → `CONTACTED` hoặc `CANCELLED` (`updateLeadStatusAction`, bắt buộc ghi báo cáo) → `CONVERTED` (chỉ qua `convertLeadAction`, không qua `changeStageAction`).

---

## Phụ lục A — Mô hình dữ liệu (49 bảng, theo phân hệ)

| Phân hệ | Bảng chính |
|---|---|
| Nền tảng | `tenants`, `users`, `password_reset_tokens`, `user_territories`, `departments`, `audit_logs` |
| Danh mục | `territories`, `channels`, `product_groups`, `technologies`, `products`, `programs`, `program_products` |
| Khách hàng | `customers`, `customer_stage_history`, `customer_contacts`, `customer_notes`, `customer_attachments`, `customer_classifications`, `dealer_inactive_notes` |
| Bán hàng | `orders`, `order_lines`, `order_payments`, `order_attachments` |
| Chỉ tiêu & KPI | `targets`, `kpi_monthly`, `custom_kpis`, `custom_kpi_entries`, `custom_kpi_logs` |
| Công việc | `weekly_plans`, `plan_items`, `daily_reports`, `check_ins`, `customer_locations`, `tasks` |
| Trang chủ | `announcements` |
| Chuyển giao | `training_courses`, `customer_course_completions`, `events` |
| Đề xuất | `proposals`, `proposal_approvers` |
| Văn bản | `company_documents` |
| Sự kiện mời khách | `invite_events`, `invite_event_dept_quotas`, `invite_event_territory_quotas`, `invite_event_staff_quotas`, `invite_event_guests` |
| Cảnh báo & dự báo | `alerts`, `forecasts` |

**Enum quan trọng:** `user_role` (DIRECTOR/GDKV/MANAGER/SALES) · `customer_stage` (LEAD/PROSPECT/DEALER) · `class_tier` (A–E) · `order_status` (DRAFT/CONFIRMED/CANCELLED) · `alert_type` (7 loại, xem Mục 12) · `alert_severity` (INFO/WARNING/CRITICAL) · `alert_status` (OPEN/ACKNOWLEDGED/RESOLVED) · `proposal_status`/`approver_decision` (PENDING/APPROVED/REJECTED) · `product_role` (CORE/POTENTIAL/RECOVERY/NORMAL) · `territory_level` (REGION/PROVINCE/DISTRICT/WARD).

### A.1 Từ điển dữ liệu — các bảng trung tâm

Đầy đủ cột của những bảng quan trọng nhất (nơi hầu hết logic nghiệp vụ chạm vào). Các bảng còn lại (danh mục, junction, quota…) đơn giản hơn nhiều — đọc thẳng `src/db/schema.ts`, mỗi cột đã có chú thích tiếng Việt tại chỗ và là **nguồn chính xác nhất** khi tài liệu này lệch với mã nguồn.

**`users`** — tài khoản nhân viên
| Cột | Kiểu | Ý nghĩa |
|---|---|---|
| `email` | text, duy nhất theo `tenant_id` | Đăng nhập bằng email, không hỏi công ty |
| `password_hash` | text | Băm `scrypt` |
| `role` | enum | DIRECTOR/GDKV/MANAGER/SALES |
| `territory_id`, `department_id` | uuid | Khu vực địa lý / nhánh phân quyền (Mục 2.2) |
| `job_title` | text tự do | Chỉ hiển thị, **không quyết định quyền** |
| `can_upload_documents`, `can_edit_staff_info`, `can_post_announcements` | boolean | Cờ quyền lẻ (Mục 2.3) |
| `failed_login_count`, `locked_until` | | Khoá tạm sau 5 lần sai |
| `password_changed_at` | timestamp | Phiên cấp trước mốc này bị từ chối |

**`customers`** — hồ sơ khách hàng
| Cột | Kiểu | Ý nghĩa |
|---|---|---|
| `code` | text, duy nhất | Mã khách hàng |
| `stage` | enum | LEAD/PROSPECT/DEALER (Mục 18.5) |
| `lead_status` | text | Trạng thái chuyển đổi khi `stage=LEAD` (Mục 18.5) |
| `owner_id`, `territory_id`, `channel_id` | uuid | Người phụ trách / khu vực / kênh bán |
| `dealer_since` | date | Ngày mở code chính thức — mốc **bắt đầu** đếm số tháng liên tục không lấy hàng (trống thì dùng ngày tạo hồ sơ); số tháng đếm được hiển thị **tối đa 24** (giới hạn hiển thị cho gọn, không phải ngưỡng cảnh báo — ba ngưỡng cảnh báo thật là 2/3/6 tháng, xem Mục 12) |
| `is_key` | boolean | Đại lý Key (quản lý xác nhận tay) |
| `ward` | text | Phường/xã — tách riêng khỏi `territory_id` (cấp tỉnh trở lên) |

**`orders`** / **`order_lines`** / **`order_payments`** — xem đầy đủ ở Mục 7.1 và 18.1. Cột quan trọng nhất khi tính toán: `orders.total_amount` (tiền hàng, không gồm KM), `orders.surcharge_amount` (phụ thu — **cộng vào "phải thu" ở mọi nơi tính công nợ**), `order_lines.qty_sold` vs `qty_promo` (tách hẳn số bán và số khuyến mãi), `order_payments.amount` (mỗi lần thu là một dòng, không sửa đè).

**`targets`** — chỉ tiêu kế hoạch
| Cột | Kiểu | Ý nghĩa |
|---|---|---|
| `period_type` / `period_key` | enum / text | MONTH/QUARTER/YEAR, `'2026-08'`\|`'2026-Q3'`\|`'2026'` — **chỉ giao theo tháng thật sự**, quý/năm là cộng dồn |
| `scope_type` / `scope_id` | enum / uuid | COMPANY (scope_id null) \| DEPARTMENT \| TERRITORY \| USER \| CUSTOMER |
| `revenue_target`, `order_target`, `new_customer_target`, `sku_target` | | Bốn chỉ tiêu độc lập |

**`kpi_monthly`** — KPI tháng tự tính (đầu vào cảnh báo `STAFF_UNDERPERFORM`)
| Cột | Ý nghĩa |
|---|---|
| `revenue` / `revenue_target` / `achievement_pct` | Doanh thu thực hiện / chỉ tiêu / % (làm tròn) |
| `order_count`, `visit_count`, `new_customer_count`, `active_dealer_count` | Số liệu phụ trợ hiển thị cùng |
| `plan_compliance_pct` | % việc trong lộ trình tuần đã hoàn thành |

**`custom_kpis`** / **`custom_kpi_entries`** / **`custom_kpi_logs`** — KPI tự tạo (Mục 5.1). `custom_kpi_entries.actual_value` được **cộng dồn** mỗi lần nhân viên ghi log (`custom_kpi_logs`), không ghi đè — mọi báo cáo đọc `actual_value` vẫn khớp.

**`tasks`** — công việc (khác `plan_items` là lộ trình tuần)
| Cột | Ý nghĩa |
|---|---|
| `assignee_id` / `created_by` | Người thực hiện / người tạo (giao việc) |
| `status` | Chuỗi tự do, 6 giá trị — xem Mục 18.3 |
| `reason` | Bắt buộc khi Dời/Huỷ/Chưa hoàn thành |
| `start_date`, `due_date` | Ngày bắt đầu, hạn chót |

**`proposals`** / **`proposal_approvers`** — xem Mục 8 và 18.2. `proposals.details` (jsonb) = `{type, values}` theo `src/lib/proposal-types.ts`; `proposal_approvers.sort_order` = **bước duyệt** (cùng số = duyệt song song), `step_label` = tên bước hiển thị.

**`alerts`** — xem Mục 12 và 18.4. `entity_type` ∈ {customer, product, territory, user}; ràng buộc duy nhất `(tenant, type, entity_id, period_key)` là cơ chế chống trùng khi quét lại nhiều lần; `metrics` (jsonb) lưu số liệu để hiển thị không phải truy vấn lại.

**`forecasts`** — dự báo dự trù. `forecast_qty` = số máy tính ra; `adjusted_qty` (nullable) = số người dùng chỉnh tay, **giữ nguyên khi tính lại mỗi đêm**; ràng buộc duy nhất `(tenant, period_key, product_id)`.

**`invite_events`** + 4 bảng quota/guest — chuỗi giao chỉ tiêu 3 tầng (Mục 5.1): `invite_event_territory_quotas` (TGĐ giao theo khu vực) → `invite_event_dept_quotas` (GĐKV giao theo phòng/team) → `invite_event_staff_quotas` (Quản lý giao từng NVKD) → `invite_event_guests` (từng khách mời thật, cờ `wants_cme`/`wants_certificate`/`attended` riêng biệt).

**`company_documents`** — `category` (chuỗi, khoá ở `document-categories.ts`, thêm loại mới không cần migration); `file_path` trỏ vào `src/lib/storage.ts` (ngoài thư mục công khai, phục vụ qua `/api/anh/...` có kiểm quyền).

**`announcements`** — `scope_type`/`scope_id` (COMPANY/DEPARTMENT/TERRITORY) quyết định ai thấy; `expires_at`/`event_date` để tự ẩn; `is_pinned` để ghim.

## Phụ lục B — Danh sách trang chính

| Đường dẫn | Nội dung |
|---|---|
| `/` | Trang chủ: thông tin/thông báo, cảnh báo nổi bật, lịch 7 ngày tới |
| `/login`, `/quen-mat-khau`, `/dat-lai-mat-khau` | Đăng nhập / quên / đặt lại mật khẩu |
| `/tai-khoan` | Hồ sơ cá nhân |
| `/thong-bao`, `/tim-kiem` | Chuông thông báo, tìm kiếm nhanh |
| `/cong-viec`, `/cong-viec/hom-nay`, `/cong-viec/danh-sach`, `/cong-viec/chot-tuan`, `/cong-viec/lich` | Công việc: tổng quan, báo cáo ngày, danh sách việc, chốt tuần, lịch |
| `/cong-viec/de-xuat` | Phiếu đề xuất |
| `/khach-hang`, `/khach-hang/[id]`, `/khach-hang/moi`, `/khach-hang/[id]/sua`, `/khach-hang/tiem-nang` | Khách hàng: danh sách, chi tiết, thêm mới, sửa, khách tiềm năng |
| `/don-hang`, `/don-hang/[id]`, `/don-hang/moi`, `/don-hang/[id]/sua` | Đơn hàng: danh sách, chi tiết, thêm mới, sửa |
| `/san-pham` | Danh mục sản phẩm |
| `/van-ban` | Văn bản công ty |
| `/phan-tich`, `/bao-cao`, `/bao-cao/chuyen-giao`, `/bao-cao/in` | Phân tích, báo cáo, báo cáo chuyển giao, bản in báo cáo (qua chức năng in của trình duyệt) |
| `/canh-bao` | Cảnh báo & dự báo dự trù |
| `/danh-muc/*` | Danh mục: người dùng & phân quyền, nhân viên (kèm tab chuyên môn), phòng ban, khu vực, khoá chuyển giao, chỉ tiêu, KPI tự tạo, sự kiện mời khách, thông báo trang chủ, cài đặt chung, nhật ký thao tác, nhập Excel |

## Phụ lục C — Chữ ký các thao tác ghi chính

Mọi thao tác tạo/sửa/xoá đều là **server action** (`"use server"`), nhận một `FormData`, trả về `{ ok?: string; error?: string }` (không `throw` — bản chạy thật giấu nội dung lỗi ném ra). Bảng dưới liệt kê tên hàm, tệp chứa, và các trường `FormData` chính — đủ để gọi lại hoặc mở rộng mà không phải đọc hết file. Tham số phụ (ghi chú, tệp đính kèm...) xem trực tiếp trong file.

**Đơn hàng** (`src/app/(app)/don-hang/actions.ts`)
| Hàm | Trường chính | Khoá quyền |
|---|---|---|
| `createOrderAction` | `customerId`, `orderDate`, `line` (JSON, lặp lại theo dòng hàng), `dueDate`, `vatRequired`, `surchargeAmount` | Khách trong phạm vi người lập |
| `updateOrderAction` | như trên + `id` | Chỉ khi `DRAFT`; `CONFIRMED` thì chỉ Ban giám đốc |
| `approveOrderAction` | `id` | GĐKV / Ban giám đốc |
| `cancelOrderAction`, `deleteOrderAction` | `id` | Huỷ: quản lý trở lên (đơn `DRAFT`) / chỉ Ban giám đốc (`CONFIRMED`); xoá hẳn: chỉ Ban giám đốc |
| `recordPaymentAction`, `deletePaymentAction` | `orderId`, `amount`, `paidOn` | Chỉ đơn `CONFIRMED`; xoá chỉ quản lý trở lên |
| `requestOrderRevisionAction` | `id`, `note` | GĐKV / Ban giám đốc |
| `uploadOrderAttachmentAction`, `deleteOrderAttachmentAction` | `orderId`, `file` | Gỡ tệp của đơn `CONFIRMED`: chỉ Ban giám đốc |

**Khách hàng** (`src/app/(app)/khach-hang/actions.ts`)
| Hàm | Trường chính |
|---|---|
| `createCustomerAction`, `updateCustomerAction` | Toàn bộ trường hồ sơ (Mục 6.2) |
| `changeStageAction` | `id`, `stage` (LEAD→PROSPECT→DEALER) |
| `convertLeadAction`, `updateLeadStatusAction` | `id`, báo cáo bắt buộc (Mục 18.5) |
| `assignLeadsAction` | `ids[]`, `ownerId` — giao hàng loạt |
| `toggleKeyAction` | `id` — đánh dấu đại lý Key |
| `addContactAction`/`updateContactAction`/`deleteContactAction` | Người liên quan |
| `addNoteAction`/`deleteNoteAction` | Thông tin trao đổi |
| `uploadAttachmentAction`/`deleteAttachmentAction` | Tệp đính kèm hồ sơ |
| `recordCourseCompletionAction`/`deleteCourseCompletionAction` | `customerId`, `courseId`, `completedDate`, `isFree` |

**Phiếu đề xuất** (`src/app/(app)/cong-viec/de-xuat/actions.ts`) — xem Mục 18.2 cho bảng trạng thái: `createProposalAction` (`type`, `title`, `content`, `directorId`, `relatedDept[]`, `extraApprover[]`, `d_<trường riêng theo loại>`) · `decideProposalAction` (`proposalId`, `decision`, `note`) · `withdrawProposalAction` (`proposalId`) · `updateProposalAction`/`deleteProposalAction` (`proposalId` — chỉ Ban giám đốc).

**Công việc & lộ trình** (`src/app/(app)/cong-viec/actions.ts`, `.../danh-sach/actions.ts`)
| Hàm | Trường chính |
|---|---|
| `createTaskAction`/`updateTaskAction`/`deleteTaskAction` | `assigneeId`, `title`, `startDate`, `dueDate` — chỉ người tạo sửa/xoá |
| `updateTaskStatusAction` | `id`, `status`, `reason` (bắt buộc với Dời/Huỷ/Chưa hoàn thành) |
| `addPlanItemAction`/`setPlanItemStatusAction` | Lộ trình tuần: `planDate`, `content`, `customerId`, `status`, `missedReason` |
| `saveDailyReportAction`, `saveWeeklySummaryAction` | Báo cáo ngày / tổng kết tuần |
| `checkInAction` | `lat`, `lng`, ảnh tuỳ chọn — khoảng cách tính ở máy chủ |

**KPI** (`src/app/(app)/danh-muc/chi-tieu/actions.ts`, `.../kpi-tu-tao/actions.ts`)
| Hàm | Trường chính |
|---|---|
| `saveTargetAction`/`deleteTargetAction` | `periodKey`, `scopeType`, `scopeId`, 4 chỉ tiêu (Phụ lục A) |
| `createKpiAction`/`deactivateKpiAction` | `name`, `unit` — KPI tự tạo |
| `assignKpiAction`/`deleteEntryAction` | `kpiId`, `userId`, `periodKey`, `targetValue` |
| `recordActualAction`, `logKpiProgressAction` | `entryId`, kết quả — cộng dồn vào `actualValue` |
| `recomputeAction` | Chạy tay phân hạng/KPI/cảnh báo/dự báo (thường job đêm tự làm) |

**Cảnh báo & dự báo** (`src/app/(app)/canh-bao/actions.ts`)
| Hàm | Trường chính |
|---|---|
| `acknowledgeAlertAction` | `id` — chuyển `OPEN` → `ACKNOWLEDGED` |
| `adjustForecastAction` | `id`, `adjustedQty` (0–10.000.000, bỏ trống = quay lại số máy tính) |

**Nhập Excel hàng loạt** (`src/app/(app)/danh-muc/nhap-excel/actions.ts`, Mục 7.6) — chỉ Ban giám đốc: `previewAction` (`kind` ∈ san-pham/khach-hang/don-hang, `file` — chỉ đọc và kiểm tra, không ghi) · `confirmImportAction` (như trên — ghi thật, ghi nhật ký kèm tên file).

**Tài khoản & phân quyền** (`src/app/(app)/danh-muc/nguoi-dung/actions.ts`, `.../tai-khoan/actions.ts`, `src/app/login/actions.ts`)
| Hàm | Trường chính |
|---|---|
| `createUserAction`/`updateUserAction` | Hồ sơ + `role`, `departmentId`, `territoryId` — chỉ gán được vai trò thấp hơn người thao tác |
| `toggleUserActiveAction`, `deleteUserAction` | Khoá / xoá hẳn (chỉ tài khoản chưa có dữ liệu) |
| `toggleUserCanUploadDocumentsAction`, `toggleCanPostAnnouncementsAction`, `toggleCanEditStaffInfoAction` | Bật/tắt cờ quyền lẻ — chỉ cấp lại được cờ mình đang có |
| `resetPasswordAction`, `unlockUserLoginAction` | Cấp lại mật khẩu / mở khoá đăng nhập |
| `updateStaffInfoAction` | HCNS (cờ `canEditStaffInfo`) sửa hồ sơ người khác |
| `changePasswordAction`, `updateMyProfileAction` | Tự đổi mật khẩu / sửa hồ sơ chính mình |
| `loginAction`, `logoutAction` | Đăng nhập / đăng xuất |

## Phụ lục D — Quy tắc kỹ thuật bắt buộc khi phát triển tiếp

Toàn bộ nội dung chi tiết nằm ở mục **"Quy tắc bắt buộc khi viết mã"** trong `CLAUDE.md` — bản tóm tắt các quy tắc quan trọng nhất, vi phạm là có lỗi thật đã từng xảy ra:

1. Mọi truy vấn bảng nghiệp vụ bắt đầu bằng `scopeFilter()` — không bao giờ tự lọc riêng.
2. Không so sánh ngày bằng đối tượng `Date` trong truy vấn — dùng chuỗi `YYYY-MM-DD`.
3. Không dùng truy vấn con tương quan trong `sql` template khi câu lệnh ngoài không có JOIN.
4. Không đưa mảng JavaScript vào `sql` template (`= any(${mang})`) — dùng `inArray()`.
5. Mọi thao tác tạo/sửa/xoá dữ liệu nghiệp vụ phải gọi `writeAudit()`.
6. Trang có số liệu điều hành phải khai báo `force-dynamic`.
7. Không đưa dữ liệu người dùng vào `sql.identifier()` hay `sql.raw()` (chặn bằng luật ESLint).

## Phụ lục E — Tài liệu liên quan

- `CLAUDE.md` — ghi chú kỹ thuật đầy đủ, lịch sử quyết định, các lỗi đã gặp
- `HUONG-DAN-TRIEN-KHAI-HAWKHOST.md` — triển khai và cập nhật phiên bản
- `TAI-LIEU-BAN-GIAO-VA-BAO-TRI.md` — bàn giao, sao lưu, giám sát, bảo trì
- `THIRD-PARTY-LICENSES.md` — giấy phép thư viện bên thứ ba
- `trao-doi-khach/bo-test-case-2026-09-19.xlsx` — bộ test case và bảng đối chiếu từng dòng yêu cầu gốc (không đưa vào kho mã nguồn, lưu trữ riêng)
