263 lines
30 KiB
Markdown
263 lines
30 KiB
Markdown
# 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 “Duyệt”." | :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` CÓ `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` là **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) | 1024−288 = **736** | 260 | **476** | 300 | **436** | **−40px** |
|
||
| 1280 (xl) | 1280−320 = **960** | 320 | **640** | 340 | **620** | **−20px** |
|
||
| 1366 (laptop SE) | 1366−320 = **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:` là **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` có `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` có `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` và `/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.
|