# SPEC — Đồng bộ style KHKK theo Duyệt NCC + 8 owner-item (05-08-2026)

> Nguồn: `/fable-clone frontend-designer` ensemble **5 lane / 5 xong / 0 chết** (`wf_016b1bed-b85`, 755K tok, 147 tool-use, ~17′).
> Engine **propose-only**; file này do **LEAD ghi sau khi verify** (H21 §Spec-file). Lane-MD gốc: `sub-frontend-designer-L1..L5.md`.
> 🔴 **O7/O8 tới SAU khi lane đã phóng** ⇒ lane KHÔNG thấy; lead tự phủ. "Lane không nhắc" ≠ "không có việc".

---

## ① TÍNH-NĂNG / ĐỀ-BÀI

Module **KHKK (GĐ2)** phải nhìn và cư xử **cùng một hệ** với **Duyệt NCC (GĐ1 — golive, owner coi là CHUẨN)**.
Owner: *"cho style nó giống với duyệt NCC nhé, style hiển thị 2 bên khác nhau ko đc"* · *"cho nó quét lại hết luôn, design lại"*.

Phạm vi = 8 owner-item (O1–O8) + **3 phát hiện lane** owner chưa nêu nhưng nặng hơn phần lớn danh sách:

| id | Phát hiện thêm | Vì sao vào spec |
|---|---|---|
| **X1** | `KhkkWorkflowPanel:501` tải file bằng `<a href>` trần ⟂ PE `:331` `api.get(responseType:'blob')` | JWT chỉ gắn qua axios interceptor ⇒ **401, nút không chạy**. Đây là LỖI, không phải style. LEAD ĐÃ VERIFY |
| **X2** | `KhkkWorkflowPanel:247` bọc `card-accent` (viền + rail 3px + bóng + hover) ⟂ PE `:345` panel TRẦN | Lệch **đập vào mắt trước mọi mục owner liệt kê**: PE phẳng, KHKK là thẻ-trong-thẻ |
| **X3** | Màn Thao tác `<lg`: `<main>` là `hidden … lg:block` ⇒ **mobile không tạo được phiếu** | 🔴 **Do chính lead gây ra khi dựng host 2-panel trong phiên này.** Regression, phải vá |

🔴 **Hai giả-định của LEAD bị lane BÁC — đã tự verify, xác nhận lane đúng:**
- **O3 KHÔNG cần migration.** `ContractSigningPlanLineConfiguration.cs:37-38` unique index **3 cột** `(PlanId, SupplierId, CatalogEntryId)` ⇒ 1 NCC **đã được phép** N dòng × N hạng mục. Máy rộng sẵn từ Mig 71; chỉ UI hẹp. Lead từng báo owner "nặng nhất, cần migration" — **SAI, đã đính chính**.
- **O4 FE-only, 0 BE.** Rail `dossierItemId` đã land đủ 6 tầng từ Mig 69.

---

## ② CÁCH IMPLEMENT — 4 đợt, xếp theo thứ tự land an toàn

### ĐỢT A — SỬA LỖI (không phải style; land trước, không chờ ai)

| # | File | Đổi | Vì sao |
|---|---|---|---|
| A1 | `components/khkk/KhkkWorkflowPanel.tsx:500-505` | `<a href>` → `api.get(url,{responseType:'blob'})` + `URL.createObjectURL`, mirror `PeWorkflowPanel.tsx:327-342` | **X1** — nút tải hiện trả 401 |
| A2 | `pages/khkk/KhkkCreatePage.tsx` `<main className="hidden … lg:block">` | Bỏ ngõ cụt `<lg`: cho `<main>` hiện ở mọi breakpoint, panel-1 chuyển `hidden lg:flex` (hoặc accordion) | **X3** — regression lead vừa gây |
| A3 | `pages/khkk/KhkkDetailPage.tsx` | Bọc `KhkkWorkflowPanel` trong `<div className="lg:hidden">` | Nợ đã khai với owner: mobile mất nút Gửi duyệt/Xóa |

### ĐỢT B — BÊ THẲNG TỪ PE (FE-only, 0 quyết định, rủi ro thấp)

**B1 — panel Quy trình duyệt (O1)** · `KhkkWorkflowPanel.tsx`
- **X2** gỡ `card-accent` + `p-5` → panel trần `space-y-4` (PE `:345`). 🔴 **GỠ CLASS, KHÔNG đè `border-0`** — `.card-accent` (`index.css:112`) ngoài `@layer` nên thắng utility Tailwind v4 (**gotcha #66, đã cắn thật phiên này**).
- Tiêu đề → `<h3 text-sm font-semibold text-slate-900>` trần, bỏ `icon-chip` (PE `:346-354`).
- Số hiệu WF `v${n}` → `padStart(2,'0')` ⇒ `v01` (PE `:350`).
- Banner kết-thúc: violet → **emerald** + câu chữ PE (`:360-377`). Dữ-liệu ĐỦ (`khkk.ts:385-390` `endedByLevelFinalize` 3-ngả) ⇒ bê thẳng. *Violet đang bị trộn 2 nghĩa — PE dành violet riêng cho "Duyệt thay CEO".*
- Chip PHÒNG: `border-slate-300 bg-white` → `bg-emerald-50 text-emerald-700` (PE `:406-410`).
- Nút hành động: lucide `<Check>/<Undo2>/<X>` → glyph chuỗi `'✓ Duyệt'/'← Trả lại'` (PE `:540`); nút **Xóa** xám → đỏ `border-red-300 text-red-700` (PE `:574`).
- "File đính kèm khi duyệt": thêm `border-t border-slate-200 pt-4` + `<h3 text-sm>` + **nút `<Eye>` xem trước** — route BE **đã có** `ContractSigningPlansController.cs:231-232`; cần thêm prop `basePath` cho `AttachmentPreviewDialog` (đang hardcode `evaluationId`).
- "Lịch sử duyệt (n)": bỏ pill đếm → `(n)` trong ngoặc; thẻ `bg-slate-50/60 text-[11px]` → `rounded border bg-white p-3 text-sm`; mốc-giờ **đẩy phải** bằng `justify-between`; badge **2-ngả → 3-ngả** (Từ chối rose / Trả lại amber / Duyệt emerald) — `KhkkApprovalDto.toPhase` đã có (`khkk.ts:307`).
- "Lịch sử thay đổi": bỏ gập, render ngay (PE `PeDetailTabs.tsx:787-794`).
- Micro-copy: "Huỷ"→"Hủy", "Ý kiến (tuỳ chọn)"→"Ghi chú (tùy chọn)".

**❌ KHÔNG bê:** chip **"NS PRO"/"NS CCM"** (L1 H3). Hai cờ `canEditProBudget/canEditCcmBudget` 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ó**. Bê sang = vẽ nhãn quyền không tồn tại.
**✅ GIỮ chỗ KHKK TỐT HƠN PE:** mốc chữ-ký `✓ dd/mm/yyyy hh:mm` + in đậm người đã ký (`:338-355`).

**B2 — nội dung phiếu (L5: 6 P0)** · `KhkkDetailContent.tsx`
`table-fixed` + width cột + `min-w-[880px]` trong `overflow-x-auto` · ô rỗng `—` → `text-slate-300` (đang cùng màu chữ thật ⇒ đọc nhầm là dữ liệu) · empty-block `text-muted-foreground` → `text-sm italic text-slate-400` · bỏ emoji `🔒` → `<Lock>` lucide · thống nhất `text-xs` cả hàng bảng · `hover:bg-slate-50/60` cho `<tr>`.

**B3 — panel danh sách (L2)** · `KhkkListPanel.tsx` + `KhkkWorkListPanel.tsx`
Chế-độ cây thiếu tiêu-đề/số đếm ⇒ cột cụt đầu · **gộp 2 ô tìm chồng nhau** · thang chữ 4 nấc (13/11/10/**9**px) → về 3 nấc chuẩn · 🔴 **`slate-400` = 2.56:1 cho 6 dòng chữ mang thông tin** — dưới ngưỡng WCAG, phải nâng · nút phân trang ký tự trần → icon · chip nhóm lệch baseline.

**B4 — host (L3)** · `KhkkCreatePage.tsx`
Grid `300px` → **`260px`** khớp PE (`lg:grid-cols-[260px_1fr] xl:grid-cols-[320px_1fr]`) · sửa comment sai sự thật (L3 H-5: `grep -c Button` = 0, đối chứng dương `PeListPanel` = 8).
**CHỐT KHÔNG SỬA (L3):** `h-calc` toàn hệ · violet 2-hệ toàn hệ · nút tạo ở header List · `truncate` (KHKK tốt hơn chuẩn — **đừng hạ xuống**).

### ĐỢT C — CẦN QUYẾT ĐỊNH TRƯỚC KHI CODE

| # | Việc | Lane khuyến nghị | Cần owner |
|---|---|---|---|
| C1 | **O5** flatMode đổi Cây→Phẳng khi lọc | **GIỮ MÁY R-11, SỬA HÌNH**: bỏ `term` khỏi `flatMode` + gộp ô tìm + **công tắc Cây/Danh-sách** + chip lọc | Gật/bác |
| C2 | **O2** badge "SẮP" → 0 | Lane đo: **không GĐ nào còn đúng nghĩa "sắp"** (cả 4 đều fetch API thật); `SoonChip` **không export**, 8 hit nội bộ, **0 host phụ thuộc** ⇒ bỏ sạch, an toàn | Xác nhận "bỏ hết" = mọi GĐ |
| C3 | **P2-1** `Field` tô brand cho MỌI value ⟂ PE giữ slate | label giữ `text-brand-600`, **value → `text-slate-900`**, chừa màu cho winner/cảnh báo | Nhìn 1 lần |
| C4 | Nút **"Từ chối"**: PE có code nhưng **thực tế không hiện** (BE gỡ khỏi `nextPhases`, UAT S60) ⟂ KHKK VẪN hiện | Đừng tự cắt | Quyết |

### ĐỢT D — VIỆC LỚN

- **O6** (`usePipelineStages.ts:219` → `/purchase-evaluations?id=…&expand=1`): thêm callback **`onOpenPe`** mirror khuôn `stage2Options.onOpenKhkk` (ternary `:233`). **Không truyền ⇒ giữ navigate** ⇒ 5 host kia không đổi hành vi.
- **O3** hạng mục N-dòng + SUM tổng — **Model-A N-ROW, 0 migration**. Tổng giá duyệt ghi phía trên. 🔴 Người land phải mở đủ `Features.cs:1310-1360` + `CreateContractFromSigningPlanFeatures.cs:100-240` (lane chỉ grep, chưa soi hết) và soi `KhkkDetailPage.tsx:312` — **tách N dòng sẽ in N lần cho 1 NCC**, có thể phải gộp.
- **O4** shopdrawing đính kèm — **FE-only**, rail `dossierItemId` đã đủ 6 tầng từ Mig 69.
- **O7** nút edit trên card KHKK — mirror `PeListPanel` `onEditClick`.
- **O8** tầng trên ra cây thư mục **Dự án › Hạng mục** 2 cấp (hiện `byProject` mới 1 cấp).

---

## ③ CHECKLIST — acceptance ĐO ĐƯỢC

| # | Deliverable | Acceptance (đo được, không phải "trông ổn") |
|---|---|---|
| 1 | Đợt A land | `grep -c 'href={`/api/contract-signing-plans' KhkkWorkflowPanel.tsx` = **0**; control-dương `grep -c "responseType: 'blob'"` ≥ **1** |
| 2 | A2 mobile | DevTools 375px màn Thao tác: **bấm được "Tạo kế hoạch"** (hiện `<main>` bị `hidden`) |
| 3 | B1 gỡ card-accent | `grep -n 'card-accent' KhkkWorkflowPanel.tsx` = **0 hit**. 🔴 KHÔNG chấp nhận `border-0`/`shadow-none` (gotcha #66) |
| 4 | Mirror 2 app | `sha256sum` **từng cặp** file đụng tới — **khớp 100%**. O2+O6 chạm **5 cặp = 10 file**, nghiệm thu **5/5** |
| 5 | Build | `npm run build` **cả 2 app**, 0 `error TS`. 🔴 `cd` đúng thư mục mỗi lần — thời gian bất thường (`~500ms`) = dấu hiệu build nhầm app |
| 6 | Bundle prod | `curl` bundle + `grep` chuỗi mới **có** ∧ chuỗi cũ **vắng**, cả 2 host (gotcha #77). Mọi 0-hit phải kèm **control-dương** |
| 7 | 6 host cây | O2/O6 land xong: mở PE-list · GĐ3 ×2 · GĐ4 ×2 · KHKK-list — **5 host không truyền callback giữ nguyên hành vi navigate** |
| 8 | 🔴 Visual loop | **CẢ 5 LANE ĐỀU KHÔNG CÓ SCREENSHOT** (lane audit + dev-rig chặn: chỉ `:8080` LISTENING, API `:5443` không có, trang sau `ProtectedRoute` — gotcha #3). Mọi kết luận là **đọc mã, không phải đã nhìn**. ⇒ Người land **BẮT BUỘC** soi thật **375 / 1366 / 1440** trên **prod sau deploy** trước khi tuyên PASS |
| 9 | O3 tiền | Trước khi viết BE: **xác nhận 1 dòng với owner** cách đọc "sum tổng" — đây là tiền |

**Thứ tự land:** A → B → (chờ owner) C → D. A độc lập, land ngay. B không phụ thuộc C/D. D2 (`O3`) land cuối vì chạm dữ-liệu tiền.
