wal: flush 20260805T1614

This commit is contained in:
pqhuy1987
2026-08-05 16:14:27 +07:00
parent 05210835e8
commit df21a33912
6 changed files with 565 additions and 42 deletions

View File

@ -25,6 +25,11 @@ mỗi lệch phải có `file:line` CẢ HAI phía; đầu ra = spec thi-hành-
| O4 | Shopdrawing / căn cứ hồ sơ cho **đính kèm file** | chưa làm |
| O5 | Menu Đang duyệt / Đã duyệt / Đã xóa "lọc đúng trạng thái" — lead đã truy 5/5 mắt xích, **chuỗi lọc ĐÚNG**; nghi vấn thật = `KhkkListPanel:202 flatMode` đổi CÂY→PHẲNG khi có filter | cần phán |
| O6 | **Bấm card PE trong cây ở màn KHKK phải GIỮ NGUYÊN màn KHKK** (view tại chỗ), không văng sang phiếu gốc — "tương tự với duyệt NCC" | chưa làm |
| O7 | **Nút EDIT trên card KHKK** ở tầng dưới panel Thao tác — mirror `PeListPanel` `onEditClick` (bút chì hiện khi hover, bấm = mở thẳng chế-độ sửa) | chưa làm |
| O8 | **Tầng TRÊN panel Thao tác cho ra CÂY THƯ MỤC Dự án Hạng mục** — "cho cây thư mục dự án hạng mục ra luôn nhé". Hiện mới gom-theo-dự-án 1 cấp (`KhkkWorkListPanel` `byProject`), owner muốn cây 2 cấp như cây toàn trình | chưa làm |
> 🔴 **O7/O8 đến SAU khi 5 lane đã phóng** (`wf_016b1bed-b85`) ⇒ lane KHÔNG thấy 2 mục này.
> Lead PHẢI tự phủ chúng ở bước synthesis; đừng đọc "lane không nhắc" thành "không có việc".
## Đã land trong phiên (đừng đề xuất lại)

View File

@ -23,44 +23,169 @@
## 1. Bản đồ file — LOC + cấu trúc block
- PE `fe-admin/src/components/pe/PeWorkflowPanel.tsx` = **1062 dòng**
- KHKK `fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx` = **726 dòng**
### 1.1 PE panel (CHUẨN)
_(chờ đọc)_
| Khối | Dòng | Ghi chú |
|---|---|---|
| Root wrapper | `:345` `<div className="space-y-4">` | **KHÔNG card/section/accent** — panel trần |
| Header "Quy trình duyệt" | `:346-354` | `<h3 text-sm font-semibold text-slate-900>` + meta `{code} v{NN} · {name}` (mono 11px, `padStart(2,'0')`) — **KHÔNG icon-chip, KHÔNG màu** |
| Banner kết-thúc-sớm | `:360-377` | `endsBeforeCeo`**emerald** 300/50/800, 2 nhánh DaDuyet ✅ / đang chạy ⚑ |
| Sơ đồ Bước→Cấp→NV | `:381-478` | `<ol>` step box + `<ul ml-7 border-l-2 border-violet-200>` cấp |
| — chip phòng | `:406-410` | `bg-emerald-50 text-emerald-700` **KHÔNG viền** |
| — badge "Kết thúc tại X" | `:411-418` | slate-100/400, gắn ở **Bước** bị mờ |
| — badge ⚑ Duyệt thay CEO | `:441-448` | violet-100/800 9px |
| — chip ✎ NS PRO / ✎ NS CCM | `:458-463` | amber-100/700 · sky-100/700, 8px, cạnh tên NV |
| Fallback V1 | `:481-485` | KHKK cố ý không port (R-8) |
| Banner "đến lượt bạn" | `:489-508` | emerald/amber |
| Banner phiếu-trước-chưa-duyệt | `:512-516` | amber (PE-only, S134) |
| Khối HÀNH ĐỘNG | `:518-581` | `<Label className="text-xs">Hành động:</Label>` + `flex flex-wrap gap-1.5` |
| — nút Duyệt/Trả lại/Từ chối | `:546-563` | **label TEXT `'✓ Duyệt'` / `'← Trả lại'` / `'✗ Từ chối'`, KHÔNG lucide icon** |
| — nút Xóa phiếu | `:569-578` | `<Trash2>` + `border-red-300 bg-white text-red-700` |
| readOnly hint | `:582-586` | "Vào menu Duyệt để chuyển phase" |
| Dialog transition | `:588-873` | 4 return-mode radio + skipToFinal + finalizeByCcm + applyLevelFinalize + **price-picker** + file-picker |
| Dialog xóa | `:879-918` | `size="sm"`, `variant="danger"`, 4 bullet hệ-quả + ô lý do |
| **2-cấp phòng ban** | `:920-924``:983-1052` | `DeptApprovalsSection` (PE-only) |
| **📎 File đính kèm khi duyệt** | `:928-967` | `border-t pt-4` + `<h3 text-sm font-semibold text-slate-900>` + Eye preview + Download |
| **Lịch sử duyệt (n)** | `:969-971` | delegate `PeApprovalsSection` (PeDetailTabs) |
| **Lịch sử thay đổi** | `:973-975` | delegate `PeHistorySection` (PeDetailTabs) |
### 1.2 KHKK panel (ĐỐI TƯỢNG)
_(chờ đọc)_
| Khối | Dòng | Ghi chú |
|---|---|---|
| Root wrapper | `:247` `<section className="card-accent" --accent: emerald-500>` | **CÓ card + rail accent** ⟂ PE trần |
| Header | `:249-265` | `<header border-b px-5 py-3>` + `icon-chip` `<Workflow>` + `<h3 text-emerald-700>`**màu + icon ⟂ PE slate-900 trần**; version `v${n}` **không padStart** |
| Body | `:267` | `<div className="space-y-4 p-5">` |
| Banner kết-thúc-sớm | `:269-281` | **violet** 300/50/800 ⟂ PE **emerald** |
| Sơ đồ Bước→Cấp→NV | `:284-366` | cùng khuôn `STATUS_BOX`/`STATUS_DOT`/`STATUS_ICON` = **khớp PE** |
| — chip phòng | `:298-302` | `border-slate-300 bg-white text-slate-600` ⟂ PE `bg-emerald-50 text-emerald-700` |
| — badge ⚑ Duyệt thay CEO | `:327-334` | **khớp PE byte-level** (violet-100/800) |
| — NV + chữ ký | `:338-355` | KHKK **thêm** `✓ {thời điểm}` + in-đậm-người-đã-ký (PE không có) |
| — "không phải qua" | `:326` | KHKK-only (PE dùng badge "Kết thúc tại X" ở Bước) |
| Banner "đến lượt bạn" | `:375-390` | khớp PE |
| Banner TraLai / TuChoi | `:392-401` | KHKK-only (orange/red) |
| Khối HÀNH ĐỘNG | `:404-481` | cùng `<Label>Hành động:</Label>` |
| — nút Gửi duyệt | `:408-416` | KHKK-only (`<Send>` brand) |
| — Duyệt/Trả lại/Từ chối | `:422-469` | **lucide `<Check>/<Undo2>/<X>`** ⟂ PE text-glyph |
| — Xóa phiếu | `:470-478` | `border-slate-300 text-slate-600` hover→đỏ ⟂ PE đỏ-sẵn |
| **📎 File đính kèm khi duyệt** | `:484-510` | **KHÔNG `border-t`**, tiêu đề `text-xs` + `<Paperclip teal>` + pill đếm; `<a href>` "Tải xuống" — **KHÔNG có preview** |
| **Lịch sử duyệt (N)** | `:516-556` | inline, `text-xs` + `<Check emerald>` + pill đếm |
| **Lịch sử thay đổi** | `:559-601` | inline, **collapse lazy** (`showChangelog`) ⟂ PE mở sẵn |
| Dialog transition | `:604-704` | 1 banner/action + applyLevelFinalize + file-picker; **KHÔNG price-picker, KHÔNG 4 return-mode** |
| Dialog xóa | `:706-723` | 1 câu, `variant="outline"`+Button mặc-định ⟂ PE `variant="danger"` |
---
## 2. Kê TỪNG hạng mục owner nêu — PE có / KHKK thiếu-khác
### H1. Banner trạng-thái tóm tắt (phiếu kết thúc ở đâu)
_(chờ)_
> Ký hiệu: **≡** = khớp · **≠** = CÓ nhưng style lệch · **∅** = KHKK THIẾU HẲN · **⊘** = không có dữ-liệu
### H2. Bước/Cấp + icon TRÒN check xanh
_(chờ)_
### H0 (owner không nêu nhưng LỆCH NẶNG NHẤT). Vỏ panel + header — ≠
| | PE (chuẩn) | KHKK |
|---|---|---|
| Vỏ | `PeWorkflowPanel.tsx:345` `<div className="space-y-4">`**panel TRẦN** | `KhkkWorkflowPanel.tsx:247` `<section className="card-accent" style={{'--accent':'var(--color-emerald-500)'}}>`**card viền + rail emerald 3px + shadow + hover translateY(-1px)** (`fe-admin/src/index.css:112-124`) |
| Host | `PurchaseEvaluationsListPage.tsx:668` `<aside … bg-white p-4>` | `KhkkListPage.tsx:186` `<aside … bg-white p-4>`**GIỐNG HỆT** |
| Header | `:346-354` `<h3 text-sm font-semibold text-slate-900>Quy trình duyệt</h3>` | `:249-257` `<header border-b px-5 py-3>` + `.icon-chip` `<Workflow>` emerald + `<h3 … text-emerald-700>` |
| Padding thân | không (aside p-4 lo) | `:267` `p-5`**tổng 4+20 = 36px mép** |
| Meta workflow | `:349-352` `{code} v{padStart(2,'0')}` + `· {name}` | `:258-264` `{code} v{version}`**không pad `v01`→ ra `v1`** |
### H3. Chip "NS PRO" / "NS CCM" cạnh tên người
_(chờ)_
**Kết:** cùng đặt trong 1 aside trắng `p-4`, PE là panel phẳng còn KHKK là **thẻ-trong-thẻ** (viền + bóng + rail xanh + hover nhấc). Đây là thứ đập vào mắt trước mọi hạng mục owner liệt kê. **Bê thẳng được** — 0 dữ-liệu liên quan.
### H4. Badge "Duyệt thay CEO"
_(chờ)_
### H1. Banner trạng-thái tóm tắt (phiếu kết thúc ở đâu) — ≠ (màu) + ∅ (nhánh báo-trước)
- PE `PeWorkflowPanel.tsx:360-377` — gate `evaluation.endsBeforeCeo`, khung **emerald** `border-emerald-300 bg-emerald-50 text-emerald-800`, 2 nhánh theo phase: `DaDuyet` → "✅ Phiếu đã kết thúc tại **{finalizeStepName}** · {finalizeLevelName} — không qua CEO." / đang chạy → "⚑ Quy trình rút gọn: duyệt đến **{…}** là KẾT THÚC, không trình CEO. Các cấp trước vẫn duyệt như thường."
- KHKK `KhkkWorkflowPanel.tsx:269-281` — gate `plan.endedByLevelFinalize`, khung **violet** `border-violet-300 bg-violet-50 text-violet-800`, 2 nhánh theo `isApproved`.
-**màu lệch emerald ⟂ violet** (KHKK dùng violet = màu PE dành riêng cho badge "Duyệt thay CEO" `:441-448` ⇒ KHKK trộn 2 nghĩa vào 1 màu).
- ∅/⊘ **nhánh "báo trước"**: xem §4.B — phụ thuộc ngữ nghĩa cờ BE (đo ở dưới).
### H5. Khối HÀNH ĐỘNG (Duyệt / Trả lại)
_(chờ)_
### H2. Bước/Cấp + icon TRÒN check xanh — ≡ (gần như khớp tuyệt đối)
- PE `:381-478` (`stepIcon` `:384`, dot `:397-404`, cấp `:423-434`) ⟂ KHKK `:284-366` (`STATUS_DOT/BOX/ICON` `:64-78`, dot `:293-295`, cấp `:318-320`).
- Cùng `✓/●/○`, cùng `bg-emerald-500 / bg-brand-600 / bg-slate-200`, cùng `h-5 w-5` (Bước) + `h-4 w-4 text-[9px]` (Cấp), cùng `<ul ml-7 border-l-2 border-violet-200 pl-2>`, cùng nhãn phụ "đang chờ" (brand-700) / "đã duyệt" (emerald-600). **KHÔNG cần sửa.**
-**chip PHÒNG**: PE `:406-410` `rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700` (nền xanh, không viền) ⟂ KHKK `:298-302` `rounded border border-slate-300 bg-white … text-slate-600` (viền xám, nền trắng). **Bê thẳng được.**
-**làm mờ nhánh không chạy**: PE mờ theo cấu-hình `isStepAfterFinalize/isLevelAfterFinalize` `:165-169` + badge "Kết thúc tại {X}" gắn ở **Bước** `:411-418` (slate-100/400) ⟂ KHKK mờ theo **thực-tế đã-ký** `skipped` `:313` + chữ "không phải qua" ở **Cấp** `:326`. Hai cách khác nhau về chất, xem §4.B.
- **KHKK CÓ THÊM** (PE không có): mốc chữ-ký `✓ {dd/mm/yyyy hh:mm}` cạnh tên người đã ký + in-đậm người ký ⟂ làm-mờ người chưa ký `:338-355`. Đây là **cải tiến**, không phải lệch — xem §3.
### H6. "File đính kèm khi duyệt"
_(chờ)_
### H3. Chip "NS PRO" / "NS CCM" cạnh tên người — ∅ / ⊘ KHÔNG CÓ DỮ-LIỆU
- PE `:457-463`: `a.canEditProBudget``bg-amber-100 text-amber-700 text-[8px]` "✎ NS PRO"; `a.canEditCcmBudget``bg-sky-100 text-sky-700` "✎ NS CCM".
- KHKK: **không có, và KHÔNG bê được** — xem §4.B (field DTO + khái niệm ngân-sách per-gói-thầu là món riêng của PE, Mig 50). Chính file KHKK đã khai chủ đích ở `:29`.
### H7. "Lịch sử duyệt (n)" — badge Duyệt/Trả lại + timestamp
_(chờ)_
### H4. Badge "Duyệt thay CEO" — ≡ KHỚP BYTE-LEVEL
- PE `:441-448` ⟂ KHKK `:327-334`: cùng `ml-1.5 inline-flex items-center rounded bg-violet-100 px-1.5 py-0.5 text-[9px] font-semibold text-violet-800`, cùng chữ "⚑ Duyệt thay CEO", cùng `title` y hệt. **KHÔNG cần sửa.**
- Khác nguồn: PE đọc `lv.allowApproverFinalize` (1 cấp) ⟂ KHKK `group.some(l => l.allowApproverFinalize)` (gom OR-of-N) — khác *cách lấy*, không khác *hình*.
### H8. "Lịch sử thay đổi"
_(chờ)_
### H5. Khối HÀNH ĐỘNG (Duyệt / Trả lại) — ≠ (icon + số nút)
| | PE `:518-581` | KHKK `:404-481` |
|---|---|---|
| Nhãn khối | `<Label className="text-xs">Hành động:</Label>` `:520` | y hệt `:406` **≡** |
| Hàng nút | `mt-1 flex flex-wrap gap-1.5` `:521` | y hệt `:407` **≡** |
| Duyệt | text `'✓ Duyệt'` `:540`, `border-emerald-300 text-emerald-700 hover:bg-emerald-50` | `<Check className="h-3 w-3"/> Duyệt` `:442`, **cùng bảng màu** |
| Trả lại | text `'← Trả lại'`, `border-amber-300 text-amber-700` | `<Undo2/>` `:454`, cùng màu |
| Từ chối | text `'✗ Từ chối'`**BE đã gỡ khỏi `nextPhases`, FE filter `:317` ⇒ THỰC TẾ KHÔNG HIỆN** | `<X/> Từ chối` `:456-467`**VẪN HIỆN** |
| Gửi duyệt | ∅ (PE gửi duyệt từ nơi khác) | `<Send/>` `:408-416` brand — KHKK-only |
| Xóa phiếu | `<Trash2/> Xóa phiếu` `:569-578` `border-red-300 bg-white text-red-700` | `:470-478` `border-slate-300 text-slate-600` → hover mới đỏ |
| Class nút | `rounded border px-2 py-1 text-[11px] font-bold` `:553` | `inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold` `:438` |
- 🔴 Lệch thấy được: (a) PE dùng **glyph trong chuỗi** `✓ / ← / ✗`, KHKK dùng **icon lucide** `Check/Undo2/X`; (b) KHKK **thừa nút "Từ chối"** mà màn PE của owner không còn; (c) nút Xóa của KHKK xám-nhạt còn PE đỏ.
### H6. "File đính kèm khi duyệt" — ≠ (nặng)
| | PE `:928-967` | KHKK `:484-510` |
|---|---|---|
| Ngăn cách | `<div className="border-t border-slate-200 pt-4">` `:929` | **không có `border-t`** — trôi trong `space-y-4` |
| Tiêu đề | `<h3 className="text-sm font-semibold text-slate-900">📎 File đính kèm khi duyệt</h3>` `:930` | `text-xs font-semibold text-slate-700` + `<Paperclip className="… text-teal-500">` + **pill đếm** `:486-492` |
| Phụ đề | `:931` "File do người duyệt tải lên trong quá trình duyệt." | ∅ |
| Dòng file | `flex … rounded bg-slate-50 px-1.5 py-1 text-[11px]` + `<Paperclip slate-400>` + tên truncate + **kích-thước** `:934-937` | `rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5` + tên + `{size} · {thời điểm}` `:495-499` |
| Xem trước | ✅ `<Eye/>` violet + `AttachmentPreviewDialog` cho pdf/png/jpg/webp `:938-946`, `:957-965` | ∅ **THIẾU** |
| Tải xuống | `<Download/>` brand, đi qua `api.get(responseType:'blob')` **có JWT** `:327-342`, `:947-953` | `<a href="/api/…/download">` `:500-505`**thẻ `<a>` trần** |
| 🔴 | | `<a>` trần = **rủi ro chức năng**, không chỉ style: tải file không kèm Authorization header của axios. Xem §6. |
### H7. "Lịch sử duyệt (n)" — ≠ (đậm/nhạt + vị trí timestamp)
| | PE `:969-971``PeDetailTabs.tsx:778-785` + `:3109-3219` | KHKK `:516-556` |
|---|---|---|
| Ngăn cách | `border-t border-slate-200 pt-4` `:970` | ∅ |
| Tiêu đề | `<h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử duyệt ({n})</h3>` `PeDetailTabs.tsx:781`**(n) trong ngoặc, không icon, không pill** | `text-xs font-semibold text-slate-700` + `<Check text-emerald-500>` + **pill** `bg-slate-100 rounded-full` `:517-523` |
| Rỗng | `<p className="text-sm text-slate-500">Chưa có bước duyệt nào.</p>` `:3194` | `text-[11px] italic text-slate-400` "Chưa có lượt duyệt nào." `:525` |
| Thẻ entry | `rounded border border-slate-200 bg-white p-3 text-sm` `:3201` | `rounded border border-slate-200 bg-slate-50/60 px-2.5 py-1.5 text-[11px]` `:529` |
| Badge | `decisionBadge(decision, toPhase)``rounded px-1.5 py-0.5 text-[11px] font-medium` `:3204` | `rounded px-1.5 py-0.5 text-[9px] font-semibold` `:531-536`, nhãn từ `DECISION_LABEL` `:80-85` |
| Timestamp | **đẩy phải** `justify-between` + `text-xs text-slate-500` `:3202`,`:3209` | **inline trái**, sau tên người, `text-slate-500` `:538-539` |
| Dòng 2 | `{tên} · {ý kiến}` `text-xs text-slate-500` `:3211-3213` | `Bước n — {tên} · Cấp n ({tên}) · {từ}→{đến}` `:541-548` rồi ý-kiến ở dòng 3 `:549-551` |
- Lệch: PE = thẻ **trắng, chữ `text-sm`, mốc-giờ canh phải**; KHKK = thẻ **xám, chữ 11px, mốc-giờ chen giữa dòng**. Đứng cạnh nhau nhìn ra ngay là 2 hệ.
### H8. "Lịch sử thay đổi" — ≠ (mở sẵn ⟂ gập)
- PE `:973-975``PeDetailTabs.tsx:787-794`: `border-t pt-4` + `<h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử thay đổi</h3>` + `HistoryTab` **render NGAY**, dữ-liệu đã fetch sẵn (`['pe-changelog', id]`, `:3223-3226`).
- KHKK `:559-601`: **nút gập** `<button … w-full>` + `<History text-slate-400>` + `text-xs text-slate-700` + chữ "Xem/Ẩn" canh phải `:567`; query `enabled: showChangelog` `:186`**mặc định đóng, phải bấm mới tải**.
- Lệch hành-vi *thấy được*: owner mở phiếu PE thấy ngay lịch sử; mở KHKK thấy 1 dòng "Lịch sử thay đổi … Xem". **Bê thẳng được** (bỏ gập) — nhưng xem §6 caveat tải-dữ-liệu.
---
## 3. Gap phụ (KHKK có mà PE không / style lệch ngoài 8 hạng mục)
_(chờ)_
## 3. Gap phụ (ngoài 8 hạng mục owner nêu)
### 3.1 KHKK CÓ mà PE KHÔNG — giữ hay bỏ?
| Thứ | KHKK | Đề nghị |
|---|---|---|
| Nút "Gửi duyệt" | `:408-416` `<Send>` brand | **GIỮ** — PE gửi duyệt ở màn khác, KHKK gộp vào panel là đúng luồng |
| Banner TraLai / TuChoi | `:392-401` orange / red | **GIỮ** — PE không có nhưng là thông tin thật, không phá style |
| Chữ ký `✓ {thời điểm}` + in-đậm người đã ký | `:338-355` | **GIỮ** — tốt hơn PE (`:450-465` chỉ liệt tên) |
| "không phải qua" ở Cấp bị bỏ | `:326` | GIỮ, nhưng xem §4.A-15 (PE gắn badge ở **Bước**) |
| Ép nhập ý-kiến khi Trả lại/Từ chối | `:242-244` `commentRequired` | **GIỮ** — chặt hơn PE (`:869` luôn tùy chọn) |
### 3.2 PE có, KHKK không — NGOÀI 8 hạng mục
| Thứ | PE | KHKK |
|---|---|---|
| "Tiến trình duyệt 2-cấp phòng ban" | `:920-924``:983-1052` | ⊘ không có bảng/endpoint tương ứng |
| Banner phiếu-trước-cùng-gói-thầu chưa duyệt | `:512-516` + nhắc lại trong dialog `:621-625` | ⊘ lũy-kế ngân sách là món PE |
| Bộ chọn giá chốt | `:794-820` | ⊘ KHKK chốt giá per-Line ở BE (`:28-29`) |
| 4 mode Trả lại (radio) | `:635-713` | ⊘ 1 mode (về Bước 1·Cấp 1) — đã khai `:30` |
| Ô-tích "Duyệt thẳng Cấp cuối" | `:725-748` | ⊘ |
| Ô-tích "Duyệt done, miễn CEO (CCM)" | `:751-768` | ⊘ |
| Gợi ý readOnly "Vào menu Duyệt…" | `:582-586` | ⊘ (KHKK không có prop readOnly) |
### 3.3 Lệch chi-tiết cấp-chữ (detail-level, vẫn là style)
| Điểm | PE | KHKK |
|---|---|---|
| Nút hủy dialog | `variant="ghost"`, chữ **"Hủy"** `:615` | `variant="outline"`, chữ **"Huỷ"** `:610`,`:712`*khác cả kiểu nút lẫn chính-tả* |
| Nhãn ô ghi chú | "Ghi chú (tùy chọn)" `:869` | "Ý kiến (tuỳ chọn)" `:648-650`*"tùy" ⟂ "tuỳ"* |
| Nút xác nhận | "Xác nhận" tĩnh `:616` | đổi chữ khi chạy "Đang gửi..." `:612` |
| Nút chọn file trong dialog | hộp **gạch đứt brand** `border-dashed border-brand-300 bg-brand-50/50` + `<Upload>` + "+ Chọn file đính kèm" `:841-847`; danh sách có **kích-thước + nút `<X>` bỏ từng file** `:848-866` | nút xám đặc `border-slate-300` + `<Paperclip>` + "Chọn file" `:675-681`; tên file **nối bằng dấu phẩy** + link chữ "bỏ chọn" `:682-689` |
| Dialog xóa | `size="sm"` + `variant="danger"` + **4 gạch đầu dòng hệ-quả** + ô lý do `:879-917` | 1 câu + nút mặc-định `:706-723` |
---

View File

@ -15,24 +15,164 @@
| B | `fe-admin/src/components/khkk/KhkkListPanel.tsx` | Đối chiếu (GĐ2) |
| C | `fe-admin/src/components/khkk/KhkkWorkListPanel.tsx` | Đối chiếu (GĐ2) |
_(pending — append khi đọc)_
Đã đọc đủ 3 file @lượt-2/3. LOC: A=264 · B=375 · C=305 (`wc -l`).
---
## 1. Inventory cấu trúc 3 file
_(pending)_
### A — `PeListPanel.tsx` (CHUẨN, 264 dòng)
Shell `<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">` (:92).
1. **Header filter block** `space-y-2 border-b border-slate-200 p-3` (:94)
- Hàng nhãn: `text-[11px] font-semibold uppercase tracking-wide text-slate-500` "Danh sách phiếu" + **pill đếm** `rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600` (:96-101)
- Ô tìm: `Search` icon absolute `left-2.5 top-2.5 h-4 w-4 text-slate-400` + `Input className="pl-8"` (:103-111)
- Ô lọc: `Select` "Tất cả trạng thái" + map `PeDisplayStatus` (:117-122); nhánh `editableOnly` thay Select bằng **banner lọc-cố-định** `rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px]` (:112-115)
2. **Body** `flex-1 overflow-y-auto` (:127)
- loading: 6 skeleton `h-16 animate-pulse rounded-md bg-slate-100` (:128-134) — ⚠️ KHÔNG có `motion-reduce:animate-none`
- empty: `<EmptyState icon={ClipboardCheck} title description>` bọc `p-6` (:135-147)
- **KHÔNG có nhánh `isError`** (:127-236 — 0 hit `isError`)
- list `<ul className="divide-y divide-slate-100">` (:148)
3. **Card phiếu** = `<li className="group relative">` + `<button className="block w-full px-3 py-2.5 pr-9 text-left transition hover:bg-slate-50">` + selected `bg-brand-50 ring-1 ring-inset ring-brand-200` (:150-156). ⚠️ KHÔNG có `focus-visible:*`.
- hàng 1: chips (`PeUrgentChips` + `PeFinalizeChip`) + tiêu đề `text-[13px] font-medium text-slate-900 truncate` ⟂ badge phải `shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium` (**rounded, KHÔNG border**) (:158-190)
- hàng 2: `mt-0.5 text-[11px] text-slate-500``font-mono` mã · `·` · projectName (:165-169)
- hàng 2b (điều kiện): NCC trúng `mt-0.5 truncate text-[11px] text-emerald-600` "✓ …" (:173-181)
- hàng 3: `mt-1 flex items-center justify-between text-[11px]` — chip loại `rounded bg-slate-100 px-1.5 py-0.5 text-slate-600``<SlaTimer>` (:192-197)
- hàng 4 (điều kiện): `mt-1 text-[10px] text-brand-600` "✓ Đã tạo HĐ" (:198-200)
- **pencil overlay** `absolute right-2 top-2 rounded p-1.5` 3-state bright/dim/active (:208-232)
4. **Nút đáy** sticky `border-t border-slate-200 bg-white p-2` + `<Button className="w-full justify-center gap-1.5 text-xs">` + `Plus h-3.5 w-3.5` "Thêm mới" (:239-248)
5. **Phân trang: KHÔNG CÓ**`pageSize: 50` cứng (:76), 0 hit `page`.
### B — `KhkkListPanel.tsx` (375 dòng)
Shell `<aside>` **giống hệt A** (:254).
1. Header filter `space-y-2 border-b border-slate-200 p-3` (:257) — ⚠️ **KHÔNG có hàng nhãn + pill đếm** (A:96-101 vắng mặt)
- Ô tìm: **giống hệt A** (:258-266) — icon/`pl-8`/`Search` khớp
- Ô lọc: `Select` + `aria-label="Lọc trạng thái"` (A KHÔNG có aria-label) (:267-276)
- 2 dòng ghi-chú `text-[10px] italic text-slate-400` (flatMode + !canPe) (:277-287)
2. Body `flex-1 overflow-y-auto` (:290) — **2 chế độ**: `PipelineTreePanel` (cây, :291-298) ⟂ danh-sách phẳng (:300-344)
- loading 8 skeleton **có** `motion-reduce:animate-none` (:300-306)
- **có nhánh `isError` + nút "Thử lại"** (:307-318) — A KHÔNG có
- empty: **KHÔNG dùng `EmptyState`** mà tự vẽ `icon-chip` inline + text (:319-330)
3. Card `KhkkPlanCard` (:98-163) — export riêng, 3 hàng (chi tiết §2)
4. **Nút đáy: KHÔNG CÓ**
5. **Phân trang: CÓ**`border-t px-3 py-2 text-[11px]` + "N kế hoạch — Trang x / y" + 2 nút `←`/`→` (:347-372), `PAGE_SIZE = 20` (:42)
### C — `KhkkWorkListPanel.tsx` (305 dòng)
Shell `<aside className="flex min-h-0 flex-col overflow-hidden border-r border-slate-200 bg-white">` (:142) — thêm `min-h-0`.
- Bố cục **2 TẦNG 50/50** qua `<Tier>` (:48-75): header tầng `border-b bg-slate-50/80 px-3 py-2` + icon 5×5 + title `text-[11px] font-bold uppercase tracking-wide text-slate-600` + pill đếm `rounded-full bg-slate-100 px-2 py-0.5 text-[10px]`
- **KHÔNG có ô tìm, KHÔNG có ô lọc, KHÔNG phân trang, KHÔNG nút đáy** (cố ý, :298-302)
- Card tầng-trên (PE): `px-3 py-2` · mã `font-mono text-[10px] text-brand-700` · tên `text-[12px] font-medium text-slate-800` · selected `bg-amber-50 ring-1 ring-inset ring-amber-300` (:189-211)
- Card tầng-dưới (KHKK): `px-3 py-2` · mã `font-mono text-[10px] text-violet-700` · badge `text-[9px]` · tên `text-[12px] font-medium text-slate-800` · selected `bg-violet-50 ring-1 ring-inset ring-violet-300` (:252-282)
- loading = **chữ "Đang tải…"** `p-3 text-[11px] text-slate-500` (:160, :230) — KHÁC skeleton của A/B
- error = "Không tải được danh sách." + "Thử lại" `text-[10px]` (:161-172, :231-242)
- empty = `<p className="p-3 text-[11px] italic text-slate-400">` (:173-177, :243-248) — KHÁC A (`EmptyState`) và KHÁC B (icon-chip)
- gate quyền: `!canKhkk` → khối `Lock` giữa cột (:143-151)
---
## 2. Bảng đối chiếu 10 trục
Trục: card phiếu (bố cục hàng · cỡ chữ · badge trạng thái · chip nhóm · số tiền) · ô tìm · ô lọc · khoảng cách · empty-state · phân trang · nút đáy · loading · error.
_(pending)_
> 🔴 **Đính chính phạm vi CHUẨN (đo được, quan trọng cho §4):** `PeListPanel.tsx` **KHÔNG phải** panel đang chạy ở màn Duyệt NCC golive. Màn golive là `pages/pe/PurchaseEvaluationsListPage.tsx` — nó **tự vẽ `<aside>` panel-1 inline** (`:399-455`), có CÂY 4 tầng inline (`:472-…`). `PeListPanel.tsx` chỉ được dùng ở màn **Thao tác** (`PurchaseEvaluationWorkspacePage`). Vậy CHUẨN có **2 mặt**: (A1) panel màn-danh-sách = `PurchaseEvaluationsListPage:399-455` · (A2) panel màn-thao-tác = `PeListPanel.tsx`. Đối chiếu đúng cặp: **B ⟂ A1**, **C ⟂ A2**.
| Trục | A1 `PurchaseEvaluationsListPage` (golive) | A2 `PeListPanel` | B `KhkkListPanel` | C `KhkkWorkListPanel` | Verdict |
|---|---|---|---|---|---|
| **Vỏ panel** | `aside flex flex-col overflow-hidden border-r border-slate-200 bg-white` (:399) | y hệt (:92) | y hệt (:254) | + `min-h-0` (:142) | ✅ KHỚP |
| **Hàng nhãn + pill đếm trong panel** | KHÔNG (đếm nằm ở header trang `:386-388`) | CÓ "Danh sách phiếu" + pill (:96-101) | **KHÔNG** — và header trang B để pill = "Giai đoạn 2" (`KhkkListPage:128-130`) **CHỨ KHÔNG PHẢI SỐ ĐẾM** | pill đếm mỗi tầng (:66-70) | 🔴 **DELTA-1**: KHKK là màn DUY NHẤT không cho biết "đang có bao nhiêu phiếu" ở chế-độ cây |
| **Ô tìm** | 1 ô, `Search` `left-2.5 top-2.5 h-4 w-4`, `Input pl-8`, ph. "Tìm mã / tên gói thầu / dự án…" (:401-409) | y hệt (:103-111) | **2 Ô CHỒNG NHAU ở chế-độ cây** — ô host (`:258-266`, `Input` h-8 rounded-lg) + ô ruột cây (`PipelineTreePanel:236-244`, `input` thô h-8 rounded-**md** ph. "Lọc dự án / hạng mục / mã phiếu…") | KHÔNG có (cố ý) | 🔴 **DELTA-2 (nặng nhất)** |
| **Ô lọc trạng thái** | `Select` (:437-453) + 2 banner thay-thế: đỏ "Phiếu đã xóa" (:427-431), hổ phách "Lọc cố định: Đã gửi duyệt" (:432-435) | `Select` (:117-122) + banner xám lọc-cố-định (:112-115) | `Select` + `aria-label` (:267-276). **KHÔNG có banner cho `deletedView`** — vào leaf "Đã xóa" thì `Select` vẫn hiện dù server bỏ qua `phase` (`:209`) | KHÔNG | 🔴 **DELTA-3**: mất banner ngữ-cảnh + `Select` chết-lặng ở view Đã xóa |
| **Nhãn trạng thái (từ vựng)** | "Nháp · Đã gửi duyệt · **Cần chỉnh sửa lại** · Đã duyệt · Từ chối" (`purchaseEvaluation.ts:83-89`) | ↑ | "**Đang soạn thảo** · **Chờ duyệt** · Đã duyệt · **Trả lại** · Từ chối" (`khkk.ts:49-55`) | ↑ | 🟡 **DELTA-4**: 3/5 nhãn khác chữ cho cùng ý |
| **Badge trạng thái (hình)** | `rounded px-1.5 py-0.5 text-[10px] font-medium` + `bg-X-100 text-X-700`, **KHÔNG viền** (`PeListPanel:183-190` + `purchaseEvaluation.ts:91-97`) | ↑ | `rounded-**md** **border** px-1.5 py-0.5 text-[10px] font-medium` + `bg-X-100 text-X-**800** border-X-300` (`:136-143` + `khkk.ts:58-64`) | như B nhưng `text-[9px]` (:270-277) | 🔴 **DELTA-5**: 3 biến-thể badge cho cùng 1 hệ trạng-thái |
| **Card — bố cục hàng** | 4 hàng: tiêu-đề+badge · mã·dự-án · chip-loại+SLA · "✓ Đã tạo HĐ" | ↑ (:158-200) | 3 hàng: tiêu-đề+badge · mã+N?+ngày · người-soạn+tiền (:121-160) | 2 hàng, không badge tầng-trên (:200-210) | 🟡 hợp lý (khác dữ-liệu) |
| **Card — cỡ chữ** | tiêu-đề `text-[13px] font-medium text-slate-900`; phụ `text-[11px] text-slate-500`; mã `font-mono` | ↑ | tiêu-đề `text-[13px]` ✅; phụ `text-[11px]` ✅; mã `font-mono` ✅ | tiêu-đề **`text-[12px] text-slate-800`**; phụ **`text-[10px]`**; mã `font-mono text-[10px]` | 🔴 **DELTA-6**: C nhỏ hơn A2 nguyên 1 nấc (12/10 vs 13/11) |
| **Card — padding + selected** | `px-3 py-2.5` · `bg-brand-50 ring-1 ring-inset ring-brand-200` | ↑ (:154-156) | `px-3 py-2.5` ✅ + `bg-brand-50 ring…brand-200` ✅ + **`focus-visible:ring-2 …brand-500/60`** (A KHÔNG có) (:117-118) | **`px-3 py-2`** + ring **amber-300 / violet-300** (:196-198, :258-261) | 🟡 B tốt hơn A (giữ) · 🔴 **DELTA-7**: C dùng 2 màu chọn khác brand |
| **Chip nhóm N1-N8** | — | — | `rounded border border-violet-200 bg-violet-50 px-1 **text-[9px]** font-semibold text-violet-700`, **không `py`** (:147-151) | y hệt (:265-269) | 🟡 **DELTA-8**: 9px + thiếu `py-0.5` ⇒ lệch baseline với mã `font-mono` bên cạnh; là chữ NHỎ NHẤT toàn màn |
| **Số tiền** | không có trên card | không có | `tabular-nums`, `text-emerald-700` khi `frozen` / `text-brand-800` (:157-159); `formatVnd` = `toLocaleString('vi-VN') + ' đ'` (:44-47) | không có | 🟡 **DELTA-9**: nghĩa của màu (đã chốt/tạm tính) **không có chú giải** ở panel |
| **Khoảng cách khối lọc** | `space-y-2 … p-3` (:400) | y hệt (:94) | y hệt (:257) | không có khối lọc | ✅ KHỚP |
| **Empty-state** | `<EmptyState icon title description>` bọc `p-6` (:465-469) | y hệt (:135-147) | **tự vẽ**: `p-6 text-center text-sm` + `icon-chip` inline style (:319-330) | `<p className="p-3 text-[11px] italic text-slate-400">` (:173-177, :243-248) | 🔴 **DELTA-10**: 3 hình empty khác nhau; chỉ A dùng component chung `EmptyState` |
| **Loading** | 6 skeleton `h-16 animate-pulse rounded-md bg-slate-100`, **thiếu** `motion-reduce` (:458-463) | y hệt, thiếu `motion-reduce` (:128-134) | 8 skeleton **có** `motion-reduce:animate-none` (:300-306) | **chữ "Đang tải…"** `p-3 text-[11px]` (:160, :230) | 🔴 **DELTA-11**: C tụt hạng (chữ thay skeleton) · 🟢 B hơn A về a11y |
| **Error** | **KHÔNG có nhánh `isError`** (0 hit) | **KHÔNG** (0 hit) | CÓ + nút "Thử lại" (:307-318) | CÓ ×2 + "Thử lại" `text-[10px]` (:161-172, :231-242) | 🟢 KHKK hơn — **đừng hạ theo A**; ngược lại nên nâng A |
| **Phân trang** | KHÔNG (pageSize 50 cứng, `:84/:102`) | KHÔNG (pageSize 50, `:76`) | CÓ, chỉ ở `flatMode`: "N kế hoạch — Trang x / y" + `←`/`→` (:347-372), `PAGE_SIZE=20` (:42) | KHÔNG (30 phiếu mới nhất, `:40`) | 🔴 **DELTA-12**: nút `←`/`→`**ký tự trần**, không phải `lucide` icon (FD3) + không `aria-label` |
| **Nút đáy** | KHÔNG (nút "Lập kế hoạch"/"Tạo" nằm ở header trang) | CÓ sticky `Button w-full` + `Plus` (:239-248) | KHÔNG — nút "Lập kế hoạch" ở header trang (`KhkkListPage:133-136`) | KHÔNG (cố ý, có lý-lẽ ghi tại :298-302) | ✅ hợp lý |
| **Trạng thái thiếu-quyền** | — | — | dòng chữ `text-[10px] italic slate-400` (:282-287) | khối `Lock` giữa cột (:143-151) | 🟡 **DELTA-13**: 2 hình cho cùng 1 việc, bản B mờ hơn ghi-chú thường |
---
## 3. Delta chi tiết (mỗi delta = evidence file:line + fix cụ thể)
_(pending)_
## 3. Delta chi tiết — xếp theo mức đau (mỗi mục: bằng-chứng → sửa gì)
**MIRROR đã kiểm:** `sha256sum` 2 app → `KhkkListPanel` `ad9073db…314d` KHỚP · `KhkkWorkListPanel` `fcb97650…0e26` KHỚP ⇒ **mọi sửa phải áp 2 app** (`fe-admin` + `fe-user`), giữ SHA cặp.
### 🔴 DELTA-2 — HAI Ô TÌM CHỒNG NHAU (nặng nhất, thấy ngay bằng mắt)
Ở chế-độ cây (`flatMode === false`), panel render:
- ô 1 = host `KhkkListPanel.tsx:260-265``Input` (h-8 **rounded-lg** border-slate-**300** text-**sm**), ph. "Tìm mã kế hoạch / mã phiếu / gói thầu…"
- ô 2 = ruột cây `PipelineTreePanel.tsx:236-243``<input>` thô (h-8 **rounded-md** border-slate-**200** text-**[12px]**), ph. "Lọc dự án / hạng mục / mã phiếu…", cách ô 1 đúng 1 `p-2`+`p-3` ≈ 20px.
Hai ô **trái nghĩa nhau**: gõ ô 1 ⇒ `flatMode` bật (`:201`) ⇒ **cây + ô 2 biến mất giữa lúc đang gõ**; gõ ô 2 ⇒ lọc trong cây (`PipelineTreePanel:145-153`), ép mở hết node (`:180`).
**Sửa:** 1 ô DUY NHẤT. Nâng `PipelineTreePanel` nhận `searchTerm`/`onSearchTermChange` (controlled) + ẩn ô ruột khi `hideHeader` (cùng lý-lẽ đã dùng để ẩn header ở `:225-232`). Host giữ 1 `Input`.
### 🔴 DELTA-5 — 3 biến-thể badge cho cùng 1 hệ trạng-thái
`purchaseEvaluation.ts:91-97` (`rounded`, không viền, `text-X-700`) ⟂ `khkk.ts:58-64` (`rounded-md`+`border`+`text-X-800`) ⟂ `KhkkWorkListPanel.tsx:270-277` (`text-[9px]`).
**Sửa:** chọn 1 khuôn. Khuyến nghị **giữ khuôn KHKK có viền** (viền giúp badge không chìm khi nền card đổi sang `bg-brand-50` lúc chọn — PE đang chìm thật) và **nâng PE lên khuôn đó**, KHÔNG hạ KHKK. Nếu owner muốn "khác gì cũng phải giống golive" thì đổi ngược, nhưng phải đổi ở `khkk.ts` (1 chỗ) chứ đừng vá tại card.
→ Bắt buộc: `KhkkWorkListPanel:272` `text-[9px]``text-[10px]` (bằng B) — không có lý do để cùng badge nhỏ hơn ở màn khác.
### 🔴 DELTA-6 — card màn Thao tác nhỏ hơn 1 nấc so với chuẩn A2
`KhkkWorkListPanel:205` tiêu-đề `text-[12px] text-slate-800` + `:201-203` phụ `text-[10px]` ⟂ chuẩn `PeListPanel:160` `text-[13px] text-slate-900` + `:165` `text-[11px]`. Padding `py-2` (`:196`) ⟂ `py-2.5` (`PeListPanel:154`).
**Sửa:** đưa C về `text-[13px]/text-slate-900` + `text-[11px]` + `py-2.5`. Cột C rộng 300-340px (`KhkkCreatePage:180`) — thừa chỗ, không phải ép nhỏ vì hẹp.
### 🔴 DELTA-10 — 3 hình empty-state
A dùng `<EmptyState>` (`EmptyState.tsx` — icon trong `rounded-xl bg-slate-100 p-2.5`, title `text-[13px] font-semibold slate-700`, desc `text-xs slate-500`) · B tự vẽ `icon-chip` + `text-sm` (`KhkkListPanel:319-330`) · C chỉ 1 dòng `italic slate-400` (`:173-177`, `:243-248`).
**Sửa:** cả B và C dùng `<EmptyState>`. B: `icon={Inbox}` + title "Không có kế hoạch khớp bộ lọc" + description nói **cách thoát** ("Xoá từ khoá hoặc chọn Tất cả trạng thái"). C: title ngắn + description giữ nguyên câu giải-thích đang có (câu đó tốt, chỉ sai vỏ).
### 🔴 DELTA-11 — loading màn Thao tác tụt hạng
`KhkkWorkListPanel:160`/`:230` = chữ "Đang tải…" ⟂ chuẩn skeleton `h-16 animate-pulse` (`PeListPanel:128-134`).
**Sửa:** skeleton 4-6 dòng, **kèm `motion-reduce:animate-none`** (KHKK list đã làm đúng ở `:303`, PE golive đang THIẾU — xem DELTA-14).
### 🔴 DELTA-12 — nút phân trang là ký tự trần
`KhkkListPanel:360`/`:368` in `←` `→` bằng text, không `aria-label`, không icon set. Trái FD3 (icon set nhất quán = lucide) + FD5 (nút không có tên đọc được).
**Sửa:** `<ChevronLeft className="h-3.5 w-3.5" />` / `<ChevronRight …>` + `aria-label="Trang trước"` / `"Trang sau"`; giữ `disabled:opacity-40` (đã có).
### 🔴 DELTA-15 (MỚI, FD5 — ĐO ĐƯỢC) — chữ `slate-400` cho thông tin BẮT BUỘC ĐỌC
Tương-phản `#94a3b8` trên nền trắng = **2.56:1** (tự tính theo WCAG relative-luminance) ⇒ **trượt AA 4.5:1, trượt cả ngưỡng 3:1**. Đang dùng cho đúng những câu người dùng CẦN đọc nhất:
- `KhkkListPanel:278` "Đang lọc — danh sách lấy thẳng từ máy chủ…" (`text-[10px] italic text-slate-400`)
- `KhkkListPanel:283` "Bạn chưa có quyền xem phiếu Duyệt NCC…"
- `KhkkListPanel:230` "Gói này chưa có kế hoạch ký kết."
- `KhkkWorkListPanel:174`, `:244`, `:290`
- (systemic, KHÔNG phải lỗi riêng KHKK) `PipelineTreePanel:278` "Cây tải 200 phiếu mới nhất…"
**Sửa:** `text-slate-400``text-slate-500` (`#64748b` = **4.76:1**, PASS AA) cho MỌI dòng chữ mang thông tin; giữ `slate-400` chỉ cho icon trang trí. Và nâng `text-[10px]``text-[11px]` cho 2 dòng ghi-chú của B (chúng đang là chữ nhỏ nhất + mờ nhất trong khi giải thích hành-vi lạ nhất của màn).
### 🟡 DELTA-1 — mất số đếm
`KhkkListPage:128-130` pill header = "Giai đoạn 2 · <tên nhóm>" ⟂ golive `PurchaseEvaluationsListPage:386-388` pill = **số phiếu**. Ở chế-độ cây, B **không nơi nào** cho biết đang có bao nhiêu kế hoạch (pill của cây bị `hideHeader` ẩn, `PipelineTreePanel:210-219`).
**Sửa:** thêm pill số vào header trang B, đứng CẠNH pill "Giai đoạn 2" (2 pill khác vai, đừng gộp): flat = `list.data.total`; tree = tổng `stage2Count`.
### 🟡 DELTA-3 — mất banner ngữ-cảnh ở view "Đã xóa"
Golive có 2 banner thay chỗ `Select` (`:427-435`: đỏ cho Đã-xóa, hổ-phách cho Chờ-tôi). B vào `?view=deleted` vẫn hiện `Select` trạng-thái dù server **bỏ qua** `phase` (`KhkkListPanel:209` `phase: deletedView ? undefined : …`) ⇒ ô lọc **chết câm**: đổi giá trị, danh sách không đổi.
**Sửa:** `deletedView` ⇒ thay `Select` bằng banner đỏ đúng khuôn golive: `rounded border border-red-200 bg-red-50 px-2 py-1.5 text-[11px] text-red-700` + câu "🗑 Phiếu đã xóa — chỉ xem."
### 🟡 DELTA-4 — từ vựng trạng thái lệch
`khkk.ts:49-55` "Đang soạn thảo/Chờ duyệt/Trả lại" ⟂ `purchaseEvaluation.ts:83-89` "Nháp/Đã gửi duyệt/Cần chỉnh sửa lại".
**Sửa (đề-xuất, cần owner gật vì là chữ hiển thị):** thống nhất theo golive — `1:'Nháp'`, `2:'Đã gửi duyệt'`, `98:'Cần chỉnh sửa lại'`. 1 chỗ sửa, 0 rủi ro máy (label thuần hiển thị). Nếu owner muốn giữ chữ KHKK thì đảo lại — nhưng ĐỪNG để 2 bộ chữ song song, cùng 1 người dùng đi qua GĐ1→GĐ2 trong 1 phút.
### 🟡 DELTA-7 — màu "đang chọn" của C
`KhkkWorkListPanel:197` `ring-amber-300` + `:260` `ring-violet-300` ⟂ chuẩn `ring-brand-200`+`bg-brand-50`.
**Sửa:** giữ màu accent cho **đầu tầng** (`Tier` icon `:60`, đang đúng) nhưng trạng-thái CHỌN của mọi card về `bg-brand-50 ring-brand-200` — "đang chọn" là 1 khái niệm, không nên đổi màu theo tầng, người dùng phải học 2 lần.
### 🟡 DELTA-8 — chip nhóm N1-N8
`KhkkListPanel:147-151` `text-[9px] font-semibold px-1` **không có `py`** ⇒ hộp chip cao hơn chữ, lệch baseline với `font-mono` bên cạnh; 9px là cỡ chữ nhỏ nhất toàn bộ 3 file.
**Sửa:** `text-[10px] px-1.5 py-0.5 leading-none` + `title={khkkGroupMenuLabel(n)}` (hàm đã tồn tại, `KhkkListPage:129` đang dùng) — "N5" một mình không nói được gì.
### 🟡 DELTA-9 — số tiền đổi màu không chú giải
`KhkkListPanel:157-159`: `frozen` (phase = DaDuyet) → `text-emerald-700`, còn lại `text-brand-800`. Người dùng không có cách nào biết 2 màu = "đã chốt" ⟂ "tạm tính".
**Sửa:** `title={frozen ? 'Giá trị đã duyệt (đã chốt)' : 'Giá trị đề xuất (tạm tính)'}` + thêm 1 ký-tự phân biệt (vd `≈` trước số khi chưa chốt). Rẻ, không tốn chỗ.
### 🟡 DELTA-13 — 2 hình cho "thiếu quyền"
B = dòng chữ mờ (`:282-287`) · C = khối `Lock` căn giữa (`:143-151`). C đúng hơn (cột trắng phải nói ra lý do).
**Sửa:** B dùng lại đúng khuôn C (icon `Lock` + `text-[11px] text-slate-500`), đặt ngay trên chỗ đáng lẽ là cây.
### 🟢 DELTA-14 (nâng CHUẨN, không hạ KHKK) — 2 chỗ KHKK làm ĐÚNG hơn golive
1. **Nhánh `isError` + "Thử lại"**: B `:307-318`, C `:161-172`/`:231-242` — golive `PurchaseEvaluationsListPage``PeListPanel` **0 hit `isError`** ⇒ mất mạng = màn trắng im lặng, không phân biệt được với "chưa có phiếu".
2. **`motion-reduce:animate-none`** trên skeleton: B `:303` có; A1 `:461` + A2 `:131` **không** (FD5).
**Sửa:** port 2 thứ này **NGƯỢC** sang PE. Đây là chỗ duy nhất trong bài mà "đồng bộ" nghĩa là sửa bên golive.
### ✅ Đã KHỚP — không đụng
Vỏ `<aside>` · lưới host 3-panel `lg:grid-cols-[400px_1fr_360px]` (`KhkkListPage:140` = `PurchaseEvaluationsListPage:397`) · khối lọc `space-y-2 border-b p-3` · vị trí + kích thước icon `Search` · `Input`/`Select` cùng primitive (`h-8 rounded-lg`) · card `px-3 py-2.5` + selected `bg-brand-50 ring-brand-200` (B) · `divide-y divide-slate-100` · header trang (icon 5×5 + `h1 text-base font-semibold` + pill).
🟢 B còn **hơn** A ở `focus-visible:ring-2 ring-inset ring-brand-500/60` trên card (`:117`) — A **không có** focus ring ⇒ **giữ B, port sang A** (FD5).
---

View File

@ -33,17 +33,185 @@
---
## 1. FILE A — PurchaseEvaluationWorkspacePage.tsx (CHUẨN)
_(chưa đọc)_
## 1. FILE A — PurchaseEvaluationWorkspacePage.tsx (CHUẨN) — 147 dòng, ĐÃ ĐỌC HẾT
## 2. FILE B — KhkkCreatePage.tsx
_(chưa đọc)_
Khuôn host "Thao tác" GĐ1 (đã golive, owner tuyên "đã tốt rồi", byte revert `1b3bcaa`):
## 3. FILE C — KhkkListPage.tsx
_(chưa đọc)_
| 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" | — |
## 4. SPEC THI-HÀNH-ĐƯỢC
_(chưa viết)_
⚠️ 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
_(chưa viết)_

View File

@ -21,25 +21,56 @@
---
## A. Inventory 3 file
## A. Inventory 3 file
_(chưa đọc — append khi đọc)_
| 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(`** |
## B. Host-map (6 host)
- 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: `navigate` KHÔNG rải trong 2 file trình-bày ⇒ callback-hoá chỉ đụng 1 file logic + prop-surface.
_(chưa đọc)_
## B. Host-map — ĐỦ 6 host của `PipelineTreePanel` ✅
## C. (O2) Truy chữ "SẮP"
| # | 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) |
_(chưa đọc)_
**+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-10` khai 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.
## D. (O2) Đề xuất "0"
_(chưa đọc)_
## E. (O6) Truy điều-hướng
## E. (O6) Truy điều-hướng — ĐÚNG đường đang đi ✅
_(chưa đọc)_
`useNavigate` chỉ khai ở `usePipelineStages.ts:200`; 2 file component **0 hit** `navigate(`. 6 call-site:
| # | 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á

View File

@ -31,3 +31,57 @@
## Nhật ký đọc (append tới đâu ghi tới đó)
### §1+§2+§3 — Diff KHUNG Section (PE chuẩn ⟂ KHKK) [đọc @lượt 2-5]
**PE `Section` (`fe-admin/src/components/pe/PeDetailTabs.tsx:514-523`) — TRẦN, 2 prop:**
```
function Section({ title, children }: { title: string; children: React.ReactNode })
<section className="px-3 py-3 sm:px-5 sm:py-4">
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
```
**KHKK `Section` (`fe-admin/src/components/khkk/KhkkDetailContent.tsx:60-96`) — GIÀU, 8 prop:**
`title · icon · accent · head · chipBg · chipFg · count? · actions?`
`<section className="px-5 py-4">` + `icon-chip` 1.75rem + `h3 text-sm font-semibold tracking-tight {head}` + count-pill + `actions` ml-auto.
| Trục | PE (chuẩn golive) | KHKK | Verdict |
|---|---|---|---|
| Đánh số trong title | ✅ "1./2./3./5." nhúng chuỗi (`PeDetailTabs.tsx:402,405,418,426`) | ✅ y hệt (`:287,357,449,529`) | KHỚP |
| Count trong title | ✅ nhúng CHUỖI `(${details.length} hạng mục · ${suppliers.length} NCC)` (`:405`) | ⚠️ **HAI KIỂU LẪN**: S2 nhúng chuỗi `(${plan.lines.length} dòng)` (`:357`) · S3/S4 dùng `count` pill (`:455,535`) | **LỆCH NỘI-BỘ** |
| icon-chip | ❌ PE **không có icon** ở section-head | ✅ có | KHKK GIÀU HƠN chuẩn |
| accent/head color | ❌ đồng loạt `text-slate-500` | ✅ brand/teal/violet per-section | KHKK GIÀU HƠN chuẩn |
| Kiểu chữ head | `text-xs uppercase tracking-wide` (eyebrow) | `text-sm tracking-tight` (title thường) | **LỆCH** |
| actions slot | ❌ không (PE nhét nút vào thân, vd `ItemsTab`) | ✅ `ml-auto` | KHKK GIÀU HƠN |
| Padding | `px-3 py-3 sm:px-5 sm:py-4`**có bậc responsive**, comment `:515-516` ghi rõ "laptop màn nhỏ tiết kiệm ~16px" | `px-5 py-4` **CỨNG** | 🔴 **KHKK REGRESS** — mất bài học laptop 1366/mobile 375 |
| divide-y giữa section | `divide-y divide-slate-200` (`:397`) | y hệt (`:284`) | KHỚP |
| Vỏ ngoài | KHÔNG `card-accent` (bọc bởi host) | `card-accent` + `--accent: brand-500` (`:264`) | KHKK GIÀU HƠN |
| Khối "nổi bật" | ✅ PE có: section 4 **KHÔNG bọc `Section`** mà là `<section>` trần chứa khối viền + **thanh header `bg-[#1F7DC1] text-white uppercase`** (`:423-425` + `:1995-1998`) | ❌ KHKK không có khối nổi bật nào | 🔴 **THIẾU** — đây chính là khuôn cho O3 (xem §7.6) |
> ⚖️ **Kết luận khung (quan trọng, không đảo chiều mù quáng):** KHKK **không phải "xấu hơn"** — nó giàu hơn PE ở 4 trục (icon/accent/actions/card). Việc cần làm KHÔNG phải hạ KHKK xuống mức PE, mà là (a) **vá đúng 2 chỗ KHKK regress thật** (padding responsive + count 2-kiểu), (b) **port ngược** khuôn "khối nổi bật header xanh" của PE sang KHKK cho O3. Nếu muốn đồng bộ 1 chiều thì hướng đúng là **nâng PE lên khuôn Section giàu của KHKK** (việc lớn, ngoài lượt này) — ghi vào backlog §9-P2.
### §4 — Diff BẢNG (table)
| Trục | PE | KHKK | Verdict |
|---|---|---|---|
| Layout | `table-fixed` + **width từng cột** (`w-[24%] w-[9%]…`, `:2719-2728`) + `min-w-[860px]` | `w-full text-sm` **không table-fixed, không set width cột** (`:375,468`) | 🔴 LỆCH — cột KHKK co giãn theo nội dung ⇒ nhảy layout khi tên NCC dài |
| Viền | lưới ĐẦY ĐỦ kiểu Excel: `border border-slate-300` mọi ô (`:1402-1407`) hoặc `border border-slate-200` + `border-r` (`:2719-2727`) | chỉ `border-b` hàng (`:388,481`) | LỆCH (chấp nhận được — nhưng phải chọn 1) |
| thead | `bg-slate-50 text-slate-600` (`:2720`) **hoặc** `bg-slate-100 text-[11px] font-semibold uppercase tracking-wide text-slate-600` (`:1404,1509`) — **PE không dùng `.label-eyebrow` lần nào trong file 3613 dòng** | `bg-slate-50/70` + `th class="label-eyebrow"` (`:376-383,469-476`) | LỆCH: `.label-eyebrow` = `11px/700/ls .07em/**brand-600**` (`fe-admin/src/index.css:89-95`) ⇒ th KHKK **màu brand**, th PE **slate**. Head-xanh trên nền `bg-slate-50/70` = nhạt, kém tương phản header/thân |
| Body font-size | `text-xs` / `text-[13px]` | `text-sm` bảng + `text-xs` từng ô lẫn lộn (`:397,431-434`) | LỆCH nội-bộ: cột NCC `text-sm`, cột Hạng mục/Ghi chú `text-xs` cùng hàng ⇒ baseline gãy |
| Số | `text-right` + `tabular-nums` | y hệt (`:431-433`) | KHỚP ✅ |
| overflow | `overflow-x-auto` + min-w | `overflow-x-auto` **không min-w** (`:374,467`) | LỆCH nhẹ |
| Hàng rỗng | `<td …text-slate-300>—</td>` | dùng `'—'` chuỗi trần trong `text-slate-600` (`:434,493`) | LỆCH — em-dash KHKK **cùng màu chữ thật** ⇒ đọc nhầm là dữ liệu |
### §5 — Diff FIELD / label / value / rỗng
- KHKK `Field` (`KhkkDetailContent.tsx:98-116`): label `text-[11px] font-semibold uppercase tracking-wide **text-brand-600**` · value `font-medium **text-brand-800**` · rỗng `text-slate-300` + `—`.
- PE tương đương `FormRow` (`PeDetailTabs.tsx:2166`) + label rời `text-[11px] font-semibold uppercase tracking-wide **text-slate-500**` (`:1774`), value mặc định slate.
- 🔴 **Lệch nặng nhất về màu:** KHKK tô **brand-600/brand-800 cho MỌI value** (`:110-111`) ⇒ toàn phiếu xanh, mất hierarchy (FD4 "1 focal point"). PE giữ value slate, chỉ tô màu chỗ CÓ NGHĨA (winner emerald `:2008`, cảnh báo amber `:1336`).
- Rỗng: PE có 3 biến thể — `text-sm text-slate-500` (`:2572,3194,3285`), `italic text-slate-400` (`:603,736,3380,3533`), `text-slate-300 —` (in-cell). KHKK dùng `text-sm text-muted-foreground` (`:372,465,547`) — **token thứ 4, không có ở PE**. → chọn 1: `text-sm italic text-slate-400` cho empty-block, `text-slate-300 —` cho in-cell.
### §6 — Diff KHOẢNG CÁCH
- Section padding: xem bảng §3 (KHKK mất bậc `px-3 py-3 sm:`).
- Header→body: PE `mb-3` (`:519`) · KHKK `mb-3` (`:75`) → KHỚP ✅.
- Grid field: KHKK `gap-x-6 gap-y-4 md:grid-cols-2` (`:301,344`) — trên scale ✅.
- Ô bảng: PE `px-2 py-1.5` (dày đặc, ERP) · KHKK `px-3 py-2` (thoáng hơn) → LỆCH, nhưng KHKK **dễ đọc hơn**; giữ KHKK, ghi rõ là chủ đích.
- 🔴 Bảng KHKK **không có `hover:bg-*` hàng** (PE cũng không ở table, nhưng có ở nút) — thiếu affordance khi bảng có hành động inline (S2 select, S3 sửa/xóa). FD4 "states".