Files
solution-erp/.claude/workflows/runs/2026-08-05-S174-khkk-style-audit/sub-frontend-designer-L3.md
2026-08-05 16:27:07 +07:00

263 lines
30 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-L3 — LĂNG-KÍNH: HOST / BỐ CỤC
> Run: 2026-08-05-S174-khkk-style-audit · Lane L3 (ensemble /fable-clone frontend-designer, 5 lane cùng vai)
> Mục tiêu: quét style/UX host+bố-cục của KHKK (GĐ2) đối chiếu Duyệt NCC (GĐ1, golive = CHUẨN).
> Đầu ra: spec thi-hành-được cho Opus 5 MAX.
## 0. KHUNG RỖNG (ghi @lượt 1, TRƯỚC khi đọc file) — append-as-I-read
### 0.1 File trong lăng-kính (3 chính)
- [x] A. `fe-admin/src/pages/pe/PurchaseEvaluationWorkspacePage.tsx` — CHUẨN (host 2-panel "Thao tác" GĐ1) — đọc HẾT 147 dòng
- [x] B. `fe-admin/src/pages/khkk/KhkkCreatePage.tsx` — màn "Thao tác" GĐ2, vừa dựng lại — đọc HẾT 394 dòng
- [x] C. `fe-admin/src/pages/khkk/KhkkListPage.tsx` — màn "Danh sách" GĐ2 (3-panel) — đọc HẾT 206 dòng
- [x] (đối chiếu) `fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx`**KHÔNG mở toàn file** (807 dòng): chỉ grep có mục tiêu + đọc đúng 24 dòng header/grid (:379-402)
- [x] (phụ, grep-only — không mở toàn file) `components/pe/PeListPanel.tsx` · `components/khkk/KhkkWorkListPanel.tsx` · `components/Layout.tsx` · `components/TopBar.tsx` · `src/index.css`
### 0.2 Trục so sánh (6 trục bắt buộc)
- [x] T1. Header trang: icon · cỡ chữ title · badge · dòng hint bên phải → §1/§2/§3, lệch: **badge** (H-3)
- [x] T2. Chiều cao host `h-[calc(100vh-4rem)]`**KHỚP 4/4 file**; sai ~3px là lỗi toàn hệ (§3bis-d) — KHÔNG sửa
- [x] T3. Grid cột + breakpoint → **LỆCH** 300/340 vs 260/320 (H-1), số học ở §3bis-b
- [x] T4. EmptyState → **KHỚP** khuôn (icon+title+description), KHKK còn thêm nhánh not-found mà PE thiếu
- [x] T5. Hành vi `<lg`**LỆCH NẶNG**: KHKK-Create không có gì ⇒ ngõ cụt (H-2), 3 khuôn khác nhau ở §3bis-e
- [x] T6. Cách truyền `readOnly`**LỆCH**: hằng số (Create) vs công thức host (List) (H-4)
### 0.3 Bảng kết
| # | Trục | PE (chuẩn) | KHKK Create | KHKK List | Verdict | Sửa |
|---|---|---|---|---|---|---|
| T1a | shell + header class | `flex h-[calc(100vh-4rem)] flex-col` + header 1 khuôn | y hệt (:168-169) | y hệt (:123-124) | ✅ KHỚP BYTE | — |
| T1b | icon | `ClipboardCheck h-5 w-5 text-slate-500` | `FileCheck` cùng size/màu | `FileCheck` | ✅ (đổi icon = đúng, theo module) | — |
| T1c | title | `text-base font-semibold tracking-tight text-slate-900`, không truncate | + `min-w-0`+`truncate` | + `min-w-0`+`truncate` | ⚠️ KHKK **tốt hơn** | H-7 giữ |
| T1d | badge | Workspace: KHÔNG · List: pill **số** (:386) | **KHÔNG có** | pill "Giai đoạn 2 · nhãn" (:128) | 🔴 lệch nội-bộ GĐ2 | **H-3** |
| T1e | phải header | hint `text-[12px] text-slate-500` · List: TRỐNG | hint ✅ | **Button** "Lập kế hoạch" | ⚪ có lý do | H-9 giữ |
| T2 | chiều cao | `100vh-4rem` | y hệt | y hệt | ✅ | — |
| T3 | grid | `[260px_1fr]` / xl `[320px_1fr]` | `[300px_1fr]` / xl `[340px_1fr]` | `[400px_1fr_360px]` = PE list | 🔴 Create lệch | **H-1** |
| T4 | EmptyState | icon+title+desc (:102-108) | icon+title+desc (:217-221) | + rẽ `deletedView` (:163-171) | ✅ | — |
| T5 | `<lg` | dead-code (:135-144) | **không có gì** ⇒ không submit được | matchMedia → navigate (:102-109) | 🔴 hỏng chức năng | **H-2** |
| T6 | readOnly | không dùng — `mode="workspace"` | `readOnly` hằng số (:210) | công thức host (:112-117) | 🔴 2 luật cho 1 component | **H-4** |
| — | mirror 2 app | — | SHA256 == fe-user | SHA256 == fe-user | ✅ 4/4 SAME | giữ khi sửa |
### 0.4 Spec thi-hành-được → §4 (H-1…H-9, mỗi mục: file:line · hiện-trạng · đích · vì · rủi-ro)
- [x] Đã viết. Thứ tự thi hành + danh sách **KHÔNG SỬA** ở cuối §4.
### 0.5 Nhật ký đọc (theo thứ tự tool-call)
1. `wc -l` 4 file + Read **B** `KhkkCreatePage` (394) — song song
2. Read **A** `PurchaseEvaluationWorkspacePage` (147) + Read **C** `KhkkListPage` (206) — song song → APPEND §1/§2/§3
3. Grep header/grid `PurchaseEvaluationsListPage` + Grep root-className 3 panel
4. Grep `KhkkWorkListPanel` (sticky/tier) + Grep `PeListPanel` (sticky create)
5. Grep `PurchaseEvaluationsListPage` cơ-chế `<lg` (overlay `expand=1`) + Bash sha256 mirror + `git status`
6. Bash `grep -c Button` (đối chứng) + Grep `Layout.tsx` (main/topbar)
7. Grep `TopBar.tsx` (h-14) + Bash khảo sát toàn bộ `lg:grid-cols-[` → APPEND §3bis + §4
8. Bash `index.css` tokens + probe cổng + Read `PurchaseEvaluationsListPage:379-402`
9. Bash đếm stop `violet-*` toàn repo → **sửa lại H-8** (bác giả-thuyết từ MEMORY) + APPEND §5
---
## 1. FILE A — PurchaseEvaluationWorkspacePage.tsx (CHUẨN) — 147 dòng, ĐÃ ĐỌC HẾT
Khuôn host "Thao tác" GĐ1 (đã golive, owner tuyên "đã tốt rồi", byte revert `1b3bcaa`):
| Chi tiết | Giá trị | Dòng |
|---|---|---|
| Shell | `<div className="flex h-[calc(100vh-4rem)] flex-col">` | :69 |
| Header | `flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3` | :70 |
| Icon | `ClipboardCheck` `h-5 w-5 text-slate-500` | :72 |
| Title | `text-base font-semibold tracking-tight text-slate-900` — KHÔNG `truncate`, KHÔNG `min-w-0` ở wrapper (:71 `flex items-center gap-2`) | :71-73 |
| Badge | **KHÔNG CÓ** badge ở host Thao-tác | — |
| Hint phải | `text-[12px] text-slate-500` — "Workspace 2-panel — Workflow + Duyệt ở menu &ldquo;Duyệt&rdquo;." | :75-77 |
| Grid | `grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[260px_1fr] xl:grid-cols-[320px_1fr]` | :80 |
| Panel 2 | `<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">` | :100 |
| EmptyState | `icon={ClipboardCheck}` + title "Chọn phiếu hoặc tạo mới" + description có nhắc nút `"+ Thêm mới"` | :102-108 |
| readOnly | **KHÔNG có prop `readOnly`** — thay bằng `mode="workspace"` truyền xuống `PeDetailTabs` (:127) + `editableOnly` cho panel list (:95) | :95,127 |
| `<lg` | Panel 2 ẩn; có khối `lg:hidden` (:136-144) — **DEAD CODE**: `<button className="hidden">` không bao giờ bấm được, comment nói "tap row khi mobile sẽ navigate fullpage" nhưng KHÔNG ai gọi | :135-144 |
| Loading | `{selectedId && detail.isLoading && <div className="text-sm text-slate-500">Đang tải…</div>}` | :120-122 |
| Not-found | **KHÔNG có** nhánh "Không tìm thấy phiếu" | — |
⚠️ Ghi nhận NGƯỢC: chuẩn GĐ1 CÓ 2 chỗ yếu (dead-code `lg:hidden` :136-144 · thiếu nhánh not-found) — KHÔNG được "mirror cho giống" 2 cái này.
## 2. FILE B — KhkkCreatePage.tsx (màn Thao-tác GĐ2, vừa dựng lại) — 394 dòng, ĐÃ ĐỌC HẾT
| Chi tiết | Giá trị | Dòng | vs PE |
|---|---|---|---|
| Shell | `flex h-[calc(100vh-4rem)] flex-col` | :168 | ✅ KHỚP byte |
| Header class | y hệt PE | :169 | ✅ KHỚP byte |
| Icon | `FileCheck h-5 w-5 text-slate-500` | :171 | ✅ khớp khuôn (icon khác = đúng, GĐ2 dùng FileCheck xuyên suốt) |
| Title wrapper | `flex min-w-0 items-center gap-2` + `truncate` trên `<h1>` | :170-172 | ⚠️ LỆCH nhẹ vs PE (PE không truncate) — nhưng đây là **cải tiến ĐÚNG** vì title GĐ2 dài (`… — Thao tác · <nhãn nhóm>`), khớp `KhkkListPage:125-127` |
| Badge "Giai đoạn 2" | **THIẾU**`KhkkListPage:128-130` CÓ pill `Giai đoạn 2 · <nhãn nhóm>`, màn Thao-tác KHÔNG | :169-177 | 🔴 LỆCH NỘI-BỘ GĐ2 (2 màn cùng module, 1 có 1 không) |
| Hint phải | `text-[12px] text-slate-500` "Bấm phiếu ở cột trái — tầng trên lập kế hoạch, tầng dưới chỉ xem." | :174-176 | ✅ KHỚP khuôn PE |
| Grid | `lg:grid-cols-[300px_1fr] xl:grid-cols-[340px_1fr]` | :179 | 🔴 LỆCH SỐ ĐO: PE = `260px`/`320px`. Comment đầu file (:6) tự khai "bê khuôn `lg:grid-cols-[300px_1fr]`" — tức là **comment mô tả đúng code nhưng code KHÔNG đúng khuôn PE** |
| Panel 2 | `hidden overflow-y-auto bg-slate-50 p-6 lg:block` | :187 | ✅ KHỚP byte |
| EmptyState | `icon={FileCheck}` + title "Chọn phiếu Duyệt NCC để lập kế hoạch" + desc chỉ rõ 2 tầng | :217-221 | ✅ khớp khuôn |
| Loading | `Đang tải…` `text-sm text-slate-500` (:205) | :205 | ✅ |
| Not-found | CÓ `Không tìm thấy phiếu.` `text-sm text-red-600` (:207) | :207 | ✅ TỐT HƠN PE |
| readOnly | `<KhkkDetailContent plan={…} readOnly onChanged={…} />`**hard-code `readOnly` không điều kiện** | :210 | 🔴 LỆCH TRỤC: `KhkkListPage:113-117` tính `readOnly` theo phase+drafter (P2-7 "host quyết"); màn Thao-tác ép cứng ⇒ **người vừa tạo phiếu KHÔNG sửa được ngay tại chỗ**, phải bấm "Mở trang đầy đủ" |
| `<lg` | Panel 2 `hidden … lg:block`; **KHÔNG có** fallback navigate (không `selectPlan`-style matchMedia) | :187 | ⚠️ mobile = panel trái nằm 1 mình, bấm PE ⇒ set state nhưng panel 2 vẫn ẩn ⇒ **bấm không thấy gì**. `KhkkListPage:102-109``matchMedia('(min-width:1024px)')` → navigate `/khkk/:id`. PE cũng hở (dead-code) nhưng ở PE panel 1 có nút "+ Thêm mới" nên ít chết hơn |
| Banner "đang xem" | `border-violet-200 bg-violet-50` + 2 nút (`Mở trang đầy đủ` outline · `Về form tạo` ghost) | :191-204 | KHÔNG có ở PE — hợp lý (PE không có chế-độ xem-phiếu-người-khác trong workspace). Màu violet = **token ngoài dải brand/slate của host** (host GĐ1 chỉ dùng slate/brand) |
| Form card | `rounded-lg border border-slate-200 bg-white shadow-sm` + `divide-y divide-slate-200` + `Section` px-5 py-4 | :226-236,387-393 | ✅ khớp `PeWorkspaceCreateView` (helper `Section` khai rõ là duplicate cố ý, :385-386) |
| Footer form | `border-t border-slate-200 bg-slate-50 px-5 py-3`, hint trái + nút phải | :353-376 | ✅ khuôn PE-create |
| Ký hiệu cảnh báo | dùng ký tự `⚠` / `✓` **trong chuỗi text** (:324, :357, :360) | :324,357,360 | 🔴 ANTI-SLOP FD3: glyph thay icon. PE-family dùng `lucide` (`X`, `Plus`, `ClipboardCheck`). GĐ2 List cũng dùng lucide `X` (:189) |
## 3. FILE C — KhkkListPage.tsx (màn Danh-sách GĐ2, host 3-panel) — 206 dòng, ĐÃ ĐỌC HẾT
| Chi tiết | Giá trị | Dòng |
|---|---|---|
| Shell | `flex h-[calc(100vh-4rem)] flex-col` | :123 |
| Header | class y hệt PE/Create | :124 |
| Icon | `FileCheck h-5 w-5 text-slate-500` | :126 |
| Title | `truncate text-base font-semibold tracking-tight text-slate-900`, wrapper `flex min-w-0 items-center gap-2` | :125-127 |
| **Badge** | `ml-2 truncate rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600` — "Giai đoạn 2 · <nhãn nhóm>" | :128-130 |
| Phải | **Nút** `Lập kế hoạch` (`Plus` + navigate `/khkk/create?group=`) — KHÔNG phải dòng hint | :132-137 |
| Grid | `lg:grid-cols-[400px_1fr_360px]` (3 panel) | :140 |
| Panel 2 | `hidden overflow-y-auto bg-slate-50 p-6 lg:block` | :162 |
| Panel 3 | `hidden overflow-y-auto border-l border-slate-200 bg-white p-4 lg:block` | :186 |
| EmptyState | `icon={FileCheck}`, title/desc rẽ theo `deletedView` | :163-171 |
| `<lg` | `selectPlan` :102-109 dùng `window.matchMedia('(min-width: 1024px)')``<lg` navigate `/khkk/:id` (giữ `?group=`) | :102-109 |
| readOnly | tính ở HOST: `!plan || !(DangSoanThao|TraLai) || drafterUserId !== user.id` | :112-117 |
| Hộp rỗng panel 3 | `rounded-lg border border-dashed border-slate-200 p-6 text-center text-sm text-slate-400` + lucide `X` | :188-193 |
## 3bis. BẰNG CHỨNG PHỤ (đọc ngoài 3 file chính — chỉ grep có mục tiêu, KHÔNG mở toàn file)
### (a) Khảo sát TOÀN BỘ host-grid `fe-admin/src/pages` (`grep -rn "lg:grid-cols-\["`)
```
contracts/ContractCreatePage.tsx:129 lg:grid-cols-[320px_1fr] ← "thao tác" GĐ3
contracts/ContractsListPage.tsx:125 lg:grid-cols-[340px_1fr_360px]
khkk/KhkkCreatePage.tsx:179 lg:grid-cols-[300px_1fr] xl:[340px_1fr] ← GĐ2 thao tác
khkk/KhkkListPage.tsx:140 lg:grid-cols-[400px_1fr_360px] ← = PE list ✅
pe/PurchaseEvaluationsListPage.tsx:397 lg:grid-cols-[400px_1fr_360px] ← CHUẨN list
pe/PurchaseEvaluationWorkspacePage.tsx:80 lg:grid-cols-[260px_1fr] xl:[320px_1fr] ← CHUẨN thao tác
```
`KhkkCreatePage`**giá trị THỨ BA** trong họ "thao tác" (260/320 · 320 · 300/340) — không khớp PE, cũng không khớp Contract.
⇒ Đối chứng DƯƠNG: `KhkkListPage:140` khớp `PurchaseEvaluationsListPage:397` **byte-for-byte**, và `KhkkListPage:9` ghi thẳng luật *"Số đo panel lấy của PE (400px) vì hiến-pháp owner là 'giống nhất với Duyệt NCC'"***luật đã tồn tại, file Create không áp**.
### (b) Số đo thật (Layout `fe-admin/src/components/Layout.tsx:372` sidebar `w-72` = 288px, `xl:w-80` = 320px)
| Viewport | Vùng nội dung | Panel-1 PE | Panel-2 PE | Panel-1 KHKK-Create | Panel-2 KHKK-Create | Δ panel-2 |
|---|---|---|---|---|---|---|
| 1024 (lg) | 1024288 = **736** | 260 | **476** | 300 | **436** | **40px** |
| 1280 (xl) | 1280320 = **960** | 320 | **640** | 340 | **620** | **20px** |
| 1366 (laptop SE) | 1366320 = **1046** | 320 | **726** | 340 | **706** | 20px |
Panel-2 KHKK-Create trừ `p-6` (48px) ⇒ **388px** ở lg. Trong đó Section 2 dùng `md:grid-cols-2` (:284) — `md:`**viewport-based (768px)**, KHÔNG container-based ⇒ ở 1024 nó ĐÃ chia đôi: 2 ô ~186px cho "Nhóm duyệt" + "Quy trình duyệt" (ô quy trình in `mã · tên``truncate`, :313-317) ⇒ tên quy trình gần như chắc chắn bị cắt. PE cho 428px ⇒ ~210px/ô.
⇒ Lệch đi **NGƯỢC hướng nhu cầu**: cột danh sách rộng hơn chuẩn 40px, cột FORM (thứ cần chỗ) hẹp hơn chuẩn 40px.
### (c) Panel-1: hợp đồng bố-cục
- `PeListPanel.tsx:92` root `<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">` + **sticky bottom action** `:238-241` `<div className="border-t border-slate-200 bg-white p-2"><Button …` (`grep -c Button` = **8**).
- `KhkkWorkListPanel.tsx:142` root `<aside className="flex min-h-0 flex-col overflow-hidden border-r border-slate-200 bg-white">` — khớp + `min-h-0` (cần cho 2 tầng). Nhưng `grep -c Button` = **0****KHÔNG có nút hành-động dính đáy**.
🔴 ⇒ comment `KhkkCreatePage.tsx:7` *"Panel 1 = danh sách (`KhkkWorkListPanel`) + nút hành-động dính đáy"* **MÔ TẢ THỨ KHÔNG TỒN TẠI** (claim > việc đã làm). Thứ ở đáy tầng-1 là `:219 <div className="shrink-0 border-t-2 border-slate-200" />` = vạch ngăn 2 tầng, không phải nút.
- 2 tầng: `Tier()` :48-73, dùng ở :154 và :221; `min-h-0 flex-1` cả hai ⇒ tỉ lệ 50/50 (comment :46-47).
### (d) Chiều cao `h-[calc(100vh-4rem)]` — đo lại chrome thật
`Layout.tsx:370` dải gradient `h-1` (4px) + `TopBar.tsx:84` `<header className="flex h-14 …">` (56px) + `border-b` (1px) = **61px ≈ 3.81rem**, trong khi mọi host trừ **4rem = 64px** ⇒ hụt ~3px.
**KHÔNG phải lỗi KHKK** — 4/4 host (PE ×2, KHKK ×2) đều `h-[calc(100vh-4rem)]`, sai lệch là **toàn hệ thống**. Ghi nhận, KHÔNG đưa vào spec sửa lần này (đổi 1 file = lệch khỏi 3 file kia; phải sửa cả họ hoặc không sửa).
### (e) `<lg` — 3 khuôn KHÁC NHAU
| Màn | Cơ chế | Bằng chứng |
|---|---|---|
| PE list (chuẩn 3-panel) | `selectRow` set `expand=1` khi `window.innerWidth < 1024` ⇒ overlay full-bleed | `PurchaseEvaluationsListPage.tsx:147-158` |
| KHKK list | `matchMedia('(min-width: 1024px)')` sai ⇒ `navigate('/khkk/:id')` | `KhkkListPage.tsx:102-109` |
| PE workspace (chuẩn 2-panel) | **dead-code** `lg:hidden` + `<button className="hidden">` không ai bấm | `PurchaseEvaluationWorkspacePage.tsx:135-144` |
| **KHKK Create** | **KHÔNG CÓ GÌ** | `KhkkCreatePage.tsx:187` `hidden … lg:block` |
🔴 Hệ quả KHKK-Create ở `<lg`: grid về `grid-cols-1` ⇒ chỉ `KhkkWorkListPanel` hiện (aside không có `lg:` gate). Bấm PE ⇒ `pickPe` set state (:153-156) nhưng `<main>` vẫn `hidden`**màn không đổi gì**. Form + nút "Tạo kế hoạch" (:372) nằm TRONG `<main>`**không thể tạo kế hoạch trên màn <1024px**. Đây là **hỏng chức năng**, không phải lệch thẩm mỹ — nặng hơn dead-code của PE (PE vẫn còn nút "+ Thêm mới" ở panel-1 nhưng cũng dẫn vào `<main>` ẩn ⇒ PE cũng hở, chỉ là hở SẴN của chuẩn).
### (f) Mirror 2 app — ✅ SẠCH
`sha256sum` fe-admin vs fe-user (4 file KHKK): `KhkkCreatePage` `bac8e0050711` · `KhkkListPage` `ee0dc8afc5fd` · `KhkkWorkListPanel` `fcb97650f51c` · `KhkkListPanel` `ad9073db2c90`**SAME 4/4**. `git status --porcelain -- fe-admin/src fe-user/src` = **rỗng** ⇒ bản S174 đã commit, không có drift treo. ⚠️ Mọi sửa dưới đây PHẢI áp CẢ 2 app rồi so lại SHA256.
---
## 4. SPEC THI-HÀNH-ĐƯỢC (cho Opus 5 MAX) — lăng-kính HOST/BỐ-CỤC
> Nguyên tắc chấm: **chuẩn = `PurchaseEvaluationWorkspacePage.tsx` (GĐ1 golive)**. Chỉ lệch khi có lý do viết ra được; lệch không lý do ⇒ kéo về chuẩn. Mọi patch áp **CẢ `fe-admin/` và `fe-user/`** rồi verify SHA256 == (§3bis-f).
### H-1 🔴 MAJOR — Grid cột sai chuẩn (`KhkkCreatePage.tsx:179`)
- Hiện: `lg:grid-cols-[300px_1fr] xl:grid-cols-[340px_1fr]`
- Đích : `lg:grid-cols-[260px_1fr] xl:grid-cols-[320px_1fr]` (byte-identical `PurchaseEvaluationWorkspacePage.tsx:80`)
- Vì : (i) hiến-pháp owner "giống Duyệt NCC" — file anh em `KhkkListPage.tsx:9` đã áp đúng luật này cho 400px; (ii) §3bis-b: đang lấy 40px của **cột form** đưa cho **cột danh sách**, ngược nhu cầu.
- Kèm : sửa luôn comment `:6` (`lg:grid-cols-[300px_1fr]`) — comment đang mô tả số cũ, để lại là bẫy cho lượt sau.
- Rủi ro: panel-1 hẹp đi 40px ⇒ dòng PE trong `Tier` có thể wrap. Kiểm bằng mắt tầng-1 ở 1024 & 1366; nếu wrap thì **KHÔNG nới grid** mà rút gọn nội dung dòng (nguyên tắc: giữ khuôn host, đổi nội dung panel).
### H-2 🔴 MAJOR — `<lg` là ngõ cụt, không tạo được phiếu (`KhkkCreatePage.tsx:187`)
- Hiện: `<main className="hidden … lg:block">`, không có nhánh nào cho `<lg`.
- Đích : theo khuôn **`KhkkListPage.tsx:102-109`** (cùng module, đã chạy): trong `pickPe`, nếu `!window.matchMedia('(min-width: 1024px)').matches``navigate('/khkk/create-mobile'…)` KHÔNG có ⇒ **cách rẻ + đúng nhất: bỏ `hidden … lg:block` cho `<main>`, để nó xếp DƯỚI panel-1 ở `<lg`** (`grid-cols-1` sẵn có ⇒ 2 hàng), và thêm `lg:` cho panel-1 nếu cần giới hạn chiều cao.
- ⚠️ **KHÔNG mirror dead-code `PurchaseEvaluationWorkspacePage.tsx:135-144`** — nó là nợ, không phải chuẩn.
- Vì : nút submit nằm trong `<main>` ⇒ hiện tại `<1024px` = không dùng được màn. Đây là lỗi **chức năng**.
- Cách rẻ hơn nếu owner không muốn đụng layout mobile: giữ `hidden lg:block` NHƯNG thêm khối `lg:hidden` nói thẳng *"Màn Thao tác cần màn hình ≥1024px — mở mục Danh sách để xem/tạo"* + nút navigate `/khkk/list`. Thà nói thật còn hơn im lặng.
### H-3 🟡 MEDIUM — Thiếu badge "Giai đoạn 2" ở header màn Thao-tác (`KhkkCreatePage.tsx:169-177`)
- Hiện: chỉ icon + title.
- Đích : chèn sau `<h1>` đúng span của `KhkkListPage.tsx:128-130`:
`<span className="ml-2 truncate rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">Giai đoạn 2{presetGroup !== null ? ` · ${khkkGroupMenuLabel(presetGroup)}` : ''}</span>`
- Vì : 2 màn CÙNG module hiện KHÁC nhau. Chuẩn PE cũng có badge ở header list (`PurchaseEvaluationsListPage.tsx:386`, nội dung là **số lượng**) ⇒ badge-ở-header là ngôn ngữ chung của họ; GĐ2 đã chọn nghĩa "giai đoạn/nhóm" thì phải nhất quán 2 màn.
- Kèm : nếu chèn badge thì title đã có `khkkGroupMenuLabel` (:164) ⇒ **bỏ phần `· <nhãn nhóm>` khỏi title** để không lặp 2 lần cùng một chữ trên 1 dòng (title về `Kế hoạch ký kết HĐ — Thao tác`, nhãn nhóm sống trong badge — đúng như màn List).
### H-4 🟡 MEDIUM — `readOnly` ép cứng, không do host tính (`KhkkCreatePage.tsx:210`)
- Hiện: `<KhkkDetailContent plan={viewPlan.data} readOnly … />` — hằng số.
- Đích : dùng CÙNG công thức host của `KhkkListPage.tsx:112-117`
(`!plan || !(phase ∈ {DangSoanThao, TraLai}) || plan.drafterUserId !== user.id`), tức import `useAuth` như file anh em.
- Vì : (i) trục "cách truyền readOnly" đang có **2 luật khác nhau cho cùng 1 component** ⇒ người tạo xong phiếu ở màn Thao-tác không sửa được ngay, phải bấm "Mở trang đầy đủ" (:196) — thao tác thừa do host, không do quyền; (ii) `[P2-7]` đã chốt "readOnly do HOST quyết".
- ⚠️ NẾU owner CỐ Ý muốn màn Thao-tác luôn chỉ-xem (banner :193 nói vậy) ⇒ **giữ nguyên code, nhưng ghi lý do vào comment ngay tại :210** — hiện chỗ này không có dòng nào giải thích, nên đọc như bug.
### H-5 🟡 MEDIUM — Comment sai sự thật (`KhkkCreatePage.tsx:7`)
- Hiện: *"Panel 1 = danh sách (`KhkkWorkListPanel`) + nút hành-động dính đáy"*. Thực tế `grep -c Button KhkkWorkListPanel.tsx` = **0** (đối chứng dương: `PeListPanel.tsx` = 8, có sticky bottom :238-241).
- Đích : hoặc (a) xoá vế "+ nút hành-động dính đáy", hoặc (b) **thêm thật** nút dính đáy panel-1 theo khuôn `PeListPanel.tsx:238-241` (`<div className="border-t border-slate-200 bg-white p-2">`) — hợp lý nếu muốn có lối "Làm mới danh sách" / "Về form tạo" khi đang xem phiếu ở `<lg`.
- Vì : comment là hợp-đồng cho lượt sau; sai comment ⇒ lượt sau tin có nút, không kiểm.
### H-6 🟢 MINOR — Glyph thay icon (`KhkkCreatePage.tsx:324, 357, 360`)
- Hiện: `⚠ Chưa có quy trình mã…` (:324) · `⚠ Chưa đủ để tạo kế hoạch…` (:357) · `✓ Đủ thông tin bắt buộc…` (:360).
- Đích : `lucide-react``<AlertTriangle className="mr-1 inline h-3.5 w-3.5" />` / `<Check … />`, hoặc bỏ hẳn glyph và để màu chữ (`text-red-600` / `text-emerald-600`) mang nghĩa.
- Vì : FD3 anti-slop (glyph/emoji thay icon-set); cả họ file dùng lucide (`KhkkListPage:30` `FileCheck, Plus, X`; `PurchaseEvaluationsListPage:10` 5 icon). Glyph `⚠` còn render lệch baseline giữa font Be Vietnam Pro và fallback emoji-font.
### H-7 🟢 MINOR — Title `truncate` + `min-w-0`: GIỮ, và **nâng ngược lên PE**
- `KhkkCreatePage:170-172` + `KhkkListPage:125-127``min-w-0` + `truncate`; PE (`:71,73`, `PurchaseEvaluationsListPage:383,385`) KHÔNG.
- Verdict: **KHKK đúng hơn chuẩn** (title GĐ2 dài do nhãn nhóm). ⇒ **KHÔNG hạ KHKK xuống**; đề xuất RIÊNG (ngoài lượt này, cần owner đồng ý vì chạm file golive): thêm `min-w-0`+`truncate` cho 2 file PE.
### H-8 🟢 MINOR — Màu violet của banner "đang xem" (`KhkkCreatePage.tsx:191-204, 288`) — **ĐÃ ĐO, KHÔNG SỬA**
- Hiện: `border-violet-200 bg-violet-50 text-violet-800` (:191-192) + pill nhóm `border-violet-200 bg-violet-50 text-violet-700` (:288).
- Verdict bố-cục: **giữ** — host GĐ1 không có trạng-thái "đang xem phiếu" nên không có chuẩn để đối chiếu; violet tách "chế-độ xem" khỏi brand (hành-động) và amber (cảnh báo). Chốt 1 luật cho GĐ2: **violet = chế-độ chỉ-xem**.
- 🔬 **Tự bác bỏ giả-thuyết của chính tao (đo trước khi khuyên):** tao vào lượt này mang sẵn luật trong MEMORY *"dải accent chỉ có stop 50/100/500/600/700, dùng -800 = silent no-class"*. **ĐO LẠI: SAI cho `violet`.**
- `fe-admin/src/index.css:5` `@theme {`**KHÔNG có** directive reset (`grep -n "initial" index.css`**0 hit**) ⇒ dải Tailwind mặc định **vẫn còn**, `@theme` chỉ **ghi đè 5 stop** (`:41-45` violet 50/100/500/600/700).
-`violet-200`/`violet-800` **VẪN RA MÀU** — nhưng là màu của dải **mặc định Tailwind** (tím `#ddd6fe`/`#5b21b6`), khác hệ với dải tự-định-nghĩa (xanh-tím `#f1effe`/`#5042c4`). Đây là **trộn 2 hệ màu trong 1 khối**, KHÔNG phải mất-class.
- Quy mô: `grep -rno "violet-[0-9]+" fe-admin/src``violet-700`×45 · `violet-50`×38 · **`violet-200`×13** · `violet-100`×12 · **`violet-800`×11** · `violet-500`×11 · `violet-600`×7 · `violet-300`×4 · `violet-400`×1, nằm ở **12 file** — trong đó có **file GĐ1 golive** (`components/pe/PeDetailTabs.tsx`, `PeWorkflowPanel.tsx`, `PeFinalizeChip.tsx`).
-**TOÀN HỆ THỐNG, không phải lệch của KHKK.** Sửa riêng `KhkkCreatePage` = tạo lệch mới với chính GĐ1. **KHÔNG đưa vào lượt sửa này**; nếu owner muốn dọn thì mở việc riêng "chuẩn hoá dải violet 2-hệ" quét cả 12 file × 2 app.
- 🔴 Luật ĐÚNG (thay luật cũ trong MEMORY): tên màu **CÓ trong Tailwind mặc định** (`teal`, `violet`) mà chỉ định-nghĩa-lại vài stop ⇒ stop ngoài dải **im lặng rơi về hệ màu mặc định** (nguy hơn: trông vẫn "có màu"). Tên **BỊA** (`amberx`, `greenx`) ⇒ stop ngoài dải **không tồn tại class** (mất style hẳn). Hai ca KHÁC NHAU, trước nay tao gộp làm một.
### H-9 ⚪ INFO — Nút "Lập kế hoạch" ở header màn List (`KhkkListPage.tsx:132-137`) — **KHÔNG SỬA**
- Chuẩn `PurchaseEvaluationsListPage.tsx:382-390`: header **chỉ có nhóm trái** (icon + title + badge-số), **bên phải TRỐNG** — GĐ1 tạo phiếu bằng sticky "+ Thêm mới" trong `PeListPanel:238-241` ở màn Thao-tác.
- GĐ2 đảo lại: nút tạo nằm ở header màn **List**, còn panel-1 màn **Thao-tác** KHÔNG có nút nào (§3bis-c).
- Verdict: **giữ nút ở List** (gỡ đi thì GĐ2 mất lối vào duy nhất nhìn thấy được), nhưng nó chính là lý do **H-5 phải chọn nhánh (a) sửa comment** — đừng thêm nút vào panel-1 chỉ để "giống PE", vì lối tạo của GĐ2 đã có chỗ đứng khác và đang hoạt động.
### Thứ tự thi hành đề nghị
`H-2` (hỏng chức năng) → `H-1` (số đo, 1 dòng) → `H-3`+`H-7`-giữ (header) → `H-4` (quyết định owner: sửa hay ghi lý do) → `H-5`, `H-6` (dọn).
Sau mỗi nhóm: `cd fe-admin && npm run build` + `cd fe-user && npm run build` + `sha256sum` 2 bản == nhau.
### KHÔNG SỬA (chốt để lượt sau khỏi "sửa cho giống")
- `h-[calc(100vh-4rem)]` hụt ~3px — lỗi **toàn hệ thống**, sửa lẻ 1 file = tạo lệch mới (§3bis-d).
- Dead-code `lg:hidden` của PE (:135-144) — **đừng mirror**.
- Thiếu nhánh "Không tìm thấy phiếu" của PE — KHKK đã tốt hơn (`KhkkCreatePage:207`, `KhkkListPage:174`), giữ.
- Grid 3-panel `KhkkListPage:140`**đã khớp PE byte-for-byte**, không đụng.
## 5. TỰ ĐÁNH GIÁ + GIỚI HẠN
### 5.1 🔴 KHÔNG CÓ SCREENSHOT — khai thẳng, không bịa (khuôn NO-SCREENSHOT S159)
Vòng FD2 (run → shoot → critique) **không chạy được** trong lượt này:
- Probe cổng: `netstat`**chỉ `0.0.0.0:8080` LISTENING** (pid 5912). **`5443`/`5444` (API) KHÔNG có** ⇒ đăng nhập fail.
- `/khkk/create``/khkk/list` đều sau `ProtectedRoute` ⇒ không token thì kẹt `/login` (gotcha #3 đã ghi trong MEMORY: dev-rig chặn authed page).
- Dựng harness render-isolated **cố ý KHÔNG làm**: mandate ISOLATION của run này chỉ cho ghi `sub-frontend-designer-L3.md`; harness = file mới trong `fe-user/`/`fe-admin/` ⇒ vi phạm git-diff audit.
⇒ Mọi kết luận dưới đây là **đo tĩnh trên mã nguồn + số học layout**, KHÔNG phải quan sát bằng mắt. Ba mục cần **nghiệm thu bằng mắt trên prod sau khi sửa**: H-1 (panel-1 260px có wrap dòng PE không), H-2 (`<lg` sau khi mở `<main>`), H-3 (badge + title không tràn ở 1366).
### 5.2 Độ chắc của từng kết luận
| Mục | Loại bằng chứng | Chắc |
|---|---|---|
| H-1 grid | so chuỗi class + khảo sát 6 host + số học px | **CAO** — đọc được từ mã, không cần chạy |
| H-2 `<lg` | `hidden … lg:block` + vị trí nút submit trong DOM | **CAO** về mặt suy luận DOM · **CHƯA nhìn bằng mắt** |
| H-3 badge | so 2 file cùng module | CAO |
| H-4 readOnly | hằng số vs công thức host | CAO (còn phụ thuộc **ý owner**, đã nêu 2 nhánh) |
| H-5 comment sai | `grep -c Button` = 0 (đối chứng dương PeListPanel = 8) | **CAO** |
| H-6 glyph | 3 vị trí literal | CAO |
| H-7 truncate | so 4 file | CAO |
| H-8 violet | đo `index.css` + đếm toàn repo — **bác giả-thuyết ban đầu của chính tao** | CAO |
### 5.3 Ngoài lăng-kính này (lane khác lo)
Nội dung *bên trong* panel (`KhkkWorkListPanel` 2 tầng, `KhkkDetailContent`, `KhkkListPanel` cây toàn-trình), micro-copy, trạng thái loading/empty của từng tầng, a11y focus-order, typography scale — **KHÔNG chấm ở lane HOST/BỐ-CỤC**. Tao chỉ chạm chúng ở mức "root className có khớp hợp-đồng grid của host không".
### 5.4 Đã KHÔNG làm (giữ lane)
Không sửa 1 dòng code nào (lane audit). Không đụng `agent-memory/*`, không đụng MD canonical, không đụng sub-MD lane khác. `git status --porcelain -- fe-admin/src fe-user/src` đo được là **rỗng** trước khi tao bắt đầu và tao không ghi gì vào đó.
END sub-frontend-designer-L3 — VERDICT=KHỚP-MỘT-PHẦN: 4 trục khớp chuẩn (shell/header-class/panel-2/EmptyState), 2 lệch nặng (grid 300→260 · `<lg` ngõ cụt không tạo được phiếu), 3 lệch vừa (badge GĐ2 · readOnly ép cứng · comment sai sự thật), 2 minor (glyph thay icon · truncate — giữ vì tốt hơn chuẩn), 3 mục CHỐT-KHÔNG-SỬA (h-calc toàn hệ · violet 2-hệ toàn hệ · nút tạo ở header List). KHÔNG có screenshot — đã khai lý do đo được.