129 lines
17 KiB
Markdown
129 lines
17 KiB
Markdown
# 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` có `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` có `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 50–900 · 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).
|