# Modal "Thêm thư mục" — màn Kho tài liệu

> Tài liệu con của [`KHO-TAI-LIEU.md`](./KHO-TAI-LIEU.md). Hàm dùng chung nằm trong `KHO-TAI-LIEU.functions.ts`.
> Khảo sát trực tiếp bằng Playwright MCP ngày **2026-07-27**, **khảo sát lại 2026-07-30** (bổ sung
> testId thật cho toàn bộ field chính + hành vi đổi cấp cha) trên sitdev, tài khoản `ecm01`.

> 📌 **Phạm vi tài liệu — chỉ mô tả màn hình** (field, nút, testId, cấu trúc modal, 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`. Xem `KHO-TAI-LIEU.md` mục đầu file.

---

## 1. Cách mở

Từ màn chi tiết BHS → tab **Cấu trúc hồ sơ** (`lbl-tab-cauTrucHoSo`):

| Cách                        | Thao tác                                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------------------- |
| Tạo thư mục ở gốc BHS       | Hover `.ant-dropdown-trigger` **đầu tiên** trong `.ant-tabs-tabpane-active` → click `mni-them-thu-muc` |
| Tạo thư mục con trong 1 TM  | Hover dòng thư mục → hover `[data-testid^="btn-more-action-"]` → click `mni-them-thu-muc`          |

Sau khi click, **chờ ~8 s** cho modal nạp metadata.
`lbl-modal-title` = `"Thêm thư mục"`.

> ⚠️ `mni-them-thu-muc` tồn tại ở cả 2 dropdown → click qua `page.locator(".ant-dropdown:visible").last()`.

> ⚡ Tạo nhiều thư mục liên tiếp: truyền **`moLaiBoHoSo: false`** để bỏ bước mở lại BHS (~37 s/lần) —
> sau khi lưu modal tự đóng và app trả về đúng tab "Cấu trúc hồ sơ" (mục 5). Chi tiết ở JSDoc của
> `openModalThemThuMuc`. ⚠️ Chưa chạy kiểm chứng trên môi trường thật.

---

## 2. Cấu trúc modal

Modal có **2 tab**: `Thông tin` và `Phân quyền` (tab Phân quyền có badge trạng thái: `Kế thừa` / `Độc lập`).

**Cả hai tabpanel đều nằm trong DOM** — phải scope field theo tabpanel đang active:

```ts
const folderModal = page.locator(".ant-modal-content:visible").last();
const activePane = folderModal.locator('div[role="tabpanel"][aria-hidden="false"]');
```

---

## 3. Tab "Thông tin" — danh sách field

| Nhãn                       | testId                                                     | required | Ghi chú                                                              |
| -------------------------- | ---------------------------------------------------------- | :------: | --------------------------------------------------------------------- |
| **Tên**                    | `txt-ten-thu-muc`                                          |    ✔     | Ô "Tên" gồm 2 input: `txt-ma-thu-muc` (**disabled**, mã tự sinh) + input nhập tên này |
| **Index thư mục**          | `txt-ma-thu-muc`                                           |    —     | Input **disabled** — mã hệ thống tự sinh (`A`, `B`… ở gốc; `A.1` khi có cấp cha). QA gọi là "trường Index thư mục". Case: `tests/1.4.1/1.4.1.166.spec.ts` |
| Thư mục/Hồ sơ cấp cha      | `tree-sel-thu-muc-cha`                                     |          | **tree-select** — xem mục 3b                                          |
| Độ mật                     | `sel-do-mat`                                               |          | Options: `Thường`, `Mật`, `Tuyệt mật`. Mặc định **kế thừa từ cấp cha** |
| Trạng thái hiển thị        | `sel-trang-thai-hien-thi`                                  |          | Options: `Public`, `Private`. Mặc định **kế thừa từ cấp cha**          |
| Đơn vị soạn thảo           | `tree-sel-submissionUnit`                                  |   (✔)    | **Bắt buộc tuỳ cấu hình cấu trúc hồ sơ** — xem mục 5                 |
| Đơn vị lưu bản gốc         | `tree-sel-originalStorageUnit`                             |          | —                                                                     |
| Người ký                   | `pp-multi-signatory` (+ `-select`, `-orgchart-btn`)        |          | People-picker, placeholder `Nhập tên người ký`                       |
| Mã hiệu văn bản đính kèm   | `txa-textCode`                                             |          | textarea                                                              |
| Loại văn bản đính kèm      | `sel-relatedType`                                          |          | —                                                                     |
| Độ mật (văn bản đính kèm)  | `sel-security`                                             |          | Mặc định `Thường`                                                    |
| Nội dung                   | `txa-mce-notes`                                             |          | **Rich text TinyMCE** (iframe) — không dùng `fill()` trực tiếp        |
| *(field động)* test datefield | `date-test_datefield`                              |          | Metadata động theo cấu trúc hồ sơ — **không hard-code**              |

> ✅ **Khảo sát lại bằng MCP 2026-07-30**: build hiện tại **đã có `data-testid` cho toàn bộ field
> chính** của modal (kể cả Tên / Index / cấp cha / Độ mật / Trạng thái hiển thị — trước đây tài liệu
> ghi là "chưa có testId, phải bám `#tenTaiLieu`, `#capMatHS`, `#trangThai`"). Hằng `THU_MUC_FIELD`
> trong `KHO-TAI-LIEU.functions.ts` gom các testId này.

---

### 🚨 3a. Phần lớn field metadata ĐANG BỊ ẨN — luôn kiểm tra `isVisible()` trước khi điền

Các field metadata nằm trong `.ant-form-item` có thêm class **`ant-form-item-hidden`** cộng với class
kiểu dữ liệu (`TEXT`, `CHOICE`, `PEOPLE`, `DEPARTMENT`, `DATETIME`). Field nào hiển thị là do
**cấu hình "Cấu trúc hồ sơ"** của BHS quyết định — khác nhau giữa các BHS và giữa các cấp thư mục.

Kết quả đo trên BHS khảo sát (2026-07-27):

| testId                           | Trạng thái                                     |
| -------------------------------- | ---------------------------------------------- |
| `tree-sel-submissionUnit`        | ✅ hiển thị (height 30px)                      |
| `tree-sel-originalStorageUnit`   | ❌ ẩn (`ant-form-item DEPARTMENT ant-form-item-hidden`) |
| `pp-multi-signatory`             | ❌ ẩn (`... PEOPLE ant-form-item-hidden`)      |
| `txa-textCode`                   | ❌ ẩn (`... TEXT ant-form-item-hidden`)        |
| `sel-relatedType`                | ❌ ẩn (`... CHOICE ant-form-item-hidden`)      |
| `sel-security`                   | ❌ ẩn (`... CHOICE ant-form-item-hidden`)      |
| `txa-mce-notes`                  | ❌ ẩn (`... TEXT ant-form-item-hidden`)        |
| `date-test_datefield`            | ❌ ẩn (`... DATETIME ant-form-item-hidden`)    |

```ts
// ✅ Luôn guard trước khi điền field metadata:
const field = activePane.getByTestId("tree-sel-originalStorageUnit");
if (await field.isVisible().catch(() => false)) {
  await pickFirstOption(page, field);
}
```

> ⚠️ Element ẩn **vẫn tồn tại trong DOM** nên `count()` > 0 và `getByTestId` tìm thấy —
> nhưng `click`/`fill` sẽ **timeout**. Đừng dùng `count()` để kiểm tra sự tồn tại của field.

**Field "Nội dung" (`txa-mce-notes`)** là **TinyMCE inline mode** (không có iframe):
bên trong là `div.mce-content-body[contenteditable="true"]`.

```ts
const body = modal.locator('[data-testid="txa-mce-notes"] .mce-content-body');
await body.click();
await body.fill("Nội dung ghi chú");
```

> ⚠️ **Chưa kiểm chứng được thao tác điền** — trên BHS khảo sát field này đang ẩn nên không click được.
> Cấu trúc DOM (`.mce-content-body`, contenteditable, không iframe) thì đã xác minh trực tiếp.

---

## 3b. Trường "Thư mục/Hồ sơ cấp cha" — hành vi khi đổi cấp cha (khảo sát MCP 2026-07-30)

- testId `tree-sel-thu-muc-cha`, là **tree-select**; option là `.ant-select-tree-title` với text
  `"<mã>. <tên>"` (vd `"B. TM aba"`). Có ô search (nhập được).
- Dropdown **chỉ liệt kê thư mục** — tài liệu trong BHS không xuất hiện; **không có** mục "gốc BHS",
  nên mở modal từ gốc BHS thì ô này **rỗng**.
- 🚨 **Đổi cấp cha thì app tự cập nhật 4 nhóm thông tin theo cha mới** (đo trực tiếp: BHS
  `Mật`/`Public`, cha `Tuyệt mật`/`Private`):

  | Trường              | Trước | Sau  | Logic                     |
  | ------------------- | ----- | ---- | ------------------------- |
  | `txt-ma-thu-muc`    | `E`   | `E.1` | `<mã cha>.<thứ tự con>`  |
  | `sel-do-mat`        | `Mật` | `Tuyệt mật` | kế thừa từ cấp cha |
  | `sel-trang-thai-hien-thi` | `Public` | `Private` | kế thừa từ cấp cha |
  | Khối phân quyền (tab Phân quyền) | quyền của BHS | quyền của thư mục cha | kế thừa từ cấp cha |

- ✅ Dùng hàm **`doiThuMucCapCha(page, tenCha, modal?)`**; đọc giá trị select bằng
  **`docGiaTriSelectThuMuc(modal, testId)`**. Case kiểm tra: `tests/1.4.1/1.4.1.177.spec.ts`.

---

## 4. Tab "Phân quyền"

### 4a. Trạng thái mặc định — Kế thừa

- Badge trên tab: `"Kế thừa"`
- Alert đầu modal: `"Thư mục/Tài liệu này đang kế thừa quyền từ thư mục cha"` +
  nút `"Đặt quyền độc lập"` trong `.ant-alert-action`
- Trong tab: dòng chú thích *"Quyền đang được kế thừa từ thư mục cha. Thay đổi quyền ở thư mục cha
  sẽ ảnh hưởng đến thư mục này."* và *"* Hiển thị quyền đang được kế thừa từ thư mục cha (chỉ đọc)"*
- 5 dòng **Quyền Owner / Quyền Tạo mới / Quyền Cập nhật / Quyền Tải file / Quyền Xem** —
  🚨 **KHÔNG phải người dùng nhập được**: khi đang kế thừa app chỉ render **nhãn + avatar** quyền
  kế thừa (rỗng nếu không ai có quyền đó), **không có ô nhập nào**. Ô nhập (`.people-picker`)
  chỉ xuất hiện **sau khi** bấm "Đặt quyền độc lập" (mục 4b).
  *(Xác nhận 2026-07-28 qua ảnh màn "Cập nhật thư mục" do QA/dev cung cấp — chưa đọc DOM bằng MCP.)*

```ts
// Assert trạng thái kế thừa / chỉ đọc:
await expect(activePane.locator(".ant-select-selection-search-input")).toHaveCount(0);
// ✅ Dùng hàm `expectPickerPhanQuyenChiDoc(page)`.
```

> 🚨 Ngược lại, **đừng** assert trạng thái "đã enable" bằng `toBeEditable()` trên ô search:
> AntD gắn `readonly` cho ô này bất cứ khi nào select đang đóng (kiểm chứng 2026-07-28 —
> `<input readonly ... class="ant-select-selection-search-input">` ngay sau khi đã đặt quyền độc lập).
> Muốn khẳng định enable thì **click vào ô, gõ thử từ khoá, xem có gợi ý không** —
> dùng `expectPickerNhapDuoc` / `expectPickerPhanQuyenChinhSuaDuoc`.

5 khối quyền **đã có `data-testid`** (khảo sát MCP 2026-07-30 — trước đây tài liệu ghi phải dùng
`.people-picker` `.nth(i)`):

| Quyền          | testId (hằng `FOLDER_PICKER`)   |
| -------------- | ------------------------------- |
| Quyền Owner    | `pp-multi-usersRightOwner`      |
| Quyền Tạo mới  | `pp-multi-usersRightAdd`        |
| Quyền Cập nhật | `pp-multi-usersRightEdit`       |
| Quyền Tải file | `pp-multi-usersRightDownload`   |
| Quyền Xem      | `pp-multi-usersRightViewers`    |

🚨 **Khi đang kế thừa, mỗi khối chỉ có avatar chữ viết tắt** (vd `HL`) — **tên đầy đủ không có trong
DOM**. Muốn assert theo tên người: **hover avatar** (`avatar-container`) → `.ant-popover` hiện
`viết tắt / tên đầy đủ / chức vụ / email`. ✅ Dùng hàm
**`layNguoiTrongKhoiQuyenThuMuc(page, permTestId, modal?)`** (tự hover từng avatar và đọc popover).

### 4b. Bật quyền độc lập

Click nút `"Đặt quyền độc lập"` (`.ant-alert-action` → `getByRole("button", { name: "Đặt quyền độc lập" })`)
→ mở **confirm dialog** `.ant-modal-confirm` (modal thứ 3 chồng lên):

| Phần     | Giá trị                                                                                                                          |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Title    | `"Đặt quyền độc lập?"`                                                                                                            |
| Nội dung | `"Toàn bộ quyền của thư mục/tài liệu này sẽ được đặt lại và không còn kế thừa từ thư mục cha. Hành động này không thể hoàn tác."` |
| Nút      | `Hủy` / `Xác nhận`                                                                                                                |

Sau khi `Xác nhận`:

- Badge tab đổi `Kế thừa` → **`Độc lập`**
- Alert đổi thành `"Thư mục/Tài liệu này có quyền độc lập — không kế thừa từ thư mục cha"` +
  nút `"Khôi phục kế thừa"`
- Chú thích trong tab: *"Quyền được cấu hình riêng cho thư mục này. Thay đổi quyền ở thư mục cha sẽ không ảnh hưởng."*
- 5 people-picker trở thành **chỉnh sửa được**

> ⚠️ **Các chip đã kế thừa VẪN CÒN NGUYÊN** trong picker sau khi bật độc lập. Bật độc lập chỉ tách
> thư mục khỏi kế thừa cho các thay đổi **sau đó** — muốn thư mục thật sự chỉ còn account mình gán,
> phải **xoá hết chip cũ trước** (option `clearInherited: true` của `createThuMuc`).
>
> `clearInherited` **chỉ tác động lên đúng picker được chỉ định**. Kiểm chứng 2026-07-27: xoá chip ở
> picker VIEW rồi thêm `ecm06` → thư mục có `Hoàng Văn Mạnh: VIEW`, nhưng picker OWNER vẫn giữ
> nguyên 2 chip kế thừa (`Nguyễn Minh Hoàng`, `Đỗ Mạnh Cường`) và cả 2 vẫn là OWNER của thư mục.

### 4c. Thao tác với chip trong people-picker

Mỗi chip là 1 `span.ant-tag`; nút xoá là **`svg` cuối chip** (không phải `.anticon-close` của AntD):

```ts
const picker = activePane.getByTestId(FOLDER_PICKER.VIEW);
// xoá hết chip:
while ((await picker.locator(".ant-tag").count()) > 0) {
  await picker.locator(".ant-tag svg").first().click({ force: true });
  await page.waitForTimeout(600);
}
// thêm account:
const input = picker.locator(".ant-select-selection-search-input");
await input.click();
await input.pressSequentially("ecm06", { delay: 150 });
await page.waitForTimeout(5000); // chờ kết quả search
await page.keyboard.press("Enter");
await page.keyboard.press("Escape"); // đóng gợi ý còn sót, tránh che nút Xác nhận
```

> ✅ Toàn bộ đoạn trên đã gói trong `createThuMuc(..., { perm: { account, permTestId, clearInherited } })`.

---

## 5. Lưu

Nút `Xác nhận` / `Hủy` nằm ở **footer modal** (ngoài tabpanel) → scope `folderModal`, không scope `activePane`.

```ts
await folderModal.getByRole("button", { name: /Xác nhận/ }).click();
```

**Validate**: field bắt buộc thiếu → `.ant-form-item-explain-error` **bên trong folderModal**.

> ⚠️ Trên BHS khảo sát, ngoài "Tên" còn có `Trường đơn vị soạn thảo bắt buộc!` —
> tức **`tree-sel-submissionUnit` là bắt buộc**, dù `label` **không** có marker `span.icon-required`.
> Danh sách field bắt buộc **phụ thuộc cấu hình "Cấu trúc hồ sơ"** của từng BHS → hàm `createThuMuc`
> điền sẵn `tree-sel-submissionUnit` để an toàn; nếu BHS khác báo thiếu field khác, bổ sung vào `opts`.

**Thành công**: toast `.ant-message-notice` = `"Thành công"`, modal **tự đóng**, dòng mới xuất hiện
trong bảng Cấu trúc hồ sơ với tên hiển thị `"<mã prefix>. <tên>"` (vd `"A. AT-TM-001"`).

---

## 6. Dùng hàm

```ts
// Thư mục ở gốc BHS, kế thừa quyền:
await createThuMuc(admin, pw, recordUrl, `AT-TM-1-${ts}`);

// Thư mục có quyền độc lập, gán ecm05 vào Quyền Xem:
await createThuMuc(admin, pw, recordUrl, `AT-TM-2-${ts}`, {
  quyenDocLap: true,
  perm: { account: "ecm05", permTestId: FOLDER_PICKER.VIEW },
});

// Xoá hết chip kế thừa trước khi gán:
await createThuMuc(admin, pw, recordUrl, `AT-TM-3-${ts}`, {
  quyenDocLap: true,
  perm: { account: "ecm05", permTestId: FOLDER_PICKER.OWNER, clearInherited: true },
});

// Gán 1 account vào TẤT CẢ 5 trường quyền cùng lúc (permTestId nhận cả mảng):
await createThuMuc(admin, pw, recordUrl, `AT-TM-6-${ts}`, {
  quyenDocLap: true,
  perm: { account: "ecm07", permTestId: Object.values(FOLDER_PICKER) },
});

// Thư mục con bên trong 1 thư mục đã có:
await createThuMuc(admin, pw, recordUrl, `AT-TM-4-${ts}`, { parentName: `AT-TM-1-${ts}` });

// Đặt Độ mật / Trạng thái hiển thị khác giá trị kế thừa (để test kế thừa xuống cấp con):
await createThuMuc(admin, pw, recordUrl, `AT-TM-5-${ts}`, {
  doMat: "Tuyệt mật",
  trangThaiHienThi: "Private",
});
```

---

## 6b. Modal "Cập nhật thư mục"

Mở bằng menu hành động của dòng thư mục → `mni-cap-nhat` (⚠️ đổi `page.url()` sang `itemId` của
thư mục — xem `KHO-TAI-LIEU.md` mục 4.4). ✅ Dùng hàm **`openManCapNhatThuMuc(page, recordUrl, ten)`**.

**Bố cục gần như giống hệt modal "Thêm thư mục"** (xác nhận 2026-07-28 qua ảnh do QA/dev cung cấp):

- Tiêu đề: `"Cập nhật thư mục <tên thư mục>"`
- Cùng 2 tab `Thông tin` / `Phân quyền` (+ badge `Kế thừa` / `Độc lập`)
- Alert + nút `"Đặt quyền độc lập"` trong `.ant-alert-action`, confirm dialog `Xác nhận` giống mục 4b
- Footer: `Xác nhận` / `Hủy`
- Khác biệt chính: xem mục 4a — **khi còn kế thừa thì 5 trường quyền chỉ là nhãn + avatar,
  ô nhập chỉ xuất hiện sau khi đặt quyền độc lập**

> ⚠️ Chưa đọc DOM bằng MCP → chưa xác nhận modal này có `lbl-modal-title` / `btn-save` hay không.
> Các hàm liên quan (`getThuMucModal`, `openTabThuMuc`, `datQuyenDocLap`) đều bám tab/alert/text
> nên không phụ thuộc testId; riêng assert tiêu đề đang dùng `toContainText` ở cấp modal.

---

## 7. ⚠️ Chưa khảo sát

- Nút **"Khôi phục kế thừa"** *trong modal này* (sau khi đã đặt quyền độc lập): chưa bấm thử.
  (Nút tương đương ở modal Phân quyền nâng cao — `"Kế thừa quyền"` — thì **đã kiểm chứng**,
  xem `KHO-TAI-LIEU.MODAL-PHAN-QUYEN-NANG-CAO.md` mục 6c.)
- Điền field **Nội dung** `txa-mce-notes`: field đang ẩn trên BHS khảo sát nên chưa click được (mục 3a).
- Menu **"Nhân bản"**, **"Kéo thả"**, **"Chuyển lên/xuống"** của dòng thư mục: mới thấy tên trong
  dropdown, chưa mở ra xem. (`"Cập nhật"` và `"Xóa"` **đã khảo sát** — xem `KHO-TAI-LIEU.md` mục 4.4.)
