# sub-frontend-designer-L1 — LĂNG-KÍNH: PANEL QUY TRÌNH DUYỆT

> Run: 2026-08-05-S174-khkk-style-audit · Lane L1 (ensemble /fable-clone frontend-designer)
> Scope: CHỈ 2 file — `fe-admin/src/components/pe/PeWorkflowPanel.tsx` (CHUẨN) ⟂ `fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx` (ĐỐI TƯỢNG)
> Owner O1: "cho style nó giống với duyệt NCC nhé, style hiển thị 2 bên khác nhau ko đc"
> APPEND-as-I-read (khung rỗng ghi TRƯỚC khi đọc file — chống #53)

---

## 0. Trạng thái khảo sát (điền dần)

| # | Việc | Trạng thái | Ghi chú |
|---|---|---|---|
| 0.1 | Ghi khung rỗng | ✅ | lượt 1, TRƯỚC mọi Read |
| 0.2 | Đọc PE panel (chuẩn) | ✅ | 1062 dòng, đọc HẾT (2 trang) |
| 0.3 | Đọc KHKK panel (đối tượng) | ✅ | 726 dòng, đọc HẾT |
| 0.4 | Đọc DTO 2 phía (field gap) | ✅ | `types/khkk.ts` (527 dòng, đọc hết) + grep `types/purchaseEvaluation.ts` |
| 0.4b | Đọc phần PE ủy-quyền sang file khác | ✅ | `PeDetailTabs.tsx:778-794` (2 section) + `:3056-3063` (badge) + `:3109-3288` (2 tab) |
| 0.4c | Đọc vỏ host 2 bên (loại nhiễu) | ✅ | `PurchaseEvaluationsListPage.tsx:668` ⟂ `KhkkListPage.tsx:186` — **giống hệt** |
| 0.4d | Đọc CSS `.card-accent`/`.icon-chip` | ✅ | `fe-admin/src/index.css:112-137` |
| 0.4e | Đo xác thực đường tải file | ✅ | `ContractSigningPlansController.cs:222-232` + `lib/api.ts:18` → §6.1 |
| 0.4f | Đo bất-biến mirror 2 app | ✅ | sha256 khớp `fb563b25…` |
| 0.5 | Kê bảng đối chiếu 8 hạng mục | ✅ | §2 (H0-H8, H0 là mục tao thêm) |
| 0.6 | Phân loại bê-thẳng ⟂ thiếu-data | ✅ | §4.A (15) / §4.B (1) / §4.C (8) |
| 0.7 | Viết spec thi-hành-được | ✅ | §5 (7 đợt + 7 bất-biến + nghiệm thu) |
| 0.8 | Soi-bằng-mắt FD2 | ❌ | **KHÔNG chạy được** — không dev server/API; khai thẳng §6.4, KHÔNG bịa ảnh |

---

## 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)
| 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)
| 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

> Ký hiệu: **≡** = khớp · **≠** = CÓ nhưng style lệch · **∅** = KHKK THIẾU HẲN · **⊘** = không có dữ-liệu

### 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`** |

**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.

### 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).
- ✅ **ĐÃ ĐO — bác giả-thuyết ban đầu của chính tao**: tao định kê "KHKK thiếu nhánh báo-trước vì cờ chỉ bật sau khi duyệt". SAI. `khkk.ts:385-390` khai rõ `endedByLevelFinalize` là **3-NGẢ ở BE**: phiếu `DaDuyet` → cờ runtime thật (Mig 71); phiếu **chưa xong → suy từ cấu hình, POINTER-AWARE** (chỉ tính cấp `allowApproverFinalize` ở-hoặc-SAU con-trỏ); chưa pin quy trình → false. Và KHKK **đã** rẽ nhánh sẵn (`:276-278`). ⇒ H1 **chỉ lệch MÀU + CÂU CHỮ**, dữ-liệu đủ, **bê thẳng được** (§4.A-4).

### 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.

### 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`.

### 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*.

### 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ụ (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` |

---

## 4. Phân loại hành động

### 4.A BÊ THẲNG được — data đã có đủ 2 phía (13 + 2 mục)
> Mọi mục 1-13 chỉ đụng **class / JSX / chuỗi**, KHÔNG đụng query, mutation, guard quyền.

| # | Mục | Sửa gì | Bằng chứng đủ-data |
|---|---|---|---|
| 1 | **Vỏ panel** | bỏ `<section className="card-accent" style={--accent}>` → `<div className="space-y-4">`; bỏ `<header border-b px-5 py-3>` + `.icon-chip`; bỏ `p-5` (aside đã `p-4`) | thuần trình bày |
| 2 | **Tiêu đề panel** | `<h3 className="text-sm font-semibold text-slate-900">Quy trình duyệt</h3>` (bỏ `text-emerald-700`), meta xuống dòng riêng `<p className="mt-0.5 font-mono text-[11px] text-slate-500">` — khuôn PE `:347-352` | — |
| 3 | **Số hiệu quy trình** | `v${String(plan.workflowVersion ?? 0).padStart(2,'0')}` → hiện `v01` như PE `:350` | `workflowVersion` có (`khkk.ts:384`) |
| 4 | **Banner kết-thúc-sớm** | violet → **emerald** `border-emerald-300 bg-emerald-50 text-emerald-800`; nhánh đã-duyệt mở bằng `✅`, nhánh đang-chạy mở bằng `⚑ Quy trình rút gọn:` + câu "Các cấp trước vẫn duyệt như thường." | `endedByLevelFinalize` BE **3-ngả, pointer-aware, CÓ cả khi chưa duyệt** (`khkk.ts:385-390`) ⇒ nhánh báo-trước có dữ-liệu thật |
| 5 | **Chip phòng của Bước** | `border border-slate-300 bg-white text-slate-600` → `bg-emerald-50 text-emerald-700`, bỏ viền | `departmentName` có (`khkk.ts:289`) |
| 6 | **3 nút hành động** | thay `<Check/><Undo2/><X/>` bằng glyph trong chuỗi `'✓ Duyệt'` · `'← Trả lại'` · `'✗ Từ chối'`; bỏ `inline-flex items-center gap-1` để khớp `rounded border px-2 py-1 text-[11px] font-bold` (PE `:553`) | thuần trình bày |
| 7 | **Nút Xóa phiếu** | `border-slate-300 text-slate-600` → `border-red-300 bg-white text-red-700 hover:bg-red-50`; giữ `<Trash2>` (PE cũng dùng icon ở đúng nút này `:576`) | — |
| 8 | **3 khối cuối** | bọc mỗi khối `<div className="border-t border-slate-200 pt-4">` (PE `:929`,`:970`,`:974`) — đây là thứ tạo "nhịp" dọc của panel PE | — |
| 9 | **Tiêu đề 3 khối cuối** | `<h3 className="text-sm font-semibold text-slate-900">` (+`mb-2` cho 2 khối lịch sử); **bỏ icon `<Paperclip>/<Check>/<History>` + bỏ pill đếm**, viết `(n)` ngay trong tiêu đề: `Lịch sử duyệt ({approvals.length})` — khuôn `PeDetailTabs.tsx:781`,`:790` + `PeWorkflowPanel.tsx:930` | — |
| 10 | **Thẻ lịch sử duyệt** | `bg-slate-50/60 px-2.5 py-1.5 text-[11px]` → `rounded border border-slate-200 bg-white p-3 text-sm`; dòng 1 bọc `flex items-center justify-between` để **đẩy mốc-giờ sang phải** (`text-xs text-slate-500 shrink-0`); dòng 2 `mt-1 text-xs text-slate-500` | — |
| 11 | **Badge quyết định 3-ngả** | bỏ nhãn gộp *"Trả lại / Từ chối"* (`:83`), theo `decisionBadge` (`PeDetailTabs.tsx:3056-3063`): `toPhase===99` → **"Từ chối"** `bg-rose-100 text-rose-700 border border-rose-200`; reject khác → **"Trả lại"** `bg-amber-100 text-amber-700 border border-amber-200`; còn lại → **"Duyệt"** `bg-emerald-100 text-emerald-700 border border-emerald-200`. Cỡ `text-[11px] font-medium` (không phải `text-[9px] font-semibold`) | `KhkkApprovalDto.toPhase` **có** (`khkk.ts:307`) ⇒ tách được Từ-chối ⟂ Trả-lại |
| 12 | **Lịch sử thay đổi mở sẵn** | bỏ nút gập + bỏ `enabled: showChangelog` (`:186`) → render thẳng như PE `:973-975`; giữ `take:200` | xem §6.2 (chi phí 1 request) |
| 13 | **Chi-tiết cấp-chữ** | "Huỷ"→"Hủy"; `variant="outline"`→`"ghost"`; hộp chọn file dựng theo khuôn brand gạch-đứt + liệt-kê từng file có cỡ + nút `<X>` (PE `:841-866`) | — |

**Bán-bê-thẳng (cần thêm 1 component nhỏ — route BE ĐÃ CÓ SẴN):**

| # | Mục | Ghi chú |
|---|---|---|
| 14 | **Xem trước `<Eye>` + tải xuống có xác thực** | PE `:938-953` + `AttachmentPreviewDialog.tsx:24-32`. KHKK **đủ route**: `GET {id}/attachments/{attId}/view` (`ContractSigningPlansController.cs:231-232`) + `/download` (`:222-223`). Cần (a) đổi `<a href>` `:500-505` sang `api.get(…, {responseType:'blob'})` khuôn PE `:327-342`; (b) thêm prop `basePath` cho `AttachmentPreviewDialog` (đang hardcode `evaluationId`, `:18`) hoặc bản KHKK riêng. 🔴 **Đồng thời là sửa LỖI THẬT — §6.1** |
| 15 | **Mờ nhánh sau cấp KẾT THÚC theo CẤU HÌNH** | PE mờ ngay khi phiếu **đang chạy** (`:165-169`) + badge "Kết thúc tại {X}" ở **Bước** (`:411-418`). KHKK chỉ mờ **sau khi đã duyệt** (`skipped` `:313`). Data đủ (`allowApproverFinalize` `khkk.ts:277` + `finalizeStepName/LevelName` `:392-393`). **Cần owner gật** — đổi *nghĩa hiển thị*, không chỉ màu |

### 4.B THIẾU DỮ-LIỆU — nêu đúng field
| Mục | Field DTO thiếu | Nguồn BE |
|---|---|---|
| **H3 chip "✎ NS PRO" / "✎ NS CCM"** | `KhkkWorkflowLevelDto` (`fe-admin/src/types/khkk.ts:269-278`) chỉ có `{id, order, name, approverUserId, approverFullName, allowApproverFinalize}` — **thiếu `canEditProBudget` + `canEditCcmBudget`**, thứ PE có trên `PeCurrentApprovalLevelApprover` (`fe-admin/src/types/purchaseEvaluation.ts:252-253`) | `ContractSigningPlanWorkflowLevelDto` (BE `ContractSigningPlanFeatures.cs:105-110`, theo chú thích `khkk.ts:265-268`) không trả 2 cờ này |

🔴 **Nhưng KHÔNG đề nghị thêm field.** 2 cờ đó của PE suy từ role để mở quyền sửa ô **ngân sách per-gói-thầu** (`PeWorkItemBudgets`, Mig 50) — khái niệm KHKK không có (KHKK chốt giá per-Line tại choke-point BE `ApplyApprovedValuesOnFinalize`, đã khai `KhkkWorkflowPanel.tsx:29`). Bê chip sang = **vẽ nhãn quyền không tồn tại**, tệ hơn thiếu ⇒ xếp §4.C.

### 4.C KHÔNG áp dụng (khác domain thật — ĐỪNG bê)
`✎ NS PRO / NS CCM` (H3) · "Tiến trình duyệt 2-cấp phòng ban" (`:983-1052`, Mig 16) · bộ chọn giá chốt (`:794-820`) · 4 mode Trả lại (`:635-713`) · "Duyệt thẳng Cấp cuối" (`:725-748`) · "Duyệt done miễn CEO" (`:751-768`) · banner phiếu-trước-chưa-duyệt (`:512-516`) · fallback phiếu V1 (`:481-485`, đã khai `KhkkWorkflowPanel.tsx:27`).

---

## 5. SPEC thi-hành-được cho Opus 5 MAX

### 5.0 Phạm vi đụng file (KHÔNG mở rộng)
```
fe-admin/src/components/khkk/KhkkWorkflowPanel.tsx   ← sửa
fe-user/src/components/khkk/KhkkWorkflowPanel.tsx    ← MIRROR y hệt
```
🔴 **2 file đang SHA256 identical** — đo lúc audit: cả hai = `fb563b25cac09af9ccf00fe6145972244d25012a22bd8ffdd9faac2e04dd6913` (`wc -l` = 726 mỗi bên). Sửa 1 bên mà quên bên kia = vỡ bất-biến file tự khai ở `:4`. Cách an toàn: sửa fe-admin xong **copy nguyên file** sang fe-user, rồi `sha256sum` lại 2 bên.
🔴 **TUYỆT ĐỐI KHÔNG đụng** `PeWorkflowPanel.tsx` / `PeDetailTabs.tsx` — PE là **CHUẨN đang golive**, kéo PE về phía KHKK là làm hỏng thứ owner đang lấy làm mẫu.

### 5.1 Thứ tự thi công (theo mức đập-vào-mắt giảm dần)
| Đợt | Việc | Mục §4.A |
|---|---|---|
| **Đ1 — vỏ** | bỏ card-accent + header icon-chip + `p-5`; dựng lại tiêu đề + meta kiểu PE | 1, 2, 3 |
| **Đ2 — 3 khối cuối** | thêm `border-t pt-4`, đổi tiêu đề `<h3 slate-900>` + `(n)`, bỏ pill/icon, mở sẵn changelog | 8, 9, 12 |
| **Đ3 — lịch sử duyệt** | thẻ trắng `p-3 text-sm`, mốc-giờ canh phải, badge 3-ngả có viền | 10, 11 |
| **Đ4 — nút + chip** | glyph thay lucide ở 3 nút, nút Xóa đỏ, chip phòng emerald | 5, 6, 7 |
| **Đ5 — banner + chữ** | banner finalize violet→emerald + đúng câu chữ PE; "Hủy"/ghost; hộp chọn file brand | 4, 13 |
| **Đ6 — file đính kèm** | `<Eye>` preview + tải-xuống-qua-axios (**kèm sửa lỗi 401**) | 14 |
| **Đ7 — CHỜ OWNER** | mờ nhánh sau cấp KẾT THÚC theo cấu hình | 15 |

### 5.2 Bất-biến phải giữ (đụng vào = hỏng nghiệp vụ, KHÔNG phải style)
1. `stepStatus` / `levelStatus` / `currentLevels` / `signedByLevelId` (`:122-155`) — KHKK **tự suy** trạng thái vì BE trả cây THÔ (`khkk.ts:265-268`). PE có `status` BE precompute. **Đừng "đơn giản hoá" theo PE.**
2. Gom Cấp cùng `order` (`:289`, `:307`) = OR-of-N khớp `ContractSigningPlanFeatures.cs:259-260`.
3. `actingLevel` + `approverFinalizeEligible` (`:165-167`) — mirror `ResolveActingLevel` BE; soi cờ của level khác = ô-tích nói dối.
4. `applyLevelFinalize` là **opt-IN, reset `false` mỗi lần mở dialog** (`:102`, `:431-432`) — F-1 S166 đã RATIFIED @S167.
5. `canSubmit` **cố ý không ẩn theo vai** (`:169-172`, bug-class gotcha #44).
6. 5 `invalidateQueries` sau transition (`:216-219`) + 2 sau xóa (`:230-231`) — có `pipeline-khkk-index` nuôi cây 4-folder S162.
7. `commentRequired` cho Return/Reject (`:242-244`).

### 5.3 Nghiệm thu (acceptance)
- `cd fe-admin && npm run build` **0 lỗi TS**; `cd fe-user && npm run build` **0 lỗi TS**.
- `sha256sum` 2 file KHKK panel → **bằng nhau** sau khi sửa.
- `git diff --stat` chỉ hiện **đúng 2 file** đó.
- Soi mắt cạnh nhau: mở `/purchase-evaluations` chọn 1 phiếu ⟂ mở `/contract-signing-plans` chọn 1 phiếu → panel phải **cùng một hệ**: không thẻ-trong-thẻ, cùng nhịp `border-t`, cùng cỡ chữ khối lịch sử, cùng cách canh mốc-giờ.
- Bấm "Tải xuống" ở "File đính kèm khi duyệt" → **file tải được thật** (nay đang 401, §6.1).

---

## 6. Rủi ro / caveat / cái tao KHÔNG verify được

### 6.1 🔴 LỖI THẬT bắt được khi soi style (không chỉ style)
`KhkkWorkflowPanel.tsx:500-505` tải file bằng **thẻ `<a href="/api/contract-signing-plans/{id}/attachments/{attId}/download">` trần**.
- JWT chỉ được gắn bằng **interceptor axios** (`fe-admin/src/lib/api.ts:18` `config.headers.Authorization = 'Bearer …'`); trình duyệt điều-hướng thẻ `<a>` **không đi qua axios** ⇒ không có header.
- Endpoint đích **bắt buộc xác thực**: `ContractSigningPlansController.cs:222-223` `[Authorize(Policy = "KeHoachKyKet.Read")]`, cộng dồn với `[Authorize]` cấp class `:29`.
- ⇒ Suy luận: bấm "Tải xuống" **ăn 401**. PE không dính vì đi `api.get(..., {responseType:'blob'})` (`PeWorkflowPanel.tsx:327-342`).
- ⚠️ **Tao CHƯA chạy thử** (spawn này không có API + dev server) — đây là suy luận từ 2 mã nguồn, **cần bấm thật 1 lần** để chốt. Nếu đúng thì §4.A-14 đổi hạng từ "style" thành **vá lỗi**.

### 6.2 Bỏ gập "Lịch sử thay đổi" = thêm 1 request mỗi lần mở phiếu
KHKK đang `enabled: showChangelog` (`:186`) ⇒ 0 request khi chưa bấm. PE fetch sẵn nhưng **dùng chung `queryKey ['pe-changelog', id]` giữa `ApprovalsTab` (`:3115`) và `HistoryTab` (`:3224`)** nên vẫn chỉ 1 request. KHKK không có nhu cầu dùng-chung đó ⇒ bỏ gập = **+1 request `take:200` / phiếu**. Chấp nhận được (giống PE), nhưng khai thẳng chứ không giấu.

### 6.3 Nút "Từ chối" — cần owner quyết, ĐỪNG tự bỏ
PE **vẫn còn code** nút Từ chối nhưng thực tế không hiện: BE gỡ `TuChoi` khỏi `nextPhases` + FE filter thêm (`PeWorkflowPanel.tsx:314-317`, UAT S60 "bỏ luôn nút Từ chối — Duyệt hoặc Trả về thôi"). KHKK **vẫn hiện** (`:456-467`). Muốn "giống PE" tuyệt đối thì phải bỏ — nhưng đó là **bỏ chức năng**, không phải đổi style, và KHKK có `KhkkPhase.TuChoi` + banner riêng (`:397-401`) + `commentRequired`. ⇒ **Hỏi owner**, đừng tự cắt.

### 6.4 KHÔNG có ảnh chụp màn hình lượt này (khai thẳng, khuôn NO-SCREENSHOT S159)
Vòng soi-bằng-mắt FD2 **không chạy được** trong spawn này: không có dev server, không có API/SQL, và panel duyệt nằm sau `ProtectedRoute` + cần phiếu KHKK có thật ở trạng thái `ChoDuyet` (bẫy rig #3 trong ký-ức: proxy Vite HTTP ⟂ API HTTPS :5443). Thay bằng **đọc mã nguồn đối chiếu theo cặp file:line** — mọi khẳng định trên đều truy được về dòng cụ thể. **Không bịa screenshot.** Khi lane thi công xong, chốt bằng cách mở prod cạnh nhau (ký-ức: authed pages nghiệm thu ở prod, đừng vật lộn dev-rig).

### 6.5 Ngoài lăng-kính này (nêu để lane khác không sót)
- Thứ tự upload file khác nhau: PE upload **TRƯỚC** transition (`:234-244`, "file lỗi = không duyệt"), KHKK upload **SAU** (`:198-207`, "transition hỏng thì không đẻ file mồ côi"). Hai lựa chọn đều có lý do viết trong mã ⇒ **không đụng ở đợt style**.
- `AttachmentPreviewDialog` xuất sẵn `isPreviewable` (`:11-14`) nhưng `PeWorkflowPanel.tsx:326` **tự viết lại regex** — trùng lặp sẵn có bên PE, không phải việc của đợt này.
- Panel 2 KHKK (`KhkkDetailContent.tsx`) có "Ý kiến cấp duyệt" + có `signedByUserId`/`approverUserId` (`khkk.ts:256-262`) ⇒ badge "Admin duyệt thay" (khuôn `PeDetailTabs.tsx:756-760`) **dựng được** — nhưng thuộc lăng-kính panel 2, không phải lane này.

---

## 7. Kết luận 1 dòng cho owner
Sơ đồ Bước/Cấp của 2 bên **đã giống nhau gần như tuyệt đối** (H2, H4) — cái làm owner thấy "khác nhau" nằm ở **vỏ panel (thẻ-trong-thẻ có viền xanh + tiêu đề có icon)** và ở **3 khối cuối (mất đường kẻ ngăn, tiêu đề nhỏ hơn, thẻ lịch sử xám và đặc chữ, mốc-giờ không canh phải, lịch sử thay đổi bị gập)**. Toàn bộ đều **bê thẳng được**, trừ chip "NS PRO/NS CCM" (KHKK không có khái niệm ngân sách đó — **đừng bê**).

---

END sub-frontend-designer-L1 — VERDICT=15 lệch có-thể-sửa (13 bê-thẳng + 1 bán-bê kèm vá lỗi 401 + 1 chờ-owner) · 1 KHÔNG-BÊ vì thiếu khái niệm (H3 NS PRO/CCM) · H2+H4 đã khớp · lệch nặng nhất = vỏ card-accent (H0) không nằm trong danh sách owner nêu
