# Màn hình "Tầng" — Danh mục ngăn (Settings › Compartments)

> Tài liệu kỹ thuật **đầy đủ** cho màn hình `/settings/compartments` (thư mục code `06.tang`, hiển thị UI là **"Ngăn"**).
> Đây là màn **danh mục CRUD đơn giản** (List + Modal Thêm/Sửa), không có trang chi tiết riêng.
> Dùng cho UC9 (tests/uc9/). Phần fixture/account/TIMEOUT/PW method dùng chung toàn dự án xem `tests/README.md`.

> 📌 **Phạm vi tài liệu — chỉ mô tả màn hình** (field, nút, testId, cấu trúc modal/bảng, thông báo,
> luồng thao tác). **Không** ghi logic test / kỳ vọng của case / quy tắc nghiệp vụ vào đây — chỗ của
> chúng là spec `tests/<mục>/`, `<mục>.steps.ts` và `<mục>.md`.

---

## 0. File function đi kèm — `TANG.function.ts`

**Thao tác dài của màn này đã được đóng gói thành hàm** trong `src/screen-instructions/TANG.function.ts`.
Khi viết spec: **import hàm từ file đó, KHÔNG copy code dài vào spec.**

```ts
import {
  gotoTangList, discoverKhoGiaPairs, discoverGiaValues, createTang,
  searchKeyword, toggleQuickCheckboxOption, closeQuickCheckboxPopover,
  openFilterDrawer, resetAllFilters, deleteTangByCodes,
} from "../../../src/screen-instructions/TANG.function";
```

| Hàm | Công dụng |
| --- | --- |
| `gotoTangList(page, pw)` | Vào `/settings/compartments` + hard wait |
| `discoverKhoGiaPairs(page, pw, count?)` → `{kho, gia?}[]` | Màn "Kho tài liệu"/"Giá" chưa có data-testid nên **không tạo mới được** qua UI — hàm này đọc `count` Kho tài liệu (+ Giá tương ứng nếu có) **đang có sẵn** trong môi trường qua dropdown của modal tạo Ngăn, không lưu gì cả |
| `discoverGiaValues(page, pw, kho, count?)` → `string[]` | Đọc `count` Giá có sẵn thuộc 1 Kho cụ thể — dùng cho case lọc theo Giá (cần 2 Giá khác nhau cùng 1 Kho) |
| `createTang(page, pw, {tenTang, code, khoTaiLieu, gia?, active?})` | Tạo mới 1 Ngăn qua `mdl-tang`, assert toast "Thành công" |
| `searchKeyword(page, pw, keyword)` | Điền `txt-search-tang` — tự search sau debounce 500ms, không có nút riêng |
| `toggleQuickCheckboxOption(page, testId, label)` | Tick 1 option trong dropdown checkbox multi-select tuỳ biến (`sel-quick-kho-tai-lieu`/`sel-quick-gia`) — gọi nhiều lần để chọn nhiều giá trị, chưa đóng popover |
| `closeQuickCheckboxPopover(page, pw)` | Đóng popover checkbox đang mở (Escape) + chờ load |
| `openFilterDrawer(page, pw)` | Mở `drw-bo-loc-tang` qua `btn-toggle-filter` nếu đang đóng |
| `resetAllFilters(page, pw)` | Mở drawer rồi bấm `btn-clear-filter` — xoá **toàn bộ** filter (quick + nâng cao dùng chung 1 `filterForm`) |
| `deleteTangByCodes(page, pw, codes[])` | Tick checkbox từng dòng theo Mã, bấm `btn-delete`, xác nhận `Modal.confirm` ("Xác nhận"), chờ toast "Thành công" — dùng dọn dữ liệu test cuối spec. Phải bỏ hết filter trước khi gọi để đảm bảo mọi dòng cần xoá đang hiển thị |

> ⚠️ `sel-quick-kho-tai-lieu`/`sel-quick-gia` là component custom `DropdownSelectField` (checkbox popover), **không phải** antd `<Select>` chuẩn — không dùng được `pw.inputDropDownList` cho 2 field này. `sel-quick-active` thì vẫn là antd `<Select>` chuẩn, dùng `pw.inputDropDownList` bình thường.
> ⚠️ Nút xoá (`btn-delete`/`mni-delete`) mở `Modal.confirm` ("Bạn có chắc chắn muốn xóa?") — phải bấm nút "Xác nhận" mới xoá thật, khác với 1 số modal khác trong dự án không có bước confirm này.

---

## 0b. Kết quả rà soát data-testid (trước khi viết test)

Đã đọc toàn bộ 5 file trong `01.libs/01.ecm/src/pages/protected/99.settings/06.tang/`:
`01.TangList.tsx`, `02.TangModalNew.tsx`, `03.TangFilterQuickForm.tsx`, `04.TangFilterForm.tsx`, `05.TangImportExcel.tsx`.

**Kết luận: màn hình đã có đầy đủ `data-testid`/`testId` cho mọi phần tử tự thao tác được** — đủ để viết test tự động, không cần đề xuất bổ sung thêm.

So sánh đối chiếu: các màn danh mục cùng cấp (`04.Gia`, `05.ke`, `01.kho-tai-lieu`) hiện **chưa có `data-testid` nào** trong code, trong khi `06.tang` đã được gắn đầy đủ — không thiếu gì so với các màn tương tự.

Hai phần không có testid riêng nhưng **không phải thiếu sót** (dùng chung component toàn dự án, đã có cơ chế test riêng):

- Field thuộc tính động (eform) trong modal — render bởi `<FormGenerator>` dùng chung, không do màn Tầng tự quản lý.
- `TangExcelModalComp` (`05.TangImportExcel.tsx`) — chỉ truyền `templateCode={EXPORT_EXCEL_CODE.TANG}` vào `<ImportExcelModal>` dùng chung; testid nội bộ nằm trong component chung này (giống mọi màn import excel khác trong dự án).

---

## 1. Thông tin màn hình

| Mục          | Giá trị                                                                                  |
| ------------ | ----------------------------------------------------------------------------------------- |
| URL          | `/settings/compartments` (hằng `ECM_URLS.DL_DANH_MUC_TANG`)                              |
| Tên hiển thị | "Ngăn" (label UI), tên code/entity là `Tang`/`TangEntity`                                 |
| Vị trí phân cấp | Kho tài liệu → Giá (`04.Gia`) → **Ngăn** (`06.tang`)                                    |
| Permission   | `PermissionType.ECMAddCompartment` / `ECMEditCompartment` / `ECMDeleteCompartment` / `ECMManageAttributesCompartment` |
| Import/Export excel code | `EXPORT_EXCEL_CODE.TANG`, `THIET_LAP_TYPE.TANG`                              |
| Eform (thuộc tính động) | `FormTemplateType.Compartment`                                                |

---

## 2. Danh sách (List) — cột bảng & testid

| testId              | Cột / Ý nghĩa                                          |
| -------------------- | ------------------------------------------------------- |
| `lbl-stt`            | STT (số thứ tự theo trang)                              |
| `lbl-code`           | Mã ngăn                                                  |
| `lbl-ten-tang`       | Tên ngăn                                                 |
| `lbl-kho-tai-lieu`   | Kho tài liệu (lấy từ `khoTaiLieu` hoặc `gia.khoTaiLieu`) |
| `lbl-ma-gia`         | Mã giá (`gia.code`)                                      |
| `lbl-ten-gia`        | Tên giá (`gia.tenGia`)                                   |
| `lbl-active`         | Kích hoạt — hiển thị "Có"/"Không"                        |

> Ngoài các cột cứng trên, bảng còn nối thêm cột động từ eform (`eformColumns`) — không có testid cố định, tuỳ cấu hình thuộc tính.

### Bảng & dòng

| testId / locator         | Mô tả                                                                 |
| ------------------------- | ----------------------------------------------------------------------- |
| `tbl-tang`                | Container bảng danh sách                                                |
| `row-tang-${code}`        | Set trên `<tr>` mỗi dòng qua `onRow` — dùng `record.code` làm hậu tố    |

### Toolbar trái

| testId                  | Mô tả                                                                                    |
| ------------------------- | ----------------------------------------------------------------------------------------- |
| `btn-add`                | Thêm mới — mở `mdl-tang` ở chế độ tạo (yêu cầu quyền `ECMAddCompartment`)                 |
| `btn-import-excel`       | Mở modal nhập excel (yêu cầu quyền `ECMAddCompartment`)                                    |
| `btn-export-excel`       | Xuất excel (dùng lại filter hiện tại qua `tangService.getTangList`)                       |
| `btn-delete`             | Xoá hàng loạt — chỉ hiện khi đã chọn ≥1 dòng **và** có quyền `ECMDeleteCompartment`; thay thế cụm `btn-add`/`btn-import-excel`/`btn-export-excel` |
| `btn-more`               | Nút "..." — hover để mở dropdown (`mni-delete`, `mni-manage-attributes`)                   |
| `mni-delete`             | Mục xoá trong dropdown `btn-more` — chỉ hiện khi có dòng chọn + quyền xoá                  |
| `mni-manage-attributes`  | Mục "Quản lý thuộc tính" — mở `AttributeConfigDetail` (`FormTemplateType.Compartment`)     |

> ⚠️ Khi có dòng được chọn (`selectedRowKeys.length > 0`) **và** có quyền xoá, toolbar trái đổi hẳn từ cụm Thêm/Import/Export sang chỉ còn `btn-delete` — test cần bỏ chọn (`setSelectedRowKeys([])`) trước khi thao tác lại các nút Thêm/Import/Export.

### Toolbar phải

| testId               | Mô tả                                             |
| ---------------------- | ---------------------------------------------------- |
| `btn-toggle-filter`   | Bật/tắt panel filter nâng cao (`drw-bo-loc-tang`)  |

---

## 3. Quick filter (thanh filter nhanh trên đầu bảng)

Component: `03.TangFilterQuickForm.tsx` (`TangQuickFilter`).

| testId                     | Loại            | Ghi chú                                                          |
| ---------------------------- | ----------------- | ------------------------------------------------------------------ |
| `txt-search-tang`           | text (keyword)   | Ô tìm kiếm nhanh — set qua prop `testId` của `QuickFilterLayout`  |
| `sel-quick-kho-tai-lieu`   | dropdown         | Lọc theo Kho tài liệu (chỉ hiện kho `active=true`)                |
| `sel-quick-gia`             | dropdown         | Lọc theo Giá — danh sách giá tự lọc lại theo `khoTaiLieu` đã chọn |
| `sel-quick-active`         | dropdown         | Lọc theo Kích hoạt (Có/Không)                                      |

> ⚠️ `sel-quick-gia` phụ thuộc `filterValues.khoTaiLieu` — đổi Kho trước sẽ tự load lại danh sách Giá tương ứng.

---

## 4. Filter nâng cao (drawer bên phải)

Component: `04.TangFilterForm.tsx`.

| testId                | Mô tả                                                                        |
| ------------------------ | -------------------------------------------------------------------------------- |
| `drw-bo-loc-tang`       | Container drawer filter                                                         |
| `btn-clear-filter`      | Xoá toàn bộ filter đang áp dụng (chỉ có tác dụng khi filter đang không rỗng)     |
| `btn-close-filter`      | Đóng drawer filter (toggle lại `btn-toggle-filter`)                             |

Các field lọc bên trong drawer render động qua `<CustomizeFilter configs={filterConfig} .../>` (kết hợp `filterConfig` từ `useListing()` + `eformFilters`) — testId của từng field phụ thuộc cấu hình field trả về từ server, không cố định trong code màn này.

---

## 5. Modal "Thêm mới / Cập nhật Ngăn"

Component: `02.TangModalNew.tsx` (`TangDetail`).

| testId               | Loại            | required | Ghi chú                                                                                     |
| ----------------------- | ----------------- | :--------: | ----------------------------------------------------------------------------------------------- |
| `mdl-tang`            | modal container  |            | Đặt trên `ECMCustomModal`                                                                       |
| `txt-ten-tang`        | text             |    ✔     | Tên ngăn                                                                                        |
| `txt-code`            | text             |    ✔     | Mã — tự uppercase khi gõ (`onInput` toUpperCase)                                                |
| `sel-kho-tai-lieu`    | select           |    ✔     | Kho tài liệu — `disabled` khi đang sửa (`disabled={!!tang?.id}`); đổi giá trị sẽ reset field Giá |
| `sel-gia`             | select           |            | Giá — danh sách lọc theo Kho tài liệu đã chọn                                                    |
| `lbl-ma-gia`          | readonly label   |            | Mã giá tự hiển thị theo Giá đã chọn (`checkGia`)                                                 |
| `chk-active`          | checkbox         |            | Kích hoạt — mặc định `true` khi tạo mới                                                          |
| `btn-save`            | button           |            | Lưu — chỉ hiện khi không bị disable theo quyền                                                   |
| `btn-cancel`          | button           |            | Đóng modal, không lưu                                                                            |

> ⚠️ `isDisable = tang?.id ? !currentUser.canAccessAny([ECMEditCompartment]) : false` — khi **sửa** mà không có quyền `ECMEditCompartment`: mọi field bị readonly, `sel-kho-tai-lieu`/`sel-gia` đổi từ `<Select>` sang `<span>` cùng testId, và `btn-save` không hiển thị.
> ⚠️ `sel-kho-tai-lieu` luôn `disabled` khi cập nhật ngăn đã tồn tại (`tang?.id` có giá trị), **kể cả khi user có đủ quyền sửa** — không thể đổi Kho tài liệu sau khi đã tạo.
> Field thuộc tính động (eform) hiển thị thêm bên dưới `chk-active` nếu `formTemplate.eFormSettings` có cấu hình — do `<FormGenerator>` quản lý, không có testid riêng của màn Tầng.

### Nghiệp vụ khi Lưu (`saveResult`)

1. Validate field eform trước (nếu có) — lỗi đầu tiên hiện qua `message.error`.
2. Check trùng mã: gọi `tangService.checkExistCode(code)` — nếu trùng (và không phải đang sửa đúng bản ghi đó) → toast lỗi "Mã đã tồn tại", **không đóng modal**.
3. Gọi `tangService.add`/`update` tương ứng, thành công → toast "Thành công" → đóng modal → resolve promise `show()` với bản ghi mới (list gọi lại `search` để reload).

---

## 6. Xoá ngăn (`deleteItems` / `tangService.deletedStock`)

Trả về `boolean` (xoá được) hoặc object mô tả lý do chặn xoá:

| Điều kiện response | Toast lỗi hiển thị |
| --------------------- | --------------------- |
| `result.taiLieu` truthy | "Tồn tại tài liệu đang được lưu trữ trong ngăn. Vui lòng chuyển toàn bộ tài liệu bên trong sang Ngăn khác để thực hiện xóa" |
| `result.hop` hoặc `result.cap` (không có `taiLieu`) | "Tồn tại item {hộp `tenHopHoSo`	 | cặp `tenCapHoSo`} đang được liên kết với kho. Vui lòng chuyển toàn bộ item sang ngăn khác để thực hiện xóa" |
| Xoá thành công (`true`/không phải object lỗi) | "Thành công" — reset về trang 1, bỏ chọn dòng |

Có thể xoá đồng thời từ 2 chỗ: `btn-delete` (toolbar trái) hoặc `mni-delete` (dropdown `btn-more`) — cùng gọi chung `deleteItems`.

---

## 7. Import Excel

Component: `05.TangImportExcel.tsx` (`TangExcelModalComp`) — chỉ là wrapper mỏng quanh `<ImportExcelModal>` dùng chung toàn dự án.

- Mở qua `btn-import-excel` → `importExcelModalRef.current?.show()`.
- `templateCode={EXPORT_EXCEL_CODE.TANG}`, validate qua `thietLapImportExcelService.compareDataImport(rows, THIET_LAP_TYPE.TANG)`.
- Sau khi import thành công (`isChanged`), toast "Cập nhật thành công!" rồi tự gọi `reloadList` (= `search`) để load lại danh sách.
- Toàn bộ testid của field/step trong modal nằm trong component chung `ImportExcelModal`/`ImportExcelFooter` (`@ecm/components/import-excel/`) — giống hệt cơ chế đã dùng ở các màn danh mục khác (`Gia`, `Vùng`, `Loại tài liệu`, …), không định nghĩa lại trong `06.tang`.

---

## 8. Phân quyền theo permission

Toàn màn dùng 4 permission:

| Permission                                | Ảnh hưởng                                                                              |
| -------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `ECMAddCompartment`                        | Hiện `btn-add`, `btn-import-excel`; không bị áp `permissionGiaIds` khi search/export      |
| `ECMEditCompartment`                       | Cho phép sửa field trong modal (`isDisable` phụ thuộc quyền này khi `tang?.id` tồn tại)   |
| `ECMDeleteCompartment`                     | Hiện checkbox chọn dòng, `btn-delete`, `mni-delete`                                        |
| `ECMManageAttributesCompartment`           | Hiện `mni-manage-attributes` (qua `getManageAttributesPermission(location.pathname)`)      |

> ⚠️ User **không có** cả 3 quyền Add/Edit/Delete: hệ thống tự giới hạn dữ liệu hiển thị/xuất theo `permissionGiaIds` — chỉ thấy Giá thuộc Kho tài liệu mà user có trong `groupThuThu` (tính qua `khoTaiLieuService.paginate({ groupThuThu: idPrincipalUserAndGroupCurrUser })` rồi lọc `giaService`). Cần dựng dữ liệu test tương ứng nếu viết case theo vai không có quyền.

---

## 9. Lưu ý quan trọng

1. `sel-kho-tai-lieu` trong modal luôn `disabled` khi sửa ngăn đã tồn tại — test đổi Kho tài liệu chỉ áp dụng được lúc **Thêm mới**.
2. Trùng mã báo lỗi ngay tại bước Lưu (`checkExistCode`) — không chặn ở UI trước đó (không debounce khi gõ).
3. Bỏ chọn hết dòng (`selectedRowKeys = []`) trước khi thao tác `btn-add`/`btn-import-excel`/`btn-export-excel` — 3 nút này bị thay thế bởi `btn-delete` khi đang có dòng chọn.
4. Đổi `sel-quick-kho-tai-lieu` ở quick filter sẽ tự load lại option của `sel-quick-gia` — chờ dữ liệu load xong trước khi chọn Giá.
5. Field thuộc tính động (eform) và toàn bộ modal Import Excel dùng component dùng chung của dự án — không tìm testid riêng trong code màn Tầng, tham khảo cách các UC khác đã test 2 phần này (nếu có) thay vì suy đoán lại.
6. Khi tự đọc option của dropdown antd (`sel-kho-tai-lieu`, `sel-gia`, ...) bằng locator riêng thay vì `pw.inputDropDownList`: **không query `.ant-select-item-option-content` thẳng trên toàn trang** — class này dùng chung cho MỌI dropdown antd, dropdown vừa đóng có thể còn vài trăm ms fade-out vẫn tính là `:visible`, dễ đọc nhầm option của dropdown khác. Phải scope vào đúng panel `.ant-select-dropdown:visible` đang mở gần nhất (`.last()`) — xem `getOpenDropdownOptions`/`closeDropdown` (nội bộ, không export) trong `TANG.function.ts`.
