Files
solution-erp/.claude/workflows/runs/2026-07-30-S162-cay-4-folder-gd/sub-frontend-designer-0.md
2026-07-30 09:46:39 +07:00

129 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).
## MỐC 2 — T1c + T1d + T1e + T1f XONG
T1c `fe-user/src/pages/khkk/KhkkListPage.tsx`: +import `PipelineTreePanel`; PageHeader giữ full-width, phần còn lại bọc lưới `lg:grid-cols-[19rem_minmax(0,1fr)] xl:[21rem_…]` với `<PipelineTreePanel currentStage={2} />` cột trái (folder "Kế hoạch Hợp đồng" mở sẵn); cột phải `min-w-0 space-y-5` chứa NGUYÊN KpiCard + ô tìm + bảng + phân trang. 2 sửa nhỏ chống vỡ layout: hàng KpiCard `lg:grid-cols-6``xl:grid-cols-6` (6 thẻ không nhồi vào cột hẹp) và `<table>` bọc `overflow-x-auto`. Query `['khkk-list',…]`, `FILTER_TO_PHASE`, chip-lọc-không-ghi-URL: KHÔNG đụng.
T1d `fe-user/src/pages/contracts/HardCopiesPage.tsx`: cùng khuôn, `currentStage={4}` (folder "Hợp đồng cứng" mở sẵn). PageHeader + ghi chú 1-MỐC amber giữ full-width; ô tìm + bảng + `HardCopyPanel` (upload/download/ý kiến duyệt) vào cột phải; `<table>` bọc `overflow-x-auto`. Query `['hard-copies',…]` + `?type=` server-side + `SEALED_COPY=3`: KHÔNG đụng.
T1e mirror fe-admin — CẢ 6 file, verify `sha256sum` SAU khi copy, KHỚP 6/6:
| file | sha256[0:16] |
|---|---|
| components/pipeline/PipelineStageFolders.tsx | 4b37bf51d5211348 |
| components/pipeline/PipelineTreePanel.tsx | 127acc85399b033c |
| hooks/usePipelineStages.ts | e762d5eb9c3a057a |
| pages/pe/PurchaseEvaluationsListPage.tsx | 1d8d8f75c7d2cc14 |
| pages/khkk/KhkkListPage.tsx | 40a650bf6964ab4b |
| pages/contracts/HardCopiesPage.tsx | d30a911e7483f96a |
`fe-admin/src/types/contracts.ts` KHÔNG đè (bản admin `ContractPhaseColor` khác — cố ý, component chỉ đọc key nên vẫn biên dịch).
T1f build gate:
- fe-user `npm run build`**EXIT 0**, `✓ built in 21.91s`, `dist/assets/index-CaEssrop.js 1,629.67 kB │ gzip 411.70 kB`, css 91.82 kB.
- fe-admin `npm run build`**EXIT 0**, `✓ built in 10.15s`, `dist/assets/index-BtjKK7qJ.js 1,713.48 kB │ gzip 425.60 kB`, css 86.14 kB.
- Warning cả 2 app = PRE-EXISTING (chunk >500 kB · INEFFECTIVE_DYNAMIC_IMPORT `src/lib/realtime.ts`). 0 lỗi TS.
## MỐC 3 — FD2 visual-verify (3 vòng soi, KHÔNG ship-unseen)
Rig: authed dev-rig bị chặn (gotcha #3 — API HTTPS :5443 ⟂ vite proxy HTTP + ProtectedRoute) ⇒ dùng **static harness** như S79: mount CHÍNH component thật (không vẽ lại markup) với `queryClient.setQueryData` seed 3 cache index + menu quyền seed `localStorage:solution-erp-user-menu` (AuthProvider giữ menu cache khi `/menus/me` lỗi — `AuthContext.tsx:29-31`) + `MemoryRouter`. 4 khung: A cây trang PE · B panel GĐ2 · C panel GĐ4 · D trạng thái biên (khoá/lỗi/rỗng). Chụp `desktop-1440` + `laptop-1366` + `mobile-375`, `device_scale_factor=2`, full_page. Gotcha Vite đã né: `domcontentloaded` + `wait_for_selector` (KHÔNG networkidle) + warm-up goto 60s.
**Vòng 1 (r1) — 3 lỗi tự bắt được khi NHÌN ảnh:**
1. 🔴 Panel B/C **trông y hệt nhau**, folder giai đoạn nằm sâu 3 lớp collapsed ⇒ vào trang KHKK/HĐ-cứng KHÔNG thấy folder của mình đâu (hỏng đúng ý owner "folder trang hiện tại mở sẵn"). Kèm bug tương tác: mẹo "set rỗng = mặc định mở năm mới nhất" làm node **KHÔNG ĐÓNG ĐƯỢC** (đóng → set rỗng → mặc định bật lại).
2. 🔴 Dòng folder **khoá quyền lệch 26px** so với 3 dòng kia (thiếu khoang icon-chip) — soi rõ ở khung D mobile.
3. 🟡 Leaf: mã `font-mono 11px` bẹt ngang dòng phụ sans 11px ⇒ mất phân cấp.
**Sửa:** (1) `defaultOpen` = mở HẲN 1 NHÁNH (năm mới nhất > dự án đầu > hạng mục đầu) + state `Set|null` (`null` = chưa từng chỉnh) thay mẹo set-rỗng + đang-lọc thì ép mở hết & `toggleExpand` no-op. (2) dòng khoá giữ đúng 3 khoang (ổ khoá thay chevron + icon-chip + nhãn). (3) mã leaf `text-[12px] font-medium`.
**Vòng 2 (r2):** cả 3 PASS — B mở tới folder "Kế hoạch Hợp đồng" hiện leaf `KHKK-2026-001`, C mở "Hợp đồng cứng" hiện `HĐ-TP-2026-014 · Đã lưu bản cứng` ⇒ B/C giờ KHÁC nhau rõ, chứng minh `currentStage` ăn thật. Còn 1 điểm tự bắt: nhãn muted `text-slate-400` (#94a3b8) = **2.8:1 < sàn AA 4.5:1** mà folder rỗng VẪN bấm mở được (chữ đang-hoạt-động) → đổi `text-slate-500` (4.8:1); chip "Sắp"/icon giữ slate-400 (trạng thái trơ + token verbatim dải strip).
**Vòng 3 (r3):** chụp lại sau sửa contrast — PASS, không lệch layout.
**Rubric FD4:** hierarchy ✓ (5 cấp: Năm 13px semibold → Dự án/Hạng mục 12px slate-700 → folder 12px accent → leaf mã 12px mono + phụ 11px slate-500) · spacing ✓ (py-1.5 dòng cây / py-2 leaf, rail `ml-3 border-l` mỗi cấp) · typography ✓ · contrast ✓ (brand-800 13.0:1 · violet-700 7.4:1 · amberx-700 4.9:1 · teal-700 5.6:1 · muted slate-500 4.8:1) · states ✓ (hover/focus-visible ring brand + loading skeleton + empty + error + locked) · responsive ✓ (1440/1366/375; `<lg` panel thu về nút "Hiện cây", bảng không bị đẩy) · detail ✓ (4 dòng folder thẳng hàng tuyệt đối sau fix 2).
Ảnh giữ lại: `harness-shot-r3-desktop-1440.png` + `harness-shot-r3-mobile-375.png` (cùng thư mục run). Harness gốc archive `harness-s162.tsx.txt` + `harness-s162.html.txt`, **đã XÓA khỏi cây src cả 2 app** (verify `ls` = No such file).
## MỐC 4 — RE-MIRROR + BUILD GATE CUỐI
Bảng SHA MỚI (bảng ở MỐC 2 đã thối vì 3 fix — 2 file đầu đổi hash), đo sau khi copy lại đủ 6 file:
| file | sha256[0:16] | verdict |
|---|---|---|
| components/pipeline/PipelineStageFolders.tsx | 05c93c3619a9cfec | SHA-PAIR |
| components/pipeline/PipelineTreePanel.tsx | 76e5dafb239e402a | SHA-PAIR |
| hooks/usePipelineStages.ts | e762d5eb9c3a057a | SHA-PAIR |
| pages/pe/PurchaseEvaluationsListPage.tsx | 1d8d8f75c7d2cc14 | SHA-PAIR |
| pages/khkk/KhkkListPage.tsx | 40a650bf6964ab4b | SHA-PAIR |
| pages/contracts/HardCopiesPage.tsx | d30a911e7483f96a | SHA-PAIR |
Build gate CUỐI (sau khi đã xoá harness khỏi src):
- fe-user: `npx tsc -b --force`**EXIT 0**; `npm run build`**EXIT 0**, `dist/assets/index-JKWooGXp.js 1,630.09 kB │ gzip 411.78 kB` · css 91.82 kB · `✓ built in 472ms`.
- fe-admin: `npx tsc -b --force`**EXIT 0**; `npm run build`**EXIT 0**, `dist/assets/index-DHQO_qL3.js 1,713.91 kB │ gzip 425.69 kB` · css 86.14 kB · `✓ built in 520ms`.
- Warning 2 app = PRE-EXISTING (chunk >500 kB · INEFFECTIVE_DYNAMIC_IMPORT realtime.ts). 0 lỗi TS.
- `git status --porcelain`: đúng 12 file sản phẩm (6 × 2 app) + 4 artifact thư mục run. KHÔNG commit (em main commit).
## 2 lần lead báo "mày chết" — ĐÍNH CHÍNH bằng đĩa
Cả 2 lần đều là **stale IDE diagnostic** (bài `feedback_stale_diagnostic_background_agent`, gotcha #68), KHÔNG phải #53:
- Lần 1 (báo `:625 ')' expected` + 4 symbol unused): đo lại `npx tsc -b --force` ngay lúc đó = **EXIT 0**, vùng `:620-627` trên đĩa khâu đúng.
- Lần 2 (báo KhkkListPage `:114/:138/:141` + HardCopiesPage `:97/:116/:250-264` rách JSX): đọc thẳng 3 vùng đó trên đĩa = nguyên vẹn, `tsc -b --force` = **EXIT 0**.
⇒ Diagnostic chụp GIỮA lúc agent đang Edit nhiều file. Chỉ tin build chạy SAU khi agent dừng tay.
## MỐC 5 — MEMORY + đóng việc
- `.claude/agent-memory/frontend-designer/MEMORY.md`: +1 entry S162 (Activity log). L1 vượt ngân sách (19.6KB > sàn 17.1KB) ⇒ nén theo đúng khuôn archive sẵn có: chuyển VERBATIM 3 mục 06/2026 (S79 pe-decouple · S69 3-shared-UI · S69 OfficeDashboardPage) sang `archive/2026-06.md` + để lại dòng trỏ `substring:"…"`; cập nhật `archive/_INDEX.md` (5→8 entry). Sau nén **14.6KB**, integrity check 7/7 khoá value_protect còn nguyên (S162 · S159 · EmployeesListPage · #53 · Tailwind-v4-accent-stop · authed-rig-#3 · lucide-alias-dodge).
- KHÔNG commit (em main commit, scope `FE-User`/`FE-Admin`). KHÔNG đụng BE/menu/seeder/strip/tests.
### TỰ ĐÁNH GIÁ CÒN HỞ (lead cần biết)
1. **Chưa soi được trên PROD/authed thật** — mọi ảnh là harness tĩnh với dữ liệu giả. Nút cần nghiệm thu khi deploy: cây có ra đúng gói thầu thật không, và `pageSize 200` có đủ cho lượng HĐ/KHKK thật không (>200 thì leaf GĐ3 rơi về dạng tối giản, GĐ4 mất cờ bản cứng).
2. **GĐ3/GĐ4 nối qua `pe.contractId`** — phiếu PE chưa gắn HĐ thì 2 folder đó rỗng dù HĐ có tồn tại (đi đường KHKK→HĐ mà không ghi ngược `contractId` về phiếu là cây đứt). Đây là giới hạn DỮ LIỆU, không phải UI.
3. Trang GĐ3 (cụm HĐ 7 loại) CHƯA gắn panel — đúng spec đợt-1 (để đợt-2 sau khi owner xem prod).