[CLAUDE] FE-User+FE-Admin: cay 4 folder GD duoi tung goi thau (Duyet NCC / Ke hoach HD / Duyet HD / HD cung) x2 app + 5 fix review F-1/3/4/6/7
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m50s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m50s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -19,6 +19,7 @@ goal: Lệnh anh 30-07 sáng (mid-turn, kèm screenshot khoanh đỏ dải 4 GĐ
|
||||
- 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
|
||||
- [x] T1 designer → sub-frontend-designer-0.md (VERDICT: SHIP-READY; 2 lần dừng-sớm resume từ diary — file trên đĩa nguyên vẹn, diagnostics giữa-Edit là snapshot dở #68; tự soi ảnh 3 vòng bắt 4 lỗi UX)
|
||||
- [x] T2 reviewer → sub-reviewer-1.md (VERDICT: PASS-WITH-FLAGS — 0 blocker, 8 flag F-1..F-8)
|
||||
- [x] T2-bis lead áp 5 fix rẻ F-1/F-3/F-4/F-6/F-7 (hook + 2 component + HardCopiesPage, mirror cp 4 file) + F-8 sửa 2 câu memory designer (VERIFY→APPEND); F-2 (liên-danh — chờ W5) + F-5 (layout 19rem — nghiệm thu prod) = giới hạn khai thẳng. TSC 0 · build ×2 EXIT 0 · SHA-pair 6/6 sau fix
|
||||
- [x] T3 ship → ship-synthesis.md (cicd verify sau push → sub-cicd-monitor-2.md)
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
# ship-synthesis — cây 4-folder GĐ dưới từng gói thầu (S162, 2026-07-30)
|
||||
|
||||
VERDICT: SHIP — build ×2 EXIT 0 · TSC 0 · SHA-pair 6/6 · reviewer PASS-WITH-FLAGS (0 blocker) · 5/8 flag vá trước ship, 2 giới-hạn khai thẳng, 1 sửa-memory.
|
||||
|
||||
## Lệnh + 2 quyết owner
|
||||
Lệnh anh 30-07 sáng (mid-turn, screenshot khoanh đỏ dải 4 GĐ → cây trái trang Duyệt NCC): "Tạo folder: Duyệt NCC / Kế hoạch Hợp đồng. / Duyệt Hợp đồng / Hợp đồng cứng." AskUser: **(1) folder DƯỚI TỪNG GÓI THẦU · (2) áp cả các trang GĐ khác.** Nhãn folder VERBATIM lời anh (GĐ2 "Kế hoạch Hợp đồng" ≠ strip "Kế hoạch ký kết HĐ" — cố ý, lời-mới-nhất-thắng, 1 lệnh đổi được).
|
||||
|
||||
## Cái ship
|
||||
- 3 file MỚI ×2 app: `components/pipeline/PipelineStageFolders.tsx` (4 folder `<details>`, folder trang hiện tại mở sẵn, locked/failed/loading/truncated/hideEmpty) · `hooks/usePipelineStages.ts` (permission-gate → 2 index query key mới `pipeline-khkk-index`/`pipeline-contract-index` pageSize 200 → `buildStages()` nối GĐ2 qua `khkk.purchaseEvaluationId`, GĐ3 qua `pe.contractId`, GĐ4 = hasSealedCopy) · `components/pipeline/PipelineTreePanel.tsx` (panel cây trái trang GĐ2/GĐ4).
|
||||
- 3 trang sửa ×2 app: PE list (folder dưới WorkItemGroup — khối `<li>` phiếu 0 dòng xoá, reviewer xác nhận từng hunk) · KhkkListPage (lưới 19rem + panel, `currentStage=2`) · HardCopiesPage (`currentStage=4` + F-7 invalidate index).
|
||||
- Toàn bộ THUẦN FE: 0 API mới · 0 migration · 0 đụng menu/seeder (#84) · 0 đụng strip · 0 đụng BE.
|
||||
|
||||
## Chuỗi verdict
|
||||
1. designer `VERDICT: SHIP-READY` — 2 lần dừng-sớm, resume-từ-diary 2/2 OK (artifact-design chống #53 ăn tiền lần nữa; "file rách" giữa chừng = diagnostics snapshot dở, tsc sau đó 0 lỗi — gotcha #68 ×3 trong 1 đợt, cả với lead-edit-batch).
|
||||
2. reviewer `VERDICT: PASS-WITH-FLAGS` — 8 flag, 0 blocker; tự đo lại SHA 6/6 + diff-hunk-level.
|
||||
3. lead: áp F-1 (gate = đúng key policy `KeHoachKyKet` — MenuPermissionHandler so khớp CHÍNH XÁC 1 key, không kế thừa) · F-3 (nhãn "(ngoài chỉ mục 200 — mở để xem)") · F-4 (index giữ `total` → cờ `truncated` + notice — không tụt-số im lặng) · F-6 (hideEmpty giữ folder failed/loading) · F-7 (invalidate `pipeline-contract-index` sau upload) · F-8 (sửa 2 câu memory designer sai: "0 rác 403" thêm điều kiện + "Ct_* kế thừa Contracts" là SAI vì seeder tạo row riêng).
|
||||
4. Gate cuối: `npx tsc -b --force` = 0 · `npm run build` ×2 = EXIT 0 (Rolldown/Vite-8 warm ~0.5s) · SHA-pair 6/6 sau fix (4 hash mới: `403777e3…`/`88967a38…`/`5334a124…`/`e4cb0bbb…`; PE `1d8d8f75…` + Khkk `40a650bf…` giữ).
|
||||
|
||||
## Giới hạn CÒN LẠI (khai thẳng, không phải quên)
|
||||
- **F-2:** phiếu liên-danh đẻ N HĐ chỉ nối HĐ[0] (`CreateContractFromEvaluationFeatures.cs:145` ghi 1 `ContractId`) ⇒ GĐ3/4 hụt N−1 — lớp cardinality S87/S88; đường vá THẬT = W5 (link KHKK→HĐ). Prod hiện Contracts=0 ⇒ 0 tác động tức thời.
|
||||
- **F-5:** lưới `19rem` 2 trang chưa từng render thật (harness chỉ import 2 component; dev-rig authed bị chặn gotcha #3) ⇒ nghiệm thu prod bằng mắt anh — đúng cadence {đợt nhỏ → ship → anh nhìn prod}.
|
||||
- Nhẹ (reviewer, không đánh số): 2 index query fire dù chưa mở folder · view "Đã xóa" vẫn hiện GĐ2-4 sống · KPI đổi dải 1024-1279px.
|
||||
- Dev-data: PE=4 · KHKK=0 · Contracts=7 · PE.contractId=0/4 ⇒ Dev nhìn thấy toàn "SẮP" là ĐÚNG dữ liệu, không phải bug.
|
||||
|
||||
## cicd
|
||||
Post-push: cicd-monitor 3 chân (Actions run + test-gate 590 + bundle-hash-delta ×2 + smoke) → `sub-cicd-monitor-2.md`. FE-only ⇒ không có chân migration.
|
||||
@ -36,3 +36,64 @@ Key FE (`usePipelineStages.ts:41-44`) vs BE:
|
||||
🟡 luận-cứ trong comment `usePipelineStages.ts:10-13` ("gate phải OR nhiều key vì root có thể canRead=false") đúng cho TẦNG HIỂN THỊ nhưng sai đích cho tầng API: query nào cũng nên gate bằng ĐÚNG key policy của endpoint (`KeHoachKyKet` cho `/contract-signing-plans`).
|
||||
🟢 `/contracts` KHÔNG có policy action-level (`ContractsController.cs:16-17`, chỉ `[Authorize]` trần class) ⇒ nhánh `enabled: canContract || canHardCopy` KHÔNG sinh 403. Đổi lại: đây là bare-`[Authorize]` sẵn có của hệ (#82), diff này không làm tệ thêm nhưng có nới hiển-thị: user chỉ được cấp `Hdc_ThauPhu` vẫn thấy leaf GĐ4 của MỌI loại HĐ (`usePipelineStages.ts:211`) vì gate là OR-toàn-nhóm chứ không theo `c.type`.
|
||||
🟢 Không quyền ⇒ `enabled:false` (`:77`, `:91`, `PipelineTreePanel.tsx:108`) ⇒ KHÔNG gọi API; folder khoá render nhánh riêng KHÔNG `<details>` (`PipelineStageFolders.tsx:99-113`). Fetch lỗi ⇒ `failed` ⇒ badge "—" + body "Không tải được…" (`:140-156`) ⇒ cây không vỡ. ĐẠT.
|
||||
|
||||
## MỤC 2 — Semantics filter / `hideEmpty` — ĐẠT phần cốt lõi, 3 gợn nhẹ
|
||||
|
||||
- Nguồn cờ: PE page `filterActive = search.trim()!=='' || phase!=='' || pendingMe || deletedView` (`PurchaseEvaluationsListPage.tsx:58`); panel `hideEmpty = term.length > 0` (`PipelineTreePanel.tsx:138`). Không lọc ⇒ `buildStages` luôn trả ĐỦ 4 (`usePipelineStages.ts:240`) ⇒ "không filter = đủ 4 folder" ĐÚNG.
|
||||
- **Ca "GĐ1 rỗng chiếm chỗ" KHÔNG tồn tại:** `wg.items` là nguồn dựng nhóm nên mọi gói thầu có ≥1 phiếu ⇒ `stage1.count = wg.items.length ≥ 1` (`usePipelineStages.ts:153`). Thêm chốt chặn `if (visible.length===0) return null` (`PipelineStageFolders.tsx:220`) ⇒ không có wrapper rỗng, không vỡ layout.
|
||||
- 🟡 gợn 1: bộ lọc `hideEmpty` chấm bằng `stageCount(s) > 0` (`:219`) nên nó nuốt LUÔN folder `locked` và folder `failed` (cả hai đều count=0). Đang lọc mà GĐ2 lỗi tải ⇒ dòng "—" BIẾN MẤT thay vì báo lỗi (spec dòng 12 muốn "—"). Cũng nuốt folder đang `loading` ⇒ folder nhảy vào sau khi query xong (giật layout nhẹ).
|
||||
- 🟡 gợn 2: count GĐ2/3/4 KHÔNG chịu bộ lọc của trang (index nạp riêng, không truyền `search`/`phase`) ⇒ "chỉ hiện folder có nội dung khớp" chỉ đúng theo nghĩa YẾU (folder khác rỗng), không phải "nội dung khớp từ khoá".
|
||||
- 🟡 gợn 3: ở view "Đã xóa" (`deletedView=1`) cây vẫn treo GĐ2/3/4 SỐNG dưới phiếu đã xoá (index không lọc theo phiếu xoá) ⇒ bấm được sang KHKK/HĐ còn sống từ màn thùng rác.
|
||||
|
||||
## MỤC 4 — Query hygiene + degrade `pageSize:200`
|
||||
|
||||
**Không đụng cache trang cũ — ĐẠT.** Key mới `['pipeline-khkk-index']` / `['pipeline-contract-index']` (`usePipelineStages.ts:70,84`) + `['pipeline-pe-index']` (`PipelineTreePanel.tsx:101`). Key đang sống: `['khkk-list',{…}]` (`KhkkListPage.tsx:70`) · `['hard-copies',{…}]` + `['contract',id]` (`HardCopiesPage.tsx:75,268`) · `['pe-list',{…}]` + `['pe-detail',id]` (`PurchaseEvaluationsListPage.tsx:75,114`) · dashboard `['pipeline-pe-total']`/`['pipeline-contract-total']` (`UserDashboardPage.tsx:906,913`). Tất cả là mảng 1 phần tử CHUỖI KHÁC NHAU ⇒ 0 giao, 0 prefix-match. `invalidateQueries()` trống (nuốt tất) = 0 chỗ trong fe-user.
|
||||
🟡 mặt trái: `qc.invalidateQueries({queryKey:['hard-copies']})` sau khi lưu bản cứng (`HardCopiesPage.tsx:290`) KHÔNG chạm `pipeline-contract-index` ⇒ badge GĐ4 trên cây giữ số cũ tới lần mount/refocus kế (staleTime 60s, `usePipelineStages.ts:34`).
|
||||
|
||||
**Degrade khi vượt 200 — graceful CÓ, im lặng CŨNG CÓ.**
|
||||
- Kẹp là THẬT: `PagedResult.cs:20` `PageSize = value switch { <1 => 20, >200 => 200, _ => value }`.
|
||||
- Thứ tự lấy: 3 controller đều mặc định `sortDesc = true` (`ContractsController.cs:19` · `ContractSigningPlansController.cs:45` · `PurchaseEvaluationsController.cs:21`) ⇒ 200 bản **MỚI NHẤT**, không phải 200 bản cũ nhất. Tốt.
|
||||
- 🔴 nhưng hook VỨT `total`: `.data.items` (`usePipelineStages.ts:76,90` · `PipelineTreePanel.tsx:107`) ⇒ mã KHÔNG BIẾT mình bị cắt ⇒ không đâu có dấu "còn N nữa". GĐ2 mất leaf + tụt count; GĐ4 tụt count; header panel "{packageCount} gói" (`PipelineTreePanel.tsx:188`) cũng tụt. Đây là **sai-số-im-lặng**, không phải chỉ degrade.
|
||||
- 🔴 GĐ3 tệ hơn 1 nấc: HĐ ngoài index vẫn ra leaf nhưng **DÁN NHÃN SAI** `primary: c?.maHopDong ?? '(chưa cấp mã)'` (`usePipelineStages.ts:204`) — HĐ có mã thật vẫn bị ghi "(chưa cấp mã)". Trộn 2 nguyên nhân khác hẳn nhau (chưa cấp mã ⟂ ngoài cửa sổ 200) vào một câu khẳng định sai.
|
||||
- 🔴 độc lập với 200: `pe.ContractId = contracts[0].Id` (`CreateContractFromEvaluationFeatures.cs:145`, comment `:143` ghi rõ "N HĐ liên-danh truy qua changelog") ⇒ phiếu multi-winner đẻ N hợp đồng nhưng cây chỉ nối ĐÚNG 1 (`usePipelineStages.ts:197`) ⇒ GĐ3/GĐ4 hụt N−1 và badge in "1". Đúng lớp bài `feedback_cardinality_change_grep_consumers` (S87/S88): consumer mới đọc field single trong miền multi.
|
||||
- Cỡ dữ liệu đo được: Dev `PE=4 · KHKK=0 · Contracts=7 · PE có ContractId=0/4` ⇒ chưa chạm trần 200, nhưng cũng có nghĩa **GĐ3/GĐ4 trên Dev sẽ rỗng ("SẮP") toàn bộ**; prod không đo được từ máy này.
|
||||
|
||||
## MỤC 5 — Lưới 19rem 2 trang (KhkkListPage + HardCopiesPage)
|
||||
|
||||
- **PageHeader còn full-width — ĐÚNG.** KHKK: header đóng `/>` ở `:133`, lưới mở ở `:138` (ngoài lưới). HardCopies: PageHeader + ghi chú 1-MỐC amber đứng trước, lưới mở sau (`HardCopiesPage.tsx` hunk `@@ -110,6 +111,12`).
|
||||
- **`min-w-0` có đủ:** cột phải `<div className="min-w-0 space-y-5">` ở cả 2 trang; track lưới dùng `minmax(0,1fr)` ⇒ bảng không thổi vỡ cột. ĐẠT.
|
||||
- **Mobile:** `<lg` lưới về 1 cột, panel nằm TRÊN; thân panel `mobileOpen ? 'flex' : 'hidden'` + `lg:flex` (`PipelineTreePanel.tsx:200`) với `mobileOpen` mặc định `false` (`:95`) ⇒ mobile chỉ thêm 1 thanh tiêu đề + nút "Hiện cây", bảng không bị đẩy xuống sâu. ĐẠT.
|
||||
- 🟡 `overflow-x-auto` bọc `<table>` gần như VÔ TÁC DỤNG như lời chú thích: `<table className="w-full">` + `whitespace-nowrap` = **0 lần** ở cả 2 file ⇒ bảng co cột/xuống dòng chứ không sinh cuộn ngang. Vô hại, nhưng chú thích `KhkkListPage.tsx:175` ("bảng 7 cột cuộn ngang trong thẻ") mô tả một hành vi không xảy ra; thứ thật sự chặn tràn là `minmax(0,1fr)`.
|
||||
- 🟡 đổi hành vi có thật (nhỏ): hàng KpiCard `lg:grid-cols-6` → `xl:grid-cols-6` (`KhkkListPage.tsx:137`) ⇒ ở dải 1024–1279px KPI từ 6 cột rớt về 3 cột. Hợp lý vì cột trái ăn 19rem, nhưng đây là thay đổi hiển thị ngoài phạm vi "chỉ thêm cây".
|
||||
- 🟡 `HardCopyPanel` (`HardCopiesPage.tsx:220`, định nghĩa `:259`) nằm TRONG `<tbody>` nên nay nằm trong lớp `overflow-x-auto` mới. Không mất chức năng (upload/tải/ý kiến vẫn nguyên, 0 dòng `-` trong diff), nhưng nó bị bó vào bề rộng cột phải hẹp hơn trước.
|
||||
- 🔴 **Bằng chứng thị giác cho 2 trang này = KHÔNG CÓ.** Harness chỉ import `PipelineStageFolders` + `PipelineTreePanel` (`harness-s162.tsx.txt:10-11`), tự "sao markup" vỏ cây (`:61`), và `grep 'grid-cols-\[19rem'` trong harness = **0 hit** ⇒ chưa bao giờ render KhkkListPage/HardCopiesPage/lưới 19rem. Vậy dòng rubric FD4 "responsive ✓ (1440/1366/375 … bảng không bị đẩy)" trong `sub-frontend-designer-0.md:94` là **nhãn ĐO mà phép đo không tồn tại** — phải hoặc đo lại, hoặc bỏ dấu ✓ cho trục đó.
|
||||
- (sạch) Harness đã xoá khỏi `src` 2 app — `ls fe-*/src/harness*` = NONE.
|
||||
|
||||
## MỤC 7 — Self-write memory frontend-designer
|
||||
|
||||
ĐÚNG SỰ THẬT (tự đo lại, không tin lời khai):
|
||||
- `PagedResult.cs:20` kẹp ≤200 — ĐÚNG nguyên văn.
|
||||
- `GetMyMenuTreeQuery.cs:96` = `HasAccess(n) => n.CanRead || n.Children.Any(HasAccess)` — ĐÚNG số dòng.
|
||||
- "Sau nén 14.6KB" — `wc -c MEMORY.md` = **14.652 B**. ĐÚNG.
|
||||
- Archive **moved-not-cut** (bài S102): 3 mục chuyển đi đều CÓ MẶT trong `archive/2026-06.md` (`WidgetCard` 3 hit · `OfficeDashboardPage` 3 hit · `Maximize2` 2 hit) + 3 dòng trỏ `substring` còn ở L1. KHÔNG phải cut-giả-vờ-archive.
|
||||
- "MIRROR đủ 2 app / build PASS ×2 / `<li>` giữ nguyên từng byte" — đã tự chứng ở MỤC 1 + MỤC 6.
|
||||
2 chỗ SAI/NỬA-ĐÚNG sẽ đóng băng vào trí nhớ vai nếu commit nguyên:
|
||||
- 🔴 "`enabled:false` ⇒ **0 rác 403**" viết như tính chất vô điều kiện — sai cho đường KHKK (xem F-1): gate OR `Khkk_List` trong khi policy API là `KeHoachKyKet.Read`.
|
||||
- 🟡 "`Ct_*` **kế thừa** `Contracts`" — chỉ đúng khi menu con KHÔNG có row quyền (`GetMyMenuTreeQuery.cs:66` `!resolved.ContainsKey(m.Key)`), mà seeder LẠI tạo row cho `Ct_*` (`DbInitializer.cs:2168-2176`) ⇒ trên DB đã seed, kế thừa gần như không bao giờ chạy. Câu này sẽ dẫn sai phiên sau.
|
||||
- 🟡 nhật ký `sub-frontend-designer-0.md:82` "mount CHÍNH component thật (không vẽ lại markup)" — đúng với 2 component mới, nhưng vỏ cây thì có vẽ lại ("sao markup", `harness-s162.tsx.txt:61`).
|
||||
|
||||
---
|
||||
|
||||
# VERDICT: PASS-WITH-FLAGS
|
||||
|
||||
Hợp đồng spec giữ đúng ở mọi ranh giới kiểm được (nhãn verbatim 4/4 · mirror SHA 6/6 · khối `<li>` phiếu 0 dòng xoá · 0 chạm BE/menu/seeder/strip/tests · named-export) và các đường hỏng đều có nhánh đỡ (khoá/lỗi/rỗng). Không có blocker. 8 flag: nặng nhất là **im lặng về độ chính xác của con số** (cắt 200 không dấu vết, `contracts[0]` nuốt HĐ liên-danh, nhãn "(chưa cấp mã)" sai nguyên nhân) và **1 nhãn ✓ responsive chưa từng được đo**.
|
||||
|
||||
- **F-1** `usePipelineStages.ts:42` — gate OR `['Khkk_List','Khkk_G1','KeHoachKyKet']` trong khi API là `[Authorize(Policy="KeHoachKyKet.Read")]` (`ContractSigningPlansController.cs:36`) và `MenuPermissionHandler.cs:40` so khớp CHÍNH XÁC 1 key ⇒ role có `Khkk_List=1`/`KeHoachKyKet=0` sẽ 403 và GĐ2 hiện "—" khắp cây. Dev 13/13 role hiện đều =1 nên chưa lộ.
|
||||
- **F-2** `usePipelineStages.ts:197` + `CreateContractFromEvaluationFeatures.cs:145` — phiếu liên-danh N HĐ chỉ nối 1 (`pe.ContractId` = HĐ đầu) ⇒ GĐ3/GĐ4 hụt N−1 mà badge vẫn in số như thật.
|
||||
- **F-3** `usePipelineStages.ts:204` — HĐ ngoài cửa sổ 200 bị dán nhãn "(chưa cấp mã)"; trộn "chưa có mã" với "không nằm trong index".
|
||||
- **F-4** `usePipelineStages.ts:76,90` + `PipelineTreePanel.tsx:107` — vứt `total` ⇒ không phát hiện được việc bị cắt 200 ⇒ mọi count/`{n} gói` tụt im lặng, không dấu "còn N nữa".
|
||||
- **F-5** `sub-frontend-designer-0.md:94` — rubric "responsive ✓ … bảng không bị đẩy" không có phép đo: harness chỉ mount 2 component (`harness-s162.tsx.txt:10-11`), `grid-cols-[19rem` = 0 hit ⇒ 2 trang lưới chưa từng render.
|
||||
- **F-6** `PipelineStageFolders.tsx:219` — `hideEmpty` lọc theo count>0 nên nuốt cả folder `locked`/`failed`/`loading`; đang lọc thì dấu "—" báo lỗi biến mất.
|
||||
- **F-7** `HardCopiesPage.tsx:290` — invalidate `['hard-copies']` không chạm `['pipeline-contract-index']` ⇒ badge GĐ4 lệch tới lần mount/refocus kế.
|
||||
- **F-8** memory vai: "0 rác 403" (vô điều kiện — sai theo F-1) + "`Ct_*` kế thừa `Contracts`" (chỉ đúng khi con không có row; seeder có tạo row `Ct_*`) — nên sửa trước khi commit vì đây là trí nhớ dài hạn.
|
||||
- (nhẹ, không đánh số) `overflow-x-auto` vô tác dụng vì `table w-full` + 0 `whitespace-nowrap`; KPI `lg:`→`xl:` đổi hiển thị dải 1024–1279; view "Đã xóa" vẫn treo GĐ2/3/4 sống; 2 query index chạy sớm ngay cả khi người dùng không mở folder nào.
|
||||
|
||||
Reference in New Issue
Block a user