wal: flush 20260730T0911

This commit is contained in:
pqhuy1987
2026-07-30 09:11:32 +07:00
parent 2554845b7f
commit 2ac5fe67dc
4 changed files with 100 additions and 12 deletions

View File

@ -0,0 +1,24 @@
# run: 2026-07-30-S162-cay-4-folder-gd
goal: Lệnh anh 30-07 sáng (mid-turn, kèm screenshot khoanh đỏ dải 4 GĐ → cây): "Tạo folder: Duyệt NCC / Kế hoạch Hợp đồng. / Duyệt Hợp đồng / Hợp đồng cứng." AskUser chốt 2 quyết: (1) folder đặt DƯỚI TỪNG GÓI THẦU · (2) áp CẢ các trang GĐ khác.
## Spec contract (lead-defined literal)
- Nhãn folder VERBATIM anh gõ: "Duyệt NCC" · "Kế hoạch Hợp đồng" · "Duyệt Hợp đồng" · "Hợp đồng cứng" (folder GĐ2 ≠ nhãn strip "Kế hoạch ký kết HĐ" — CỐ Ý, lời-mới-nhất-thắng; KHÔNG đổi strip/menu).
- Vị trí: trong cây `yearGroups → projects → workItems` của [PurchaseEvaluationsListPage.tsx](../../../fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx) (dựng nhóm tại `:210-255`, key gói = `(projectId, workItemId ?? '__no_workitem__')` `:228`) — chèn cấp folder GĐ giữa WorkItemGroup và items.
- Nội dung folder: GĐ1 = PE phiếu (render leaf CŨ, chỉ re-parent) · GĐ2 = KHKK match `khkk.purchaseEvaluationId ∈ PE-ids của gói` (KhkkListItemDto `khkk.ts:106+`, leaf = maKeHoach + phase chip, click → `/khkk/:id`) · GĐ3 = HĐ qua `pe.contractId` (purchaseEvaluation.ts list DTO, click → `/contracts/:id`) · GĐ4 = HĐ đó với `hasSealedCopy === true` (contracts.ts:100, click → `/hard-copies`).
- Đếm count per folder; folder rỗng = muted + chip "SẮP" (tái dùng token màu dải strip ARC-2); folder GĐ của TRANG hiện tại mặc định expanded, 3 folder kia collapsed.
- Search/filter đang active ⇒ chỉ hiện folder có nội dung khớp (không 4 folder rỗng × mọi gói); không filter ⇒ đủ 4.
- Permission 2 tầng (bài S118 #82): usePermission key Khkk/Ct trước khi fetch — không quyền ⇒ folder khoá + SẮP, KHÔNG call API; fetch lỗi ⇒ "—", không vỡ cây.
- Trang áp dụng đợt-1: PE list (GĐ1) + [KhkkListPage.tsx](../../../fe-user/src/pages/khkk/KhkkListPage.tsx) (GĐ2, bảng giữ bên phải, cây panel trái) + [HardCopiesPage.tsx](../../../fe-user/src/pages/contracts/HardCopiesPage.tsx) (GĐ4) — fe-user; fe-admin mirror CHỈ khi trang twin cùng khuôn (HardCopies ×2 SHA-pair W7). Trang GĐ3 (cụm HĐ 7-loại 6-leaf) = đợt-2 sau khi anh xem prod.
- CẤM: sửa BE · menu/seeder (#84) · strip/labels hiện hữu · default export · logic duyệt phiếu.
## taskList snapshot
- T1 designer: component chung PipelineStageFolders + gắn 3 trang fe-user (+admin twin) + build gate
- T2 reviewer: adversarial diff vs contract trên
- T3 cicd-monitor: post-push verify (background)
## Stages
- [ ] T1 designer → sub-frontend-designer-0.md
- [ ] T2 reviewer → sub-reviewer-1.md
- [ ] T3 ship + cicd → ship-synthesis.md

View File

@ -0,0 +1,57 @@
# sub-frontend-designer-0 — T1 cây 4-folder GĐ (S162, 2026-07-30)
Ghi-đĩa-trong-lúc-làm (chống #53). Append theo mốc.
## MỐC 0 — khảo sát đĩa (DONE)
Nguồn đã đọc thật (không nhớ):
- `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx` 791 dòng — tree 4 tầng `yearGroups` memo `:210-252`; leaf phiếu `<ul className="ml-3 divide-y ...">` `:510-609`; localStorage expand key `pe_list_expanded_projects_v3`.
- `fe-user/src/types/khkk.ts``KhkkListItemDto:106``purchaseEvaluationId`, `maKeHoach`, `phase`, `peTenGoiThau`; `KHKK_PHASE_LABELS`/`KHKK_PHASE_BADGE`; `PagedResult<T>` khai TRONG file này.
- `fe-user/src/pages/khkk/KhkkListPage.tsx` 265 dòng — query `['khkk-list',{phase,search,page,deletedView}]``GET /contract-signing-plans`.
- `fe-user/src/types/purchaseEvaluation.ts:112-155``PeListItem``projectId/projectCode/projectName/workItemId/workItemName/contractId/maPhieu/tenGoiThau/phase`.
- `fe-user/src/types/contracts.ts:84-101``ContractListItem` + `hasSealedCopy:100`. ⚠️ `types/contracts.ts` KHÔNG phải SHA-pair (chỉ `ContractPhaseColor` khác giữa 2 app) nhưng `ContractListItem` + `ContractPhase(Label)` GIỐNG HỆT ⇒ component chung import an toàn.
- `fe-user/src/pages/contracts/HardCopiesPage.tsx` 447 dòng — query `['hard-copies',{type,search,page}]``GET /contracts?phase=9`.
- `fe-user/src/App.tsx` — route có thật: `/purchase-evaluations` `:70` · `/khkk/list` `:81` · `/khkk/:id` `:84` · `/contracts/:id` `:65` · `/hard-copies` `:69`.
- `fe-user/src/components/pe/PePipelineStrip.tsx` — token dải strip: active `bg-brand-600 text-white`, non-active `border border-dashed border-slate-300 bg-white text-slate-500`, chip `rounded bg-slate-100 px-1 text-[9px] font-semibold uppercase tracking-wide text-slate-400` + chữ "Sắp" (uppercase CSS ⇒ hiện "SẮP"). **KHÔNG sửa file này.**
- `fe-user/src/hooks/usePermission.ts``can(menuKey, action='Read')`; `findNode` không thấy key ⇒ false.
- Menu key thật (đọc `DbInitializer.cs:1770-1790` + `MenuKeys.cs:42-43`): GĐ2 root `KeHoachKyKet`, group `Khkk_G1`, leaf `Khkk_List` · GĐ3 root `Contracts` + `Ct_<Code>_List` · GĐ4 root `HopDongCung` + `Hdc_<Code>`. ⚠️ `GetMyMenuTreeQuery.cs:95-96` trả node khi `CanRead=true` HOẶC có con CanRead ⇒ gate phải OR nhiều key, KHÔNG chỉ root.
- `PagedResult.cs:20` — pageSize **clamp ≤200** (không 400) ⇒ index query dùng `pageSize:200` an toàn.
- lucide-react: đã node-check `ClipboardCheck/FileCheck/FileSignature/Archive/Stamp/Lock/ChevronRight/Folder/FolderOpen/ShieldCheck` — CÓ ĐỦ (tránh bẫy alias-dodge).
- Token màu `fe-user/src/index.css @theme:5-55`: brand 50900 · teal/violet/greenx 50/100/500/600/700 · **amberx** (KHÔNG phải `amber` custom) 50/100/500/600/700. ⇒ dùng `-700` cho head (trừ brand được -800).
SHA-pair fe-user ↔ fe-admin (đo `sha256sum`, 16 ký tự đầu):
| file | user | admin | verdict |
|---|---|---|---|
| pages/contracts/HardCopiesPage.tsx | c7dcc20b4c2751e0 | c7dcc20b4c2751e0 | PAIR |
| pages/khkk/KhkkListPage.tsx | f74e04ee5fc827c6 | f74e04ee5fc827c6 | PAIR |
| pages/pe/PurchaseEvaluationsListPage.tsx | 072b16dc6c003ac1 | 072b16dc6c003ac1 | PAIR |
| hooks/usePermission.ts | 39867b1ca38b6789 | 39867b1ca38b6789 | PAIR |
| types/khkk.ts | 8b757f5b14221ffc | 8b757f5b14221ffc | PAIR |
| index.css | e8631471c86a9699 | e8631471c86a9699 | PAIR |
| types/contracts.ts | fedfc6a4dd208e56 | 7c13942ddfe0cd90 | DIFF (chỉ ContractPhaseColor) |
**T1e = mirror CẢ 3 trang + 3 file mới** (không trang nào lệch khuôn).
## Quyết định thiết kế (chốt trước khi code)
1. 3 file MỚI: `components/pipeline/PipelineStageFolders.tsx` (presentational 4 folder) · `hooks/usePipelineStages.ts` (permission-gate + 2 index query + builder) · `components/pipeline/PipelineTreePanel.tsx` (panel cây trái cho trang GĐ2/GĐ4).
2. Query key MỚI `['pipeline-khkk-index']` / `['pipeline-contract-index']` / `['pipeline-pe-index']` — KHÔNG dùng lại key `khkk-list`/`hard-copies`/`pe-list` để không đổi shape cache trang khác (bài S159 `select`-transform).
3. KHÔNG sửa `yearGroups` memo của PE page (giữ verbatim) — panel dùng builder riêng trong file mới. Duplicate CÓ CHỦ ĐÍCH, đổi lấy zero-risk cho trang đang chạy.
4. Nhãn folder VERBATIM: "Duyệt NCC" · "Kế hoạch Hợp đồng" · "Duyệt Hợp đồng" · "Hợp đồng cứng".
5. Màu: GĐ1 brand · GĐ2 violet · GĐ3 amberx · GĐ4 teal (khớp accent `HardCopiesPage`).
## MỐC 1 — T1a + T1b XONG (typecheck sạch)
3 file MỚI (fe-user, named export, `import type` verbatimModuleSyntax, 0 npm dep mới):
1. `fe-user/src/components/pipeline/PipelineStageFolders.tsx` — THUẦN TRÌNH BÀY. `PIPELINE_STAGE_LABELS` nhãn VERBATIM owner. 1 folder = `<details>` controlled (`useState(stage.n===currentStage)`) ⇒ folder của TRANG hiện tại mở sẵn, 3 folder kia đóng, đều collapse/expand được. Summary = chevron (rotate 90 qua `group-open/stage`, `motion-reduce:transition-none`) + icon-chip lucide + nhãn + [count | "Sắp" | "—"]. Màu: GĐ1 brand-800/brand-50 · GĐ2 violet-700 · GĐ3 amberx-700 · GĐ4 teal-700 (stop hợp lệ, KHÔNG -800 cho accent). Chip "Sắp" copy token strip verbatim (uppercase CSS ⇒ hiện SẮP). `locked` ⇒ render dòng khoá (`Lock` + title "Bạn chưa có quyền xem giai đoạn này"), KHÔNG `<details>`. `failed` ⇒ badge "—" + body "Không tải được dữ liệu giai đoạn này." `content` ⇒ chèn nguyên khối tự vẽ (GĐ1 trang PE).
2. `fe-user/src/hooks/usePipelineStages.ts` — dữ liệu + PERMISSION 2 TẦNG. `usePipelineLinkage()` gate 4 nhóm key (OR nhiều key vì `GetMyMenuTreeQuery.cs:96` trả root canRead=false khi con có quyền; `Ct_*` kế thừa `Contracts`, `Khkk_*`/`Hdc_*` KHÔNG): PE `['PurchaseEvaluations','Pe_DuyetNcc_List','Pe_DuyetNccPhuongAn_List']` · KHKK `['Khkk_List','Khkk_G1','KeHoachKyKet']` · HĐ `['Contracts', 'Ct_<7 code>_List']` · HĐ cứng `['HopDongCung','Hdc_<7 code>']`. 2 query index key MỚI `['pipeline-khkk-index']` (`GET /contract-signing-plans?page=1&pageSize=200`) + `['pipeline-contract-index']` (`GET /contracts?page=1&pageSize=200`), `enabled` = cờ quyền ⇒ KHÔNG quyền = KHÔNG gọi API (0 rác 403), `staleTime 60s`, `retry 1`. `buildStages(peItems, opts)` dựng 4 folder/gói: GĐ2 = `khkkByPeId` theo `purchaseEvaluationId``/khkk/{id}` · GĐ3 = HĐ theo `pe.contractId``/contracts/{id}` · GĐ4 = đúng HĐ đó khi `hasSealedCopy===true``/hard-copies?type={c.type}`. HĐ ngoài index (list kẹp 200) vẫn có leaf tối giản ở GĐ3 thay vì biến mất im lặng (nhưng không suy được cờ bản cứng ⇒ không vào GĐ4).
3. `fe-user/src/components/pipeline/PipelineTreePanel.tsx` — panel cây trái dùng chung cho trang GĐ khác (T1c/T1d). Cùng cấu trúc 📅 Năm > 📁 Dự án > 🧱 Hạng mục > 4 folder, `buildTree()` riêng (KHÔNG đụng memo `yearGroups` của trang PE). Query `['pipeline-pe-index']` `GET /purchase-evaluations?pageSize=200`, gate `canPe`. Ô lọc client-side (dự án/hạng mục/mã phiếu/tên gói) ⇒ đang lọc thì `hideEmpty`. Expand nhớ ở `localStorage:pipeline_tree_expanded_v1`, mặc định mở năm mới nhất. Nút "Hiện/Ẩn cây" chỉ hiện `<lg` (mobile không bị cây đẩy bảng xuống). 4 trạng thái: no-permission / skeleton / lỗi + "Thử lại" / rỗng.
T1b — `fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx` (sửa 4 chỗ, KHÔNG đụng logic):
- `:32-33` +2 import.
- `:49-56` + `const { buildStages } = usePipelineStages()` + `filterActive = search.trim()!=='' || phase!=='' || pendingMe || deletedView`.
- `:520-526` khối `<ul>` leaf đổi thành `<PipelineStageFolders className="ml-3 border-l border-slate-200" currentStage={1} hideEmpty={filterActive} stages={buildStages(wg.items,{stage1Content:(<ul className="divide-y divide-slate-100">`
- `:625` đóng `) })} />`.
- 🔴 CHỌN CÓ CHỦ ĐÍCH: giữ NGUYÊN thụt lề khối `<li>` phiếu (dòng 527-623) ⇒ diff chỉ đụng 2 mẩu bọc, CHỨNG MINH leaf/behavior (chọn phiếu, chip GẤP, PeFinalizeChip, nút Maximize2, view "Đã xóa") không đổi 1 byte. Memo `yearGroups`, `selectRow/expandRow/collapseFocus/closeDetail/onApproved/onDeleted`, overlay S79, 3 query key cũ: KHÔNG đụng.
- `yearGroups` KHÔNG đổi ⇒ badge count ở summary Hạng mục vẫn là số phiếu; folder GĐ1 lặp lại số đó là CỐ Ý (đọc theo hàng ngang: 1 gói có mấy phiếu / mấy KHKK / mấy HĐ).
Verify: `npx tsc -b --force` trong fe-user ⇒ **TSC_EXIT=0** (build đầy đủ chạy ở MỐC 3).