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

34 KiB

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:668KhkkListPage.tsx:186giố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 endsBeforeCeoemerald 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-5tổ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õ endedByLevelFinalize3-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.canEditProBudgetbg-amber-100 text-amber-700 text-[8px] "✎ NS PRO"; a.canEditCcmBudgetbg-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-467VẪ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-505thẻ <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-971PeDetailTabs.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-975PeDetailTabs.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 :186mặ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,:712khá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-600bg-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-600border-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 (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. applyLevelFinalizeopt-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