32 KiB
sub-frontend-designer-L4 — LĂNG-KÍNH: CÂY TOÀN TRÌNH (Pipeline Tree)
Run:
2026-08-05-S174-khkk-style-audit· Lane L4 (ensemble/fable-clone frontend-designer, 5 lane cùng vai). Scope 3 file:fe-admin/src/components/pipeline/PipelineTreePanel.tsx·fe-admin/src/components/pipeline/PipelineStageFolders.tsx·fe-admin/src/hooks/usePipelineStages.ts. Owner-item BẮT BUỘC: (O2) badge chữ "SẮP" → "Bỏ hết đi đổi thành 0" · (O6) card phiếu PE trong cây văng sang/purchase-evaluations/...→ giữ tại-chỗ chỉ-xem. Ghi APPEND-as-I-read (không gom cuối). Frame lượt 1.
0. Khung rỗng (ghi TRƯỚC khi đọc) — checklist tự-ràng-buộc
- A. Inventory 3 file: LOC, export, prop-surface, ai gọi ai.
- B. Host-map: liệt ĐỦ 6 host của
PipelineTreePanel(file:line + prop truyền). - C. (O2) Truy chữ "SẮP": sinh ở file:line nào, điều kiện bật, GĐ nào đúng-nghĩa / GĐ nào stale.
- D. (O2) Đề xuất đổi sang "0": đụng gì (state/type/style), host nào đổi hành-vi.
- E. (O6) Truy
navigate(+onOpenKhkk+ mọi điều-hướng trong 3 file → đường đang đi. - F. (O6) Đề xuất callback-hoá mirror
stage2Options.onOpenKhkk, ×6 host. - G. Style/UX delta KHKK (GĐ2) vs Duyệt NCC (GĐ1 golive = CHUẨN) trong lăng-kính cây.
- H. Spec thi-hành-được (diff-level) cho Opus 5 MAX.
- I. Rủi-ro / cái KHÔNG được đụng.
Số đo nền (chạy 2026-08-05, trước mọi patch)
| Phép đo | Kết quả |
|---|---|
sha256sum 3 cặp file mirror |
3/3 KHỚP — PipelineTreePanel 3f5ffc6b…27cba2 · PipelineStageFolders bed52f39…6de936 · usePipelineStages 55770e39…2f6f4 ⇒ luật MIRROR ×2 app đang được giữ, patch PHẢI giữ tiếp |
grep -rn "SoonChip" (2 app) |
8 hit (4/app: :99 def + :195/:233/:273 gọi) |
grep -c "navigate(\|navigate('" usePipelineStages.ts` |
7 dòng = 6 lời gọi + 1 comment (:182) — cả 2 app y hệt |
grep -c navigate PipelineStageFolders.tsx |
0 (kể cả comment) |
grep -c navigate PipelineTreePanel.tsx |
1 — và là COMMENT :97, 0 lời gọi |
A. Inventory 3 file ✅
| File (fe-admin) | LOC | Vai | Router? |
|---|---|---|---|
components/pipeline/PipelineTreePanel.tsx |
352 | Panel cây trái: query pipeline-pe-index (200 phiếu) → buildTree Năm>Dự án>Hạng mục → mỗi hạng mục render <PipelineStageFolders> (:330-335) |
KHÔNG (0 useNavigate) |
components/pipeline/PipelineStageFolders.tsx |
334 | THUẦN TRÌNH BÀY: vẽ 4 folder GĐ + ngăn nhóm + leaf. Không fetch, không route | KHÔNG |
hooks/usePipelineStages.ts |
404 | Nguồn dữ liệu + TOÀN BỘ điều-hướng (useNavigate khai :200) |
CÓ — nhà duy nhất của navigate( |
- fe-user MIRROR: LOC 352/334/404 y hệt; 3 header đều khai "File MIRROR SHA256 identical giữa 2 app — sửa 1 bên PHẢI copy sang bên kia" (
PipelineStageFolders.tsx:10,usePipelineStages.ts:3,PipelineTreePanel.tsx:6) ⇒ mọi patch = ×2 app, nghiệm thu bằng SHA256 từng cặp. - Kiến trúc đã sẵn sàng cho O6:
navigateKHÔNG rải trong 2 file trình-bày ⇒ callback-hoá chỉ đụng 1 file logic + prop-surface.
B. Host-map — ĐỦ 6 host của PipelineTreePanel ✅
| # | Host (file:line render) | currentStage |
Prop đang truyền | Ghi chú |
|---|---|---|---|---|
| H1 | fe-admin/src/components/khkk/KhkkListPanel.tsx:292 |
2 | stage2Options (content+count+onOpenKhkk), hideHeader, bodyMaxHClass |
Màn KHKK 3-panel — host của cả O2 lẫn O6 |
| H2 | fe-user/src/components/khkk/KhkkListPanel.tsx:292 |
2 | (mirror H1) | mirror |
| H3 | fe-admin/src/pages/contracts/ContractsListPage.tsx:249 |
3 | bodyMaxHClass="max-h-[28rem] lg:max-h-none", render CÓ ĐIỀU KIỆN {!selectedId && …} |
HĐ (admin) |
| H4 | fe-user/src/pages/contracts/MyContractsPage.tsx:225 |
3 | như H3 | HĐ (user) |
| H5 | fe-admin/src/pages/contracts/HardCopiesPage.tsx:117 |
4 | chỉ currentStage |
Bản cứng (admin) |
| H6 | fe-user/src/pages/contracts/HardCopiesPage.tsx:117 |
4 | như H5 | Bản cứng (user) |
+2 call-site KHÔNG qua Panel (đếm cho O2 vì badge sống trong PipelineStageFolders):
- H7
fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx:524— dùng THẲNG<PipelineStageFolders currentStage={1} … stages={buildStages(wg.items, { stage1Content: … })}>(cây riêng trang Duyệt NCC;PipelineTreePanel.tsx:8-10khai rõ KHÔNG đụng cây đó). - H8
fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:524— mirror.
Host đã GỠ (đừng đếm nhầm): fe-user/src/pages/InboxPage.tsx:341 — [S174 owner] GỠ PipelineTreePanel khỏi Hộp thư. Không còn render.
⇒ O2 chạm 8 điểm render (H1-H8); O6 chạm 8 điểm nhưng chỉ H1/H2 (+H7/H8 nếu nhận nhánh đối-xứng) ĐỔI HÀNH VI — chi tiết §D/§F.
C. (O2) Truy chữ "SẮP" — sinh ở ĐÂU, bật khi NÀO ✅
Nguồn DUY NHẤT: fe-admin/src/components/pipeline/PipelineStageFolders.tsx:99-107 — hàm SoonChip():
<span className="shrink-0 rounded bg-slate-100 px-1 text-[9px] font-semibold uppercase tracking-wide text-slate-400">
Sắp
</span>
Chuỗi trong code là Sắp; CSS uppercase biến nó thành "SẮP" trên màn (comment :100-101 khai đúng vậy — token bê verbatim từ components/pe/PePipelineStrip.tsx:50-52).
3 chỗ gọi — 3 NGỮ NGHĨA KHÁC NHAU bị gộp vào cùng 1 chip (gốc của lỗi owner thấy) — vị trí ĐO bằng grep, không ước:
| # | Call-site | Điều kiện bật | Nghĩa THẬT | "SẮP" đúng không? |
|---|---|---|---|---|
| S-a | :273 (StageFolder summary, nhánh cuối của ternary :263-274) |
!stage.failed && count === 0; count = stageCount() (:95-97) |
Folder GĐ này rỗng CHO GÓI THẦU NÀY | ❌ SAI — module đã live, chỉ là gói chưa có bản ghi |
| S-b | :195 (GroupFolder summary) |
group.leaves.length === 0 |
Ngăn nhóm duyệt N1-N8 rỗng | ❌ SAI — thân ngăn ngay dưới (:200) đã ghi "Chưa có phiếu ở nhóm này." ⇒ chip nói ngược nội dung |
| S-c | :233 (nhánh stage.locked) |
stage.locked = !linkage.canX (usePipelineStages.ts:298/386/394) |
Không có QUYỀN xem GĐ này (và KHÔNG fetch) | ❌ SAI NẶNG NHẤT — "sắp có" ≠ "anh không được xem"; icon Lock :228 + title :226 đã nói đúng rồi |
GĐ nào còn đúng nghĩa "sắp" (chưa build)? → KHÔNG CÒN GĐ NÀO.
- GĐ1 Duyệt NCC: live (PE feature-complete).
- GĐ2 Kế hoạch Hợp đồng: live từ Mig 69
AddContractSigningPlans(S161) —usePipelineStages.ts:96gọi thật/contract-signing-plans. - GĐ3 Duyệt Hợp đồng: live —
:112gọi/contracts. - GĐ4 Hợp đồng cứng: live —
:369đọcc.hasSealedCopy,:378mở/hard-copies?type=. ⇒ cả 4 folder đều fetch API THẬT. Chip "Sắp" là di-tích ngữ-nghĩa bê từPePipelineStrip(thời dải strip còn GĐ chưa dựng) — nay 100% ca hiển thị của nó đều là thông tin sai. Lệnh owner "Bỏ hết đi đổi thành 0" là sửa đúng nghiệp vụ, không phải chuyện thẩm mỹ.
Có host nào ĐANG DỰA vào chữ "SẮP"? → KHÔNG (đo ở D5).
D. (O2) Đề xuất "0" — đổi gì, đụng gì ✅
D1. Patch (áp cho PipelineStageFolders.tsx, MIRROR ×2 app)
(1) XOÁ SoonChip (:99-107) — thay bằng 2 chip có ngữ-nghĩa tách bạch:
/** Số phiếu thật. `tabular-nums`: 4 folder xếp chồng, 1→2 chữ số không được giật cột. */
function CountChip({ n, style, muted }: { n: number; style: StageStyle; muted?: boolean }) {
return (
<span className={cn('shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium tabular-nums',
muted ? 'bg-slate-100 text-slate-600' : style.count)}>{n}</span>
)
}
/** Số KHÔNG XÁC ĐỊNH (lỗi tải / không có quyền) — KHÔNG được hiển thị là 0. */
function UnknownChip({ title }: { title: string }) {
return <span title={title} className="shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600">—</span>
}
(2) 3 call-site đổi như sau:
| Call-site | Trước | Sau | Lý do |
|---|---|---|---|
:270-274 StageFolder |
count>0 ? chip : <SoonChip/> |
<CountChip n={count} style={style} muted={count===0}/> |
ĐÚNG lệnh owner: rỗng ⇒ 0 |
:192-196 GroupFolder |
count>0 ? chip : <SoonChip/> |
<CountChip n={count} style={style} muted={count===0}/> |
ngăn N1-N8 rỗng ⇒ 0 (thân ngăn :200 đã ghi "Chưa có phiếu ở nhóm này." ⇒ hết mâu thuẫn) |
:233 nhánh locked |
<SoonChip/> |
<UnknownChip title="Bạn chưa có quyền xem giai đoạn này"/> |
🔴 CỐ Ý KHÔNG theo chữ "0" — xem D2 |
:263-269 nhánh failed |
chip "—" text-slate-400 |
<UnknownChip title="Không tải được dữ liệu giai đoạn này"/> |
gộp về 1 chip + vá contrast (D3) |
D2. 🔴 ĐIỂM PHẢI HỎI OWNER 1 DÒNG (đừng tự quyết im lặng)
Ca locked (:222-236) không fetch (usePipelineStages.ts:102/118 enabled: theo quyền) ⇒ leaves rỗng vì không được xem, KHÔNG phải vì không có phiếu. Ghi "0" ở đây = nói dối dữ liệu: người dùng đọc "gói này chưa có Hợp đồng" trong khi thật ra có mà họ không được thấy. Đề xuất giữ "—" (đúng một nghĩa với failed = số không xác định), icon Lock + title đã sẵn ở :226-228.
→ Câu hỏi cho owner: "'Bỏ hết SẮP đổi thành 0' — riêng folder BỊ KHOÁ QUYỀN em để dấu '—' (không biết số) thay vì 0, được không anh?". Nếu owner vẫn muốn 0 tuyệt đối ⇒ đổi 1 dòng, nhưng phải khai đây là con số không có thật.
D3. Quality ride-along ĐO ĐƯỢC (FD5 contrast floor)
| Chip | Màu | Tỉ lệ tương phản (tính tay sRGB/WCAG 2.1) | Verdict |
|---|---|---|---|
| "SẮP" hiện tại | text-slate-400 #94A3B8 / bg-slate-100 #F1F5F9 |
2.38:1 | ❌ FAIL AA (sàn 4.5:1 cho text nhỏ) |
"—" failed hiện tại (:265) |
slate-400 / slate-100 | 2.38:1 | ❌ FAIL AA |
Đề xuất 0 rỗng |
text-slate-600 #475569 / slate-100 |
6.91:1 | ✅ PASS AA (dư cả AAA text thường) |
(loại) text-slate-500 #64748B / slate-100 |
— | 4.34:1 | ❌ vẫn thiếu 0.16 — đừng dùng slate-500 |
| ⇒ Đổi "SẮP"→"0" là dịp hợp lệ để vá luôn 2 chip FAIL AA đang có, KHÔNG phải scope-creep: cùng đúng 1 vùng code, cùng 1 lần soi. |
D4. Đụng gì thêm — comment/lý-lẽ trỏ vào thứ sắp biến mất (6 chỗ, PHẢI sửa cùng lượt)
PipelineStageFolders.tsx:77-79 · :82 · :219 · :259 · usePipelineStages.ts:8 · :254-256.
⚠️ Riêng :77-79 + usePipelineStages.ts:254-256 dùng chip "Sắp" làm tiền lệ biện minh cho quyết định "hiện đủ 8 ngăn kể cả rỗng". Giữ quyết định, đổi lý-lẽ: khung 8 nhóm hiện đủ vì người dùng cần thấy chỗ nào trống để bấm vào lập phiếu — không liên quan chữ trên chip.
D5. Có host nào dựa vào chữ "SẮP"? → KHÔNG (đo, không đoán)
SoonChipkhôngexport(:99) ⇒ 0 import ngoài.grep -rn "SoonChip" fe-admin/src fe-user/src⇒ chỉ 2 file mirror này.- Không prop/host nào điều khiển nó; nó suy nội bộ từ
count/locked. - Không route/test/e2e nào bắt chuỗi.
- ⚠️ False-positive KHÔNG ĐƯỢC ĐỤNG:
fe-admin/src/pages/DashboardPage.tsx:113"Sắp quá hạn (24h)"— nghĩa hoàn toàn khác (deadline), không thuộc lớp lỗi này.
D6. 🔴 SIBLING-INSTANCE cùng lớp lỗi — NGOÀI 3-file lens, escalate em main
fe-admin|fe-user/src/components/pe/PePipelineStrip.tsx (dải strip trên đầu detail phiếu PE):
:13-18STAGEShardcodesoon: truecho GĐ2 "Kế hoạch ký kết HĐ" và GĐ4 "Hợp đồng cứng";:37title='Sắp triển khai';:49-52chipSắp(cùng token slate-400/slate-100 ⇒ cũng 2.38:1);:7-9comment khẳng định "GĐ2/GĐ4 chưa có module" + "mọi stage không-active đều KHÔNG click được — hiển thị lộ trình, không giả link". Nay SAI cả 3: GĐ2 live từ Mig 69 (S161), GĐ4 live (hasSealedCopy+/hard-copies), và cầu PE→KHKK→HĐ đã nối (usePipelineStages.ts:322-328). Owner nói "bỏ HẾT" — nếu chỉ vá cây mà để strip nguyên, người dùng mở phiếu PE vẫn thấy "SẮP" ⇒ vá nửa vời trông y hệt chưa vá. Đề nghị em main giao lane khác (chủ filecomponents/pe/) xử cùng đợt.
D7. Ảnh hưởng lên 6 host (+2 call-site trực tiếp) — O2
| Host | Đổi hành vi? | Cụ thể |
|---|---|---|
H1 KHKK admin KhkkListPanel.tsx:292 |
CÓ (thị giác) | GĐ2 dùng content ⇒ không có ngăn; folder GĐ1/3/4 rỗng: SẮP→0. Không đổi luồng bấm |
| H2 KHKK user | CÓ (thị giác) | như H1 |
H3 HĐ admin ContractsListPage.tsx:249 |
CÓ (thị giác) | GĐ2 hiện 8 ngăn ⇒ ngăn rỗng SẮP→0; folder rỗng SẮP→0 |
H4 HĐ user MyContractsPage.tsx:225 |
CÓ (thị giác) | như H3 |
H5 Bản cứng admin HardCopiesPage.tsx:117 |
CÓ (thị giác) | như H3 |
H6 Bản cứng user HardCopiesPage.tsx:117 |
CÓ (thị giác) | như H3 |
H7/H8 trang Duyệt NCC …ListPage.tsx:524 |
CÓ (thị giác) | GĐ1 dùng stage1Content (không ảnh hưởng), GĐ2-4 rỗng: SẮP→0 |
| 0 host đổi API/prop/luồng — patch nằm trọn trong component trình-bày, prop-surface KHÔNG đổi ⇒ 0 rủi ro hồi-quy hành vi. Chỉ pixel đổi. |
E. (O6) Truy điều-hướng — ĐÚNG đường đang đi ✅
useNavigate chỉ khai ở usePipelineStages.ts:200. PipelineStageFolders.tsx = 0 hit chuỗi navigate (kể cả comment); PipelineTreePanel.tsx = 1 hit DUY NHẤT và là COMMENT (:97 "…8 ngăn nhóm + navigate…"), 0 lời gọi. Toàn bộ 6 lời gọi thật nằm ở hook:
| # | file:line | Leaf/nút | Đích hiện tại | Callback-hoá? |
|---|---|---|---|---|
| N1 | usePipelineStages.ts:219 |
CARD PHIẾU PE (GĐ1) | navigate(\/purchase-evaluations?id=${p.id}&expand=1`)` |
❌ CHƯA — ĐÚNG lỗi O6 owner báo |
| N2 | :233 |
Card/leaf KHKK (GĐ2) | opts.onOpenKhkk ? onOpenKhkk(k.id) : navigate(\/khkk/${k.id}`)` |
✅ ĐÃ (M-2 S171) — khuôn mẫu để nhân bản |
| N3 | :263 |
Tên ngăn nhóm N1-N8 | navigate(\/khkk/list?group=${n}`)` |
❌ (rời trang CÓ CHỦ ĐÍCH: "mở danh sách đã lọc") |
| N4 | :272 |
Tên ngăn "(chưa phân nhóm)" | navigate('/khkk/list') |
❌ (như N3) |
| N5 | :366 |
Leaf HĐ (GĐ3) | navigate(\/contracts/${contractId}`)` |
❌ |
| N6 | :378 |
Leaf bản cứng (GĐ4) | navigate(\/hard-copies?type=${c.type}`)` |
❌ |
Chuỗi sự-kiện lỗi owner gặp (H1/H2, màn KHKK): KhkkListPanel KHÔNG truyền stage1Content ⇒ usePipelineStages.ts:204-206 rơi xuống nhánh else :207-222 ⇒ mỗi phiếu PE thành leaf onOpen = N1 ⇒ PipelineStageFolders.tsx:119 onClick={leaf.onOpen} ⇒ rời màn KHKK sang /purchase-evaluations?id=…&expand=1. Khớp nguyên văn owner.
⚠️ Đối xứng bị bỏ quên — owner nói "tương tự với duyệt NCC": ở H7/H8 (trang Duyệt NCC) GĐ1 dùng stage1Content nên PE ở-tại-chỗ ✔, nhưng leaf GĐ2 chạy N2 nhánh-mặc-định ⇒ navigate('/khkk/:id') ⇒ VĂNG khỏi màn Duyệt NCC. Lỗi TỒN TẠI ĐỐI XỨNG 2 chiều: KHKK→PE (N1) và PE→KHKK (N2 khi host không truyền callback).
F. (O6) Đề xuất callback-hoá — mirror ĐÚNG khuôn onOpenKhkk (M-2) ✅
F1. Patch tối thiểu, 3 file + 2 host
(a) usePipelineStages.ts — thêm 1 option, sửa 1 dòng:
// trong BuildStagesOptions (cạnh onOpenKhkk :185)
/** [O6] Bấm leaf phiếu PE (GĐ1) ⇒ HOST xử lý tại chỗ thay vì rời trang.
* KHÔNG truyền ⇒ giữ NGUYÊN navigate — 0 hồi-quy cho mọi call-site cũ. */
onOpenPe?: (id: string) => void
// :219 — ĐỔI ĐÚNG 1 dòng, sao y khuôn N2 (:233)
onOpen: opts.onOpenPe ? () => opts.onOpenPe!(p.id) : () => navigate(`/purchase-evaluations?id=${p.id}&expand=1`),
(b) PipelineStageFolders.tsx — thêm trạng-thái ĐANG CHỌN cho leaf (bắt buộc, xem F3):
// PipelineLeaf (:46-56)
selected?: boolean
// LeafList (:117-125): thêm aria-current + nền active
aria-current={leaf.selected ? 'true' : undefined}
className={cn('block w-full px-3 py-2 …', leaf.selected && 'bg-brand-50 ring-1 ring-inset ring-brand-200', …)}
(c) PipelineTreePanel.tsx — prop mới, KHÔNG nhét vào stage2Options:
// cạnh :98
/** [O6] Tuỳ chọn GĐ1. Tách khỏi `stage2Options` để tên prop không nói dối. */
stage1Options?: Pick<BuildStagesOptions, 'onOpenPe'> & { selectedPeId?: string | null }
// :334
stages={buildStages(wg.items, { ...stage2Options, ...stage1Options })}
🔴 KHÔNG nhồi onOpenPe vào stage2Options (dễ nhưng sai tên ⇒ người sau đọc stage2Options.onOpenPe sẽ tưởng là callback GĐ2).
(d) Host H1/H2 KhkkListPanel.tsx:292-297:
<PipelineTreePanel
currentStage={2}
hideHeader
bodyMaxHClass="max-h-[28rem] lg:max-h-none"
stage2Options={{ stage2Content, stage2Count, onOpenKhkk: onSelect }}
stage1Options={{ onOpenPe: onSelectPe, selectedPeId }} // ← MỚI
/>
onSelectPe + selectedPeId là prop mới của KhkkListPanel, do KhkkListPage cấp.
(e) KhkkListPage.tsx — nơi phiếu PE hiện TẠI CHỖ (chỉ-xem):
- state qua URL cho deep-link:
?pe=<id>(song song?id=của KHKK). Chọn PE ⇒ clearid, chọn KHKK ⇒ clearpe(panel 2 chỉ một nội dung). <lgPHẢI giữ nhánh rời trang, y hệtselectPlan(:102-109), nếu không mobile bấm PE = không có gì xảy ra:
function selectPe(id: string) {
if (window.matchMedia('(min-width: 1024px)').matches) { setPe(id); setId(null) }
else navigate(`/purchase-evaluations?id=${id}&expand=1`)
}
- Panel 2 (
:162-179) thêm nhánh — tái dùng component sẵn có, KHÔNG dựng viewer mới:
{selectedPeId && peDetail.data && (
<PeDetailTabs evaluation={peDetail.data} onBack={() => setPe(null)} onDelete={() => {}} readOnly />
)}
✅ Đã VERIFY chữ ký: PeDetailTabs.tsx:103-119 — evaluation: PeDetailBundle · onBack: () => void · onDelete: () => void (cả 2 BẮT BUỘC, không ?) · readOnly?: boolean = false. Nút Xoá bị chặn bởi :257 (… && !readOnly) ⇒ với readOnly thì onDelete không có đường chạm ⇒ () => {} an toàn — ghi comment ngay tại chỗ kẻo người sau tưởng quên nối. Tiền lệ: trang PE cũng truyền readOnly={true} ở PurchaseEvaluationsListPage.tsx:657-663.
- query:
useQuery({ queryKey: ['khkk-pe-detail', selectedPeId], queryFn: … GET /purchase-evaluations/${id}, enabled: !!selectedPeId })— key RIÊNG, không mượn key của trang PE (bài S159: đổi/đụng cache dùng chung làm vỡ trang khác). - Panel 3 (
:186-202) khi đang xem PE: KHÔNG renderPeWorkflowPanel(nó mang nút duyệt/xoá phiếu PE — cấp thao tác duyệt PE từ màn KHKK là sai lane quyền). Thay bằng hộp dashed + 1 lối thoát tường minh: nút "Mở phiếu Duyệt NCC gốc ↗" →navigate('/purchase-evaluations?id=…&expand=1'). Owner nói "vẫn cho hiển thị ra", không cấm mở trang gốc — chỉ cấm tự động văng.
F2. 🔴 "tương tự với duyệt NCC" — 2 cách hiểu, cả 2 đều dẫn tới cùng một máy
- (i) Trên màn Duyệt NCC cũng phải ở-tại-chỗ: hiện leaf GĐ2 trên cây trang PE chạy N2 nhánh mặc định ⇒
navigate('/khkk/:id')⇒ VĂNG khỏi màn Duyệt NCC — lỗi đối xứng, ĐANG TỒN TẠI. - (ii) Làm y như khuôn PE đang làm: chính là
stage1Content+onOpenKhkk. → Đề nghị ship 2 nấc: N1 = màn KHKK (F1, nhỏ, đúng nguyên văn owner); N2 = màn Duyệt NCC truyềnonOpenKhkk→ panel 2 render<KhkkDetailContent plan readOnly/>(component đã nhậnreadOnly, chứng cứKhkkListPage.tsx:177) + query['pe-khkk-detail', id]. Nấc 2 to hơn (thêm 1 query + 1 nhánh panel ở trang PE) ⇒ khai với owner là 2 lượt, đừng gộp im lặng rồi vỡ trang PE đang golive.
F3. Vì sao BẮT BUỘC có selected (không phải trang trí)
KhkkPlanCard có selected={selectedId === k.id} (KhkkListPanel.tsx:241) nên bấm card KHKK là cây sáng lên. Leaf PE không có khái niệm đó (PipelineLeaf :46-56). Nếu chỉ chuyển navigate → callback mà không thêm selected: người dùng bấm phiếu PE, panel 2 đổi ở cột giữa trong khi cột trái im như không nhận lệnh ⇒ họ bấm lại lần 2, lần 3. Đây là hồi-quy trải-nghiệm do chính bản vá gây ra — FD4 "Polish/states" fail. selected + aria-current là phần không thể cắt.
F4. Ảnh hưởng lên 6 host — O6
| Host | Đổi hành vi? | Cụ thể |
|---|---|---|
H1 KHKK admin KhkkListPanel.tsx:292 |
🔴 CÓ — đây là mục tiêu | bấm card PE: navigate('/purchase-evaluations?id=…&expand=1') → ở lại màn KHKK, panel 2 = PeDetailTabs readOnly, leaf sáng selected. <lg vẫn rời trang (không có panel 2) |
| H2 KHKK user | 🔴 CÓ | mirror H1 |
H3 HĐ admin ContractsListPage.tsx:249 |
✅ KHÔNG | không truyền stage1Options ⇒ opts.onOpenPe === undefined ⇒ nhánh navigate cũ nguyên vẹn |
H4 HĐ user MyContractsPage.tsx:225 |
✅ KHÔNG | như H3 |
| H5 Bản cứng admin | ✅ KHÔNG | như H3 |
| H6 Bản cứng user | ✅ KHÔNG | như H3 |
H7/H8 trang Duyệt NCC :524 |
✅ KHÔNG ở nấc 1 | GĐ1 vẫn dùng stage1Content (onOpenPe không đụng tới nhánh content). Nấc 2 mới đổi (leaf GĐ2) |
Cơ chế bảo đảm "không đổi": cùng khuôn ternary đã chứng minh ở M-2 (:233) — vắng callback ⇒ rơi về navigate. Đây là lý do chọn callback tuỳ chọn thay vì đổi thẳng đích navigate. |
G. Style/UX delta cây: GĐ2 (KHKK) vs GĐ1 (Duyệt NCC = CHUẨN) ✅
| # | Delta | Bằng chứng | Verdict |
|---|---|---|---|
| G1 | KHKK truyền hideHeader ⇒ mất thanh "Cây toàn trình · N gói" và mất nút "Hiện/Ẩn cây"; :233-234 phải ép flex để <lg còn thấy cây |
PipelineTreePanel.tsx:104-109, :234 |
⚠️ MINOR THẬT: trên mobile màn KHKK, cây luôn mở, không thu gọn được, đẩy danh sách phiếu xuống dưới màn hình. Trang PE có đường thu gọn. Đề xuất: khi hideHeader, <lg mặc định đóng + 1 nút text nhỏ "Hiện cây" phía trên thân |
| G2 | KHKK bỏ vỏ card-accent + lg:sticky |
:203 |
✅ ĐÚNG — owner S174 chốt đồng bộ viền; cây nhúng trong panel không cần vỏ thứ hai |
| G3 | GĐ2 trên màn KHKK không có 8 ngăn N1-N8 (vì dùng content), còn trên màn PE/HĐ/Bản-cứng thì CÓ |
usePipelineStages.ts:168-171, :292-297 |
🔴 LÝ-LẼ ĐÃ MẤT CHÂN ĐẾ: comment :170 biện minh "lối vào nhóm giữ ở Select nhóm của khối filter panel 1" — nhưng Select nhóm ĐÃ BỊ GỠ (KhkkListPanel.tsx:184-186: "Select nhóm đã gỡ khỏi UI. Nhóm nay đến DUY NHẤT từ URL"; khối filter :257-288 chỉ còn ô tìm + Select trạng thái). ⇒ sửa comment cho khỏi nói dối, và cân nhắc trả 8 ngăn về khi group === null |
| G4 | Leaf GĐ1 (mã mono + badge, compact) vs card GĐ2 (KhkkPlanCard, giàu) |
usePipelineStages.ts:209-221 vs KhkkListPanel.tsx:239-244 |
✅ NHẤT QUÁN có nguyên tắc: GĐ đang đứng = card, GĐ khác = leaf. GIỮ |
| G5 | Emoji thay icon ở 3 tầng trên: 📅 :294 · 📁 :310 · 🧱 :326 — trong khi tầng 4 (folder GĐ) dùng lucide (STAGE_ICON PipelineStageFolders.tsx:29-34) |
PipelineTreePanel.tsx:294/310/326 |
❌ ANTI-SLOP CATCH (FD3): 2 bộ icon lệch nhau trong cùng một cây. Emoji còn đổi hình theo OS và không nhận màu token. Đề xuất lucide CalendarDays / FolderClosed / Layers + icon-chip như folder GĐ |
| G6 | Chevron 3 tầng trên = <svg> inline thủ công ×3 (lặp), tầng 4 = <ChevronRight> lucide |
:293/309/325 vs PipelineStageFolders.tsx:165/245 |
❌ cùng lớp G5 — thay bằng lucide, bớt 3 khối SVG lặp |
| G7 | Chevron 3 tầng trên thiếu motion-reduce:transition-none (folder GĐ có) |
:293/309/325 vs PipelineStageFolders.tsx:167/247 |
❌ FD5: prefers-reduced-motion chỉ được tôn trọng nửa cây |
| G8 | <summary> 3 tầng trên thiếu hoàn toàn focus-visible:ring… (folder GĐ + ngăn nhóm đều có) |
:292/308/324 vs PipelineStageFolders.tsx:164/244 |
❌ FD5 FAIL THẬT: đi bàn phím qua Năm/Dự án/Hạng mục mất dấu focus, tới folder GĐ mới thấy lại. Vá: dán đúng chuỗi focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/60 |
| G9 | Chip đếm 3 tầng trên: bg-slate-200 text-slate-700 (:296) và bg-slate-100 text-slate-600 (:312/328) — tầng 4 lại style.count theo màu GĐ |
— | ✅ chấp nhận: xám = tầng cấu-trúc, màu = tầng nghiệp-vụ. Nhưng nên thêm tabular-nums cho cả 3 (cùng lý do D1) |
H. Spec thi-hành-được cho Opus 5 MAX (thứ tự áp) ✅
S0 — tiền đề: 3 file MIRROR ×2 app. Mọi bước dưới áp 2 lần, chốt bằng sha256sum 3 cặp (kỳ vọng: 3 cặp khớp). Không có bước nào chạm BE/DB.
| Bước | Việc | File | Rủi ro |
|---|---|---|---|
| P1 | O2: thay SoonChip → CountChip/UnknownChip (D1) + vá 6 comment (D4) |
PipelineStageFolders.tsx (+usePipelineStages.ts comment) |
Thấp — 0 đổi prop-surface |
| P2 | O2-hỏi: chốt ca locked = "—" hay "0" (D2) |
— | Cần 1 câu owner |
| P3 | O6: onOpenPe + 1 dòng :219 (F1a) |
usePipelineStages.ts |
Thấp — ternary vắng-callback giữ nguyên |
| P4 | O6: selected + aria-current + nền active (F1b) |
PipelineStageFolders.tsx |
Thấp |
| P5 | O6: prop stage1Options + spread (F1c) |
PipelineTreePanel.tsx |
Thấp — optional |
| P6 | O6: ?pe= + panel 2 PeDetailTabs readOnly + panel 3 hộp dashed + nhánh <lg (F1d/e) |
KhkkListPage.tsx · KhkkListPanel.tsx |
Trung bình — đây là chỗ dễ vỡ nhất (loại trừ id⟂pe, <lg) |
| P7 | (tuỳ chọn, khuyến nghị) FD5/FD3: G8 focus-ring → G7 motion-reduce → G5/G6 lucide | PipelineTreePanel.tsx |
Thấp, thuần class |
| P8 | Escalate D6 (PePipelineStrip) + G3 (comment Select nhóm đã gỡ) |
ngoài lens | — |
Nghiệm thu (đo được, không cảm tính):
- Có control-dương, KHÔNG dùng phép trích rỗng. ⚠️ ĐỪNG grep
">Sắp<": JSX để chữ trên DÒNG RIÊNG (:103-105) nên mẫu đó trả 0 hit ngay cả lúc chưa vá — 0-hit giả đọc y hệt đã-sạch.- Control-dương ĐO TRƯỚC PATCH (2026-08-05):
grep -rn "SoonChip" fe-admin/src fe-user/src⇒ 8 hit (mỗi app 4: định nghĩa:99+ gọi:195/:233/:273). - Sau patch: 0 hit. Chênh 8→0 mới là bằng chứng.
grep -rn "Sắp" fe-admin/src fe-user/srcsau patch ⇒ chỉ cònDashboardPage.tsx:113("Sắp quá hạn (24h)" — nghĩa khác, KHÔNG đụng) vàPePipelineStrip.tsxnếu P8 chưa làm. Phải liệt kê thẳng phần còn lại khi báo cáo, đừng khai "sạch".
- Control-dương ĐO TRƯỚC PATCH (2026-08-05):
npm run buildPASS 0 TS error ×2 app.sha256sum3 cặp file mirror ⇒ khớp 3/3.grep -c "navigate(\|navigate('" fe-admin/src/hooks/usePipelineStages.ts⇒ **vẫn 7 DÒNG** (đo trước-patch 2026-08-05: 7 = **6 lời gọi** :219/:233/:263/:272/:366/:378 **+ 1 comment** :182 — đừng đọc 7 thành "7 lời gọi"). Callback là nhánh THÊM, KHÔNG xoá nhánhnavigate` nào ⇒ đây là bằng chứng máy-đo cho "5 host kia không đổi hành vi".- Soi mắt: KHKK screen — bấm card PE không đổi URL path (chỉ thêm
?pe=), panel 2 hiện phiếu PE chỉ-xem, leaf trái sáng.
I. Rủi-ro / KHÔNG được đụng ✅
- KHÔNG đổi
PIPELINE_STAGE_LABELS(:22-27) — nhãn VERBATIM lời owner,:6-8khai rõ GĐ2 CỐ Ý khác nhãn strip. - KHÔNG đổi quyết định "hiện đủ 8 ngăn kể cả rỗng" — chỉ đổi chữ trên chip + lý-lẽ trong comment.
- KHÔNG đổi gate quyền (
KHKK_MENU_KEYS = ['KeHoachKyKet']:57) — F-1/S162, gotcha #85. - KHÔNG sửa
PePipelineStriptrong lane này (ngoài 3-file lens) — escalate. - KHÔNG xoá nhánh
navigatecũ khi thêm callback — 5-6 host sống nhờ nó. stage2Contentluôn đi kèmstage2Count(PipelineStageFolders.tsx:95-97); nếu ai đó thêmstage1Contentcho KHKK thì luật y hệt.PeDetailTabsmặc địnhreadOnly=false⇒ quênreadOnlylà mở nút thao tác phiếu PE ngay trong màn KHKK. Bắt buộc truyền.- Cửa sổ index 200 (
INDEX_PAGE_SIZE :47,PE_INDEX_PAGE_SIZE PipelineTreePanel.tsx:22) không đổi ⇒ "0" trên folder vẫn có thể là "0 trong 200 bản ghi mới nhất"; dòng khaitruncated(PipelineStageFolders.tsx:301-305) giữ nguyên, đừng bỏ — nó chính là thứ giữ cho số 0 không nói dối.
J. FD2 visual-loop — KHAI THẲNG ✅
Lượt này KHÔNG có screenshot và không bịa: (a) lane là audit/spec, 0 dòng code UI đổi ⇒ không có gì mới để chụp; (b) màn KHKK nằm sau ProtectedRoute + cần API+SQL, dev-rig authed đã ghi nhận chặn (MEMORY gotcha #3: vite proxy http://localhost:5443 ⟂ API bind HTTPS). Thay thế: static-analysis + số đo tương phản tính tay theo WCAG 2.1 (D3) + đối chiếu class token trực tiếp trên đĩa. Khi P1-P7 land, FD2 loop chạy trên prod sau deploy (đường tin cậy nhất theo MEMORY).
K. Bổ sung sau khi đo — file HOST cũng đang MIRROR ✅
sha256sum (2026-08-05): components/khkk/KhkkListPanel.tsx ad9073db…c314d khớp 2 app · pages/khkk/KhkkListPage.tsx ee0dc8af…b69f4 khớp 2 app.
⇒ Bước P6 (chỗ sửa nhiều nhất của O6) cũng phải áp ×2 app và giữ SHA khớp. Tổng cộng O2+O6 chạm 5 cặp file = 10 file, nghiệm thu 5/5 cặp SHA khớp. Bỏ sót 1 bên ⇒ admin và user lệch hành vi im lặng (đúng lớp lỗi mà 3 header file cảnh báo).
Bảng phủ owner-item
| Item | Đòi hỏi | Phủ ở | Trạng thái |
|---|---|---|---|
| O2 | chữ SẮP sinh ở đâu (file:line) | §C bảng S-a/S-b/S-c — :99-107 def, gọi :195/:233/:273 |
✅ |
| O2 | điều kiện bật từng ca | §C cột 3 | ✅ |
| O2 | GĐ nào còn đúng "sắp" / GĐ nào đã live | §C — không GĐ nào còn đúng, cả 4 đều fetch API thật | ✅ |
| O2 | đổi sang 0 thì đụng gì | §D1 (patch) + §D4 (6 comment) + §D3 (contrast) | ✅ |
| O2 | có host nào dựa vào chữ SẮP không | §D5 — KHÔNG (SoonChip không export, 8-hit nội bộ) |
✅ |
| O6 | truy onOpenKhkk + mọi navigate( trong 3 file |
§E bảng N1-N6 + §số-đo-nền | ✅ |
| O6 | chỉ ra ĐÚNG đường đang điều hướng | §E — usePipelineStages.ts:219 → /purchase-evaluations?id=…&expand=1 |
✅ |
| O6 | đề xuất callback-hoá mirror stage2Options.onOpenKhkk |
§F1 (a-e) — onOpenPe, cùng khuôn ternary :233 |
✅ |
| BẮT BUỘC | 6 host bị ảnh hưởng thế nào — cho CẢ HAI đề xuất | §D7 (O2, 6 host + H7/H8) · §F4 (O6, 6 host + H7/H8) | ✅ |
END sub-frontend-designer-L4 — VERDICT=SHIP-SPEC-READY (2 đề xuất thi-hành-được; 1 câu hỏi owner treo ở §D2; 1 escalate ngoài lens ở §D6)