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: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— gateevaluation.endsBeforeCeo, khung emeraldborder-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— gateplan.endedByLevelFinalize, khung violetborder-violet-300 bg-violet-50 text-violet-800, 2 nhánh theoisApproved. - ≠ 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-390khai rõendedByLevelFinalizelà 3-NGẢ ở BE: phiếuDaDuyet→ cờ runtime thật (Mig 71); phiếu chưa xong → suy từ cấu hình, POINTER-AWARE (chỉ tính cấpallowApproverFinalizeở-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ùngbg-emerald-500 / bg-brand-600 / bg-slate-200, cùngh-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-410rounded 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-302rounded 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ùngml-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ùngtitley hệt. KHÔNG cần sửa. - Khác nguồn: PE đọc
lv.allowApproverFinalize(1 cấp) ⟂ KHKKgroup.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 lucideCheck/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>+HistoryTabrender 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; queryenabled: 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)
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óstatusBE precompute. Đừng "đơn giản hoá" theo PE.- Gom Cấp cùng
order(:289,:307) = OR-of-N khớpContractSigningPlanFeatures.cs:259-260. actingLevel+approverFinalizeEligible(:165-167) — mirrorResolveActingLevelBE; soi cờ của level khác = ô-tích nói dối.applyLevelFinalizelà opt-IN, resetfalsemỗi lần mở dialog (:102,:431-432) — F-1 S166 đã RATIFIED @S167.canSubmitcố ý không ẩn theo vai (:169-172, bug-class gotcha #44).- 5
invalidateQueriessau transition (:216-219) + 2 sau xóa (:230-231) — cópipeline-khkk-indexnuôi cây 4-folder S162. commentRequiredcho Return/Reject (:242-244).
5.3 Nghiệm thu (acceptance)
cd fe-admin && npm run build0 lỗi TS;cd fe-user && npm run build0 lỗi TS.sha256sum2 file KHKK panel → bằng nhau sau khi sửa.git diff --statchỉ hiện đúng 2 file đó.- Soi mắt cạnh nhau: mở
/purchase-evaluationschọn 1 phiếu ⟂ mở/contract-signing-planschọn 1 phiếu → panel phải cùng một hệ: không thẻ-trong-thẻ, cùng nhịpborder-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:18config.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. AttachmentPreviewDialogxuất sẵnisPreviewable(:11-14) nhưngPeWorkflowPanel.tsx:326tự 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ônPeDetailTabs.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