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

17 KiB
Raw Blame History

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.tsKhkkListItemDto:106purchaseEvaluationId, 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-155PeListItemprojectId/projectCode/projectName/workItemId/workItemName/contractId/maPhieu/tenGoiThau/phase.
  • fe-user/src/types/contracts.ts:84-101ContractListItem + 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.tscan(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-6xl: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 buildEXIT 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 buildEXIT 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 --forceEXIT 0; npm run buildEXIT 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 --forceEXIT 0; npm run buildEXIT 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).