Files
solution-erp/.claude/workflows/runs/2026-08-04-S171-khkk-ui-mirror-pe/sub-investigator-codebase-invest-s171.md
2026-08-04 16:28:33 +07:00

44 KiB
Raw Blame History

sub-investigator-codebase-invest-s171 — KHKK UI mirror PE (fable-real LỆNH-A)

ENGINE-ĐẮT single top-model deep-pass. PROPOSE-ONLY. Mọi khẳng-định kèm file:line. Đường-dẫn tương-đối gốc fe-admin/src/ trừ khi ghi rõ khác. fe-user SHA256-identical (lead đã đo 4/4).

§0. Hai đính-chính vào Ground-truth của lead (đọc TRƯỚC §1)

Đ-1 — Panel 1 của PE KHÔNG phải PeListPanel. Lead ghi "PE panel 1/2/3 = PeListPanel · PeDetailTabs · PeWorkflowPanel". Đo lại: PurchaseEvaluationsListPage.tsx không import PeListPanel; panel 1 được viết INLINE trong chính file đó (:399-639). PeListPanel chỉ có một call-site duy nhất: pages/pe/PurchaseEvaluationWorkspacePage.tsx:84 (màn workspace 2-panel, grid-cols-[260px_1fr] :80). ⇒ Hệ quả cho §5: không có component list dùng-lại được để KHKK nhặt. Muốn "giống" panel 1 thì phải TÁCH nó ra trước, hoặc chép khuôn.

Đ-2 — Đã tồn tại một bản mirror PE 3-panel KHÁC, mới hơn: GĐ3 (Hợp đồng). pages/contracts/ContractsListPage.tsx:125 = grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[340px_1fr_360px], header :106-123 sao y PE, Panel 2 = ContractDetailContent, Panel 3 = WorkflowHistoryPanel (components/contracts/), và Panel 3 khi chưa chọn thì render PipelineTreePanel currentStage={3} (:249). ⇒ Đây là tiền-lệ trong nhà, cùng chuỗi giai-đoạn, làm ở S168. KHKK là giai-đoạn NẰM GIỮA GĐ1 (PE, 3-panel) và GĐ3 (HĐ, 3-panel) mà lại là trang-giấy-cuộn ⇒ nó là mắt xích lệch duy nhất của cả 4 giai-đoạn. Đây là lập-luận mạnh nhất ủng-hộ đề-bài owner, và nó không nằm trong ảnh — owner nhìn thấy triệu-chứng, cái này là nguyên-nhân.


§1. Bản-đồ 3 panel PE (mẫu)

1.0 Khung trang (container)

Hạng-mục Sự-thật file:line
Root <div className="flex h-[calc(100vh-4rem)] flex-col"> — trang CHIẾM TRỌN viewport, KHÔNG scroll body pages/pe/PurchaseEvaluationsListPage.tsx:381
Header 1 dòng <header>: icon ClipboardCheck + h1 tiêu-đề động + badge đếm rounded-full bg-slate-100 :382-390
Tiêu-đề động headerTitle đổi theo deletedView/typeFilter/pendingMe :289-293
Grid grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px] :397
URL-param lái toàn màn type · pendingMe · deleted · q · phase · awId · id · expand :38-50
2 query nuôi 3 panel list (['pe-list',{…}]) + detail (['pe-detail', selectedId], enabled: !!selectedId) :74-117

🔴 PE là một trang duy nhất; không có route detail riêng cho desktop. PurchaseEvaluationDetailPage (:782-807) chỉ là fallback fullpage mobile /purchase-evaluations/:id, và vẫn dựng đúng 2 component đó xếp dọc.

1.1 Panel 1 — LIST (trái, 400px)

<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white"> :399

  1. Khối filter space-y-2 border-b border-slate-200 p-3 :400
    • Search icon-trong-ô: Search absolute + <Input className="pl-8">, placeholder "Tìm mã / tên gói thầu / dự án…" :401-409
    • Select quy-trình duyệt — chỉ khi pendingMe :413-422
    • Banner đỏ "Phiếu đã xóa" :427-431 / banner amber "Lọc cố định: Đã gửi duyệt" :432-435 / Select trạng-thái :437-453 (ba-ngả loại-trừ)
  2. Vùng cuộn flex-1 overflow-y-auto :457
    • Skeleton 6× h-16 animate-pulse :458-464 · EmptyState icon={ClipboardCheck} :465-469
    • Cây 3 tầng <details>: 📅 Năm📁 Dự án🧱 Hạng mục :472-637; chevron group-open/year|proj|wi:rotate-90; badge đếm mỗi tầng; dựng ở yearGroups :220-262
    • Tầng 4 = <PipelineStageFolders currentStage={1} hideEmpty={filterActive} stages={buildStages(wg.items,{stage1Content:<ul>…</ul>})} /> :524-625 — 4 folder giai-đoạn; danh-sách phiếu bị RE-PARENT vào folder GĐ1
    • Card phiếu :530-621: hàng 1 PeUrgentChips + PeFinalizeChip + tên gói thầu + badge trạng-thái; hàng 2 mã font-mono + timestamp; hàng 3 👤 drafter · dept + ✓ HĐ
    • Nút Maximize2 "Xem mở rộng" absolute, opacity-0 … group-hover/row:opacity-100 :607-620
  3. Persist mở/đóng: localStorage key pe_list_expanded_projects_v3 :269-287

1.2 Panel 2 — DETAIL (giữa, 1fr)

<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block"> :643

  • Chưa chọn ⇒ EmptyState "Chọn phiếu ở danh sách" :644-652 (khớp Ảnh 1)
  • detail.data && !isExpand<PeDetailTabs evaluation onBack={closeDetail} onDelete readOnly={true} /> :657-664

Ruột PeDetailTabs (components/pe/PeDetailTabs.tsx, 3613 dòng):

  • Đầu phiếu: <PePipelineStrip current={1} /> :277 — dải 4 giai-đoạn
  • Banner phiếu Trả-lại (readOnly) :386-395
  • <div className="divide-y divide-slate-200"> :397 chứa 5 khối đánh số, đúng thứ-tự dọc:
    1. <Section title="1. Thông tin gói thầu"> :402
    2. 2. Hạng mục + Báo giá NCC (…) :405
    3. 3. Đơn vị NCC/TP được chọn :418
    4. 4. Thông tin chọn thầu<section> trần, header xanh :423-425
    5. <Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)"> :426-437LevelOpinionsSectionV2 khi có approvalWorkflowId, ngược lại fallback V1 DepartmentOpinionsSection :434-436
  • <Section> = <section className="px-3 py-3 sm:px-5 sm:py-4"> + h3 mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500 :514-523

Section 5 (khối owner khoanh đỏ ở Ảnh 3):666-774:

  • LevelOpinionsSectionV2 lặp flow.steps; mỗi Step tính totalLevels · totalApprovers · signedLevels :680-700
  • StepOpinionsBox :705-745: header Bước {n} — {name} uppercase bold + chip departmentName nền bg-emerald-50 + meta phải ml-auto {signedLevels}/{totalLevels} cấp đã duyệt · {totalApprovers} NV tham gia :718-733; rỗng ⇒ — Chưa có ý kiến duyệt. :736; có ⇒ grid grid-cols-1 gap-3 md:grid-cols-2 :738
  • StepOpinionEntry :747-774: card rounded-lg border border-emerald-200 bg-white p-3; Cấp {n} — {tên}; badge ⚠ Admin {X} duyệt thay khi signedByUserId !== approverUserId :748,756-760; badge ✓ Đã duyệt :762-764; body whitespace-pre-wrap :766; footer timestamp :769-771

1.3 Panel 3 — WORKFLOW (phải, 360px)

<aside className="hidden overflow-y-auto border-l border-slate-200 bg-white p-4 lg:block"> :668

  • Chưa chọn ⇒ hộp border-dashed + icon X + "Quy trình duyệt sẽ hiện khi chọn phiếu." :669-676 (khớp Ảnh 1)
  • Có phiếu ⇒ <PeWorkflowPanel evaluation readOnly={!pendingMe} onApproved onDeleted /> :677-684

Ruột PeWorkflowPanel (components/pe/PeWorkflowPanel.tsx, 1062 dòng) — thứ-tự dọc ĐÚNG như Ảnh 2:

  1. h3 Quy trình duyệt + meta {code} v{NN} · {name} :344-354
  2. Banner kết-thúc-sớm evaluation.endsBeforeCeo :360-377 — 2 nhánh: 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…"
  3. Sơ-đồ Bước → Cấp → NV :381-478: step.status do BE precompute; isStepAfterFinalize(step.order)opacity-50 :393 + chip Kết thúc tại {…} :411-418; chip tên phòng step.departmentName :406-410; per-Cấp badge ⚑ Duyệt thay CEO khi lv.allowApproverFinalize :441-448; NV liệt kê /-phân-cách :453-465 + badge ✎ NS PRO / ✎ NS CCM :458-463
  4. Fallback V1 "Phiếu này dùng quy trình cũ" :481-485
  5. Banner "đến lượt bạn" :489-508 (emerald/amber theo actorInV2Level)
  6. Banner S134 phiếu-trước-chưa-duyệt :512-515
  7. 3 nút + nút XÓA (S155, CẠNH next.map) :564 · dialog transition + 4 return-mode :633-707 + toggle skip-to-final :723-745 + CCM-done :749 + ô-tích level-finalize :769-791 + bộ chọn giá chốt :792 + upload file-khi-duyệt :821
  8. DeptApprovalsSection (legacy 2-cấp phòng ban) :920-924
  9. 📎 File đính kèm khi duyệt :928-967h3 + phụ-đề "File do người duyệt tải lên trong quá trình duyệt." + hàng file (Paperclip · tên · fmtFileSize · Eye xem trước · Download)
  10. Lịch sử duyệt (N)<PeApprovalsSection ev> :969-971, định-nghĩa ở PeDetailTabs.tsx:778-785 (export NGƯỢC về panel 3, import PeWorkflowPanel.tsx:29); ruột ApprovalsTab :3109-3219: merge ev.approvals + synthetic-reject dựng lại từ changelog + dedupe 5s-bucket :3152-3192; mỗi entry = badge quyết-định decisionBadge + hint extractNextTargetHint + timestamp + tên · ghi-chú :3197-3216
  11. Lịch sử thay đổi<PeHistorySection ev> :973-975HistoryTab :3222-3300 (GET /purchase-evaluations/{id}/changelogs, filter TraLai/gửi-lại/ngân-sách)

1.4 Overlay "Xem mở rộng" (expand=1) — thành-phần THỨ TƯ của mẫu

:694-776. fixed inset-0 z-50, backdrop bg-slate-900/40, panel translate-x-full → translate-x-0 200ms, mount-then-animate 2×rAF :308-336, khoá body.overflow :340-349, Esc + focus-trap :352-378. Body = 2 cột lg:flex-row: <main> PeDetailTabs + <aside lg:w-[24rem] xl:w-[26rem]> PeWorkflowPanel :753-772. ⇒ Cùng 2 component, 3 khung khác nhau (inline · overlay · route mobile). Đây là bằng-chứng cho §5.


§2. Bản-đồ KHKK hiện-trạng

2.0 Khung — KHÁC LOẠI, không phải khác CSS

Hạng-mục KHKK file:line
Root list <div className="space-y-5"> — trang cuộn theo body pages/khkk/KhkkListPage.tsx:161
Header <PageHeader eyebrow="Giai đoạn 2" title subtitle icon accent actions> (khuôn PURO) :162-192
Grid grid gap-5 lg:grid-cols-[19rem_minmax(0,1fr)] xl:grid-cols-[21rem_minmax(0,1fr)] = 2 panel :197
Panel trái <PipelineTreePanel currentStage={2} /> — cây TOÀN TRÌNH điều-hướng, không chứa phiếu KHKK :198
Panel phải 6 KpiCard lọc :202-220 + card search :222-233 + bảng 8 cột :239-339 + phân trang :341-355
Chọn phiếu onClick={() => navigate('/khkk/'+k.id)}rời trang :280
Route /khkk/list · /khkk/create · /khkk/:id App.tsx:107-110
Detail Trang riêng, KhkkWorkflowPanel render INLINE ngay dưới PageHeader, TRÊN mọi nội-dung pages/khkk/KhkkDetailPage.tsx:411
Detail sections 4 SectionCard rời: Thông tin :414 · Dòng giá per-NCC :505 · Căn cứ hồ sơ :626 · File đính kèm :721

2.1 Panel 1 (KHKK) — ĐANG SAI CHỖ, không phải "đang xấu"

  • Ô panel-1 hiện bị PipelineTreePanel chiếm. Đó là cây điều-hướng 4 giai-đoạn dựng từ chỉ-mục PE (components/pipeline/PipelineTreePanel.tsx:106-120, query ['pipeline-pe-index'], pageSize 200), không phải danh-sách phiếu KHKK.
  • Danh-sách phiếu KHKK nằm ở bảng 8 cột bên phải — tức nội-dung thuộc panel 1 đang bị đặt vào ô panel 2.
  • ⇒ KHKK VẮNG panel-1-theo-nghĩa-PE, và THỪA một widget khác nghiệp-vụ ở đúng chỗ đó.

2.2 Panel 2 (KHKK) — VẮNG ở màn list

Không ô nào ở /khkk/list hiển thị nội-dung phiếu. Nội-dung chỉ tồn-tại sau navigate('/khkk/:id').

2.3 Panel 3 (KHKK) — CÓ, nhưng ĐẶT SAI CHỖ và THIẾU 5 khối

pages/khkk/KhkkWorkflowPanel.tsx (491 dòng — 🔴 nằm trong pages/, không phải components/) đã mirror một phần PeWorkflowPanel:

Khối (theo Ảnh 2) KHKK file:line
Header Quy trình duyệt + meta (thiếu v{NN}) :194-209
Banner "đã kết thúc tại … không qua CEO" VẮNG — (§4 D-1)
Sơ-đồ Bước → Cấp ✓/●/○ nhưng FE tự suy status :106-120 · :213-266
Chip tên phòng của Bước VẮNG types/khkk.ts:275-282 (chỉ departmentId)
Badge ⚑ Duyệt thay CEO per-Cấp VẮNG (cờ allowApproverFinalize ĐÃ CÓ trên DTO, chỉ dùng cho ô-tích) types/khkk.ts:272 · :143
Làm mờ Bước/Cấp sau finalize VẮNG
Badge vai ✎ NS PRO/CCM VẮNG
📎 File đính kèm khi duyệt VẮNG — (§4 D-7)
Lịch sử duyệt (N) VẮNG — (§4 D-2)
Lịch sử thay đổi VẮNG — (§4 D-3)
Ý kiến cấp duyệt ⚠️ CÓ NHƯNG SAI PANEL — list phẳng 1 cột trong panel duyệt :376-412
3 nút + Dialog :299-374 · :415-488
Ô-tích "Cấp này KẾT THÚC" (opt-in) :451-470
4 return-mode cố-ý bỏ (W3 = TraLai toàn phần) :6-8 · :290
Bộ chọn giá chốt cố-ý bỏ (BE chốt ở finalize choke-point) :6-8 · :449-450

🔴 Delta owner nhìn thấy rõ nhất: khối "Ý kiến cấp duyệt" (Ảnh 3, khoanh đỏ) bên PE là Section 5 của panel 2, nhóm BƯỚC → PHÒNG, lưới 2 cột; bên KHKK nó là list phẳng nhét trong panel 3. Đây vừa sai panel, vừa sai hình.


§3. Bảng DELTA per-panel

Luật phán (hiến-pháp run): "hiện tại nó đang thế" KHÔNG phải lý-do ⇒ phán lại thành GIỐNG-được.

3.A Khung trang

# Hạng-mục PE làm sao KHKK làm sao Phán Lý-do
K1 Chiều cao flex h-[calc(100vh-4rem)] flex-col, không cuộn body (:381) space-y-5 giấy-cuộn (:161) GIỐNG-được 3-panel bắt buộc phải chiếm-trọn-viewport, nếu không mỗi panel không có thanh cuộn riêng. GĐ3 đã làm y hệt (ContractsListPage.tsx:106)
K2 Header <header> 1 dòng: icon + h1 + badge đếm (:382-390) <PageHeader> eyebrow/subtitle/actions (:162-192) GIỐNG-được Cùng chức-năng; GĐ1 và GĐ3 đều dùng header 1 dòng, chỉ GĐ2 lệch. Actions (nút "Lập kế hoạch") giữ ở góc phải header
K3 Grid [400px_1fr_360px] (:397) [19rem_minmax(0,1fr)] (:197) GIỐNG-được Đề-bài trực-tiếp
K4 Chọn phiếu URL ?id= in-page, <lg mới đi overlay (:152-158) navigate('/khkk/:id') rời trang (:280) GIỐNG-được GĐ3 giữ CẢ HAI: desktop ?id=, <lg navigate (ContractsListPage.tsx:90-96) — khuôn sẵn, chép được

3.B Panel 1

# Hạng-mục PE KHKK Phán Lý-do
P1-1 Nguồn cây cây phiếu PE 3 tầng + folder GĐ (:472-637) PipelineTreePanel cây điều-hướng (:198) GIỐNG-được Cùng bộ dựng cây Năm>Dự án>Hạng mục>4-folder. Chỉ cần đổ card phiếu KHKK vào content của folder GĐ2 y như PE đổ vào GĐ1 (§4 D-8)
P1-2 Ô search trong panel 1, icon-trong-ô (:401-409) card riêng giữa trang (:222-233) GIỐNG-được Cùng chức-năng, khác chỗ đặt
P1-3 Lọc trạng-thái <Select> 1 dòng (:437-453) 6 KpiCard hàng ngang (:202-220) GIỐNG-được KpiCard đẹp nhưng ăn hết bề ngang — không sống nổi trong cột 400px. Owner đã nói "đồng nhất" ⇒ Select thắng
P1-4 Lọc nhóm N1-N8 không có (PE có awId dropdown quy-trình :413-422) đọc từ URL ?group= (:79-80), 8 leaf menu KHÁC-buộc-phải-khác Nghiệp-vụ SP-002: 8 nhóm × 8 workflow KHKK-N{n} (types/khkk.ts:163), rào 409 ở BE (ContractSigningPlanFeatures.cs:420). PE không có trục này. 🔸 Nhưng CHỖ ĐẶT phải giống: dropdown trong khối filter panel 1, đúng ô mà PE đặt awId
P1-5 Dòng phiếu card 3 hàng + chip (:546-601) <tr> 8 cột (:277-337) GIỐNG-được Bảng 8 cột không vừa cột 400px; card 3 hàng chứa đủ: mã · gói thầu · trạng thái · Σ tiền · người soạn · ngày
P1-6 Phân trang không (pageSize 50 :101) 20/trang, nút Trước/Sau (:341-355) GIỐNG-được Không phải "bỏ phân trang" — GĐ3 đã có phân trang compact NGAY TRONG panel 1 (ContractsListPage.tsx:200-222). Chép khuôn đó
P1-7 Nút "Xem mở rộng" → overlay có (:607-620 + :694-776) không có GIỐNG-được Ưu-tiên P2 (xem §7) — không chặn P1
P1-8 Persist mở/đóng cây pe_list_expanded_projects_v3 (:269) pipeline_tree_expanded_v1 (PipelineTreePanel.tsx:21) GIỐNG-được Đã có sẵn, chỉ cần giữ key riêng cho GĐ2

3.C Panel 2

# Hạng-mục PE KHKK Phán Lý-do
P2-1 Dải 4 giai-đoạn đầu phiếu <PePipelineStrip current={1} /> (PeDetailTabs.tsx:277) không có GIỐNG-được Component đã dùng-chung được (ComingSoonPage.tsx:33 truyền current động) ⇒ current={2}
P2-2 Khung Section divide-y divide-slate-200 + <Section> h3 uppercase tracking-wide đánh số (:397 · :514-523) 4 SectionCard card-accent rời, không đánh số (:414 :505 :626 :721) GIỐNG-được Đây đúng là "tương tự ⇒ giống nhau". Đổi SectionCardSection đánh số + divide-y
P2-3 Nội-dung các Section 1 gói thầu · 2 hạng mục+báo giá NCC · 3 chọn NCC · 4 thông tin chọn thầu Thông tin · Dòng giá per-NCC · Căn cứ hồ sơ b.8-9 (TVGS) · File đính kèm KHÁC-buộc-phải-khác Nghiệp-vụ GĐ2 ≠ GĐ1: KHKK không có báo-giá/so-sánh NCC (đã chốt ở GĐ1), căn-cứ hồ-sơ TVGS (ContractSigningPlanDossierItem, types/khkk.ts:67-99)
P2-4 Section CUỐI = Ý kiến cấp duyệt Section 5, dưới cùng panel 2 (:426-437) khối nằm trong panel 3 (KhkkWorkflowPanel.tsx:376-412) GIỐNG-được 🔴 Chính là ô owner khoanh đỏ Ảnh 3 — "content nhập phía trên, duyệt ghi chú phía dưới". Phải DỜI PANEL
P2-5 Nhóm ý-kiến theo BƯỚC→PHÒNG + meta x/y cấp · N NV StepOpinionsBox (:705-745) list phẳng theo opinions[] GIỐNG-được Chỉ cần workflowSteps (ĐÃ CÓ) + departmentName (§4 D-4)
P2-6 Lưới 2 cột card ý-kiến md:grid-cols-2 + card emerald + badge ✓ Đã duyệt + ⚠ … duyệt thay + placeholder (duyệt — không ý kiến) (:738-772) <li> 1 cột text-[11px] (đã có nhãn duyệt thay + placeholder) GIỐNG-được Dữ-liệu đã đủ 100% (KhkkLevelOpinionDto types/khkk.ts:245-258approverUserId/signedByUserId/comment/signedAt)
P2-7 Sửa nội-dung panel 2 readOnly={true} cứng (:663); sửa ở workspace riêng (PurchaseEvaluationWorkspacePage) sửa inline ngay trong detail (nút Sửa/Lưu, KhkkDetailPage.tsx:422-427) KHÁC-buộc-phải-khác (có điều-kiện) KHKK không có trang workspace. Ép readOnly mà chưa dựng workspace = cắt mất đường sửa duy nhất ⇒ mất chức-năng. 🔸 Điều-kiện: khi nào có workspace KHKK thì dòng này lật lại thành GIỐNG-được. Phải khai trong spec, đừng để thành di-sản im-lặng

3.D Panel 3

# Hạng-mục PE KHKK Phán Lý-do
P3-1 Meta workflow {code} v{NN} · {name} (:348-353) {code} · {name} (:203-208) GIỐNG-được Thiếu version (§4 D-5)
P3-2 Banner kết-thúc-sớm endsBeforeCeo 2 nhánh (:360-377) VẮNG GIỐNG-được Cơ-chế ĐÃ port ở K3 (cột EndedByLevelFinalize Mig 71), chỉ chưa expose (§4 D-1). Ảnh 2 có banner này ở vị-trí #2
P3-3 Sơ-đồ Bước→Cấp ✓/●/○ BE precompute status FE tự suy (:106-120) ĐÃ GIỐNG (hình) Khác ở tầng dữ-liệu, không khác ở tầng nhìn. Không ép BE precompute — rủi-ro > lợi
P3-4 Chip tên phòng của Bước có (:406-410) VẮNG GIỐNG-được §4 D-4
P3-5 Badge ⚑ Duyệt thay CEO per-Cấp có (:441-448) VẮNG GIỐNG-được 0 công BEallowApproverFinalize đã nằm trên KhkkWorkflowLevelDto (types/khkk.ts:272)
P3-6 Làm mờ Bước/Cấp sau finalize + chip Kết thúc tại … có (:393 :411-418 :425) VẮNG GIỐNG-được Suy được từ P3-5 + workflowSteps — 0 công BE
P3-7 Badge ✎ NS PRO / ✎ NS CCM có (:458-463) VẮNG KHÁC-buộc-phải-khác Quyền nhập ngân sách PRO/CCM là khái-niệm riêng của PE (PeWorkItemBudgets, Mig 50). KHKK không có cột ngân sách per-role ⇒ badge sẽ vô nghĩa
P3-8 📎 File đính kèm khi duyệt có, tự-ẩn khi rỗng (:928-967) VẮNG GIỐNG-được (có ngoặc) Khối + hành-vi tự-ẩn = giống được ngay. 🔸 Nhưng CHO PHÉP người duyệt upload là quyết-định owner, không tự làm: cần thêm ApprovalAttachment vào enum purpose (§4 D-7). Khuyến-nghị: dựng khối, đọc purpose=Other, đợi owner chốt vế upload
P3-9 Lịch sử duyệt (N) có (:969-971:3109-3219) VẮNG GIỐNG-được 🔴 Dữ-liệu ĐÃ ĐƯỢC GHIContractSigningPlanWorkflowService.cs:534. Chỉ chưa expose (§4 D-2)
P3-10 Lịch sử thay đổi (changelog) có (:973-975:3222-3300) VẮNG GIỐNG-được 🔴 Changelog ĐÃ GHI (ContractSigningPlanWorkflowService.cs:547, và ≥8 chỗ trong Features.cs), chỉ thiếu endpoint đọc (§4 D-3)
P3-11 3 nút + dialog + ô ý-kiến có (:299-374 :415-488) ĐÃ GIỐNG Không đụng
P3-12 4 return-mode (Cấp / Bước / NV / Drafter) có (:633-707) 1 mode (về Bước 1·Cấp 1) (:290) KHÁC-buộc-phải-khác Chuỗi KHKK là 3 trạm ngắn (PRO→CCM→CEO) — "lùi 1 cấp" gần trùng "về đầu". Thêm 4 mode = đổi state machine BE, ngoài phạm-vi UI. 🔸 Khai rõ, không giấu
P3-13 Bộ chọn giá chốt khi duyệt có (:792) không KHÁC-buộc-phải-khác KHKK chốt giá per-Line tại choke-point BE ApplyApprovedValuesOnFinalize (KhkkWorkflowPanel.tsx:449-450) — không có "1 giá" để người duyệt chọn
P3-14 Nút XÓA phiếu trong panel duyệt có, AllowApproverDelete Mig 68 (:564) có nhưng ở PageHeader (KhkkDetailPage.tsx:388-399) GIỐNG-được Chỉ là dời vị-trí + dialog xác-nhận riêng
P3-15 readOnly={!pendingMe} ẩn nút thao-tác ở view Danh sách có (:680) luôn hiện GIỐNG-được Cần khi panel 3 sống trong màn list
P3-16 Empty-state panel 3 khi chưa chọn hộp dashed + X + câu (:669-676) n/a GIỐNG-được Ảnh 1 có. 🔸 GĐ3 chọn cách khác: chưa chọn ⇒ nhét PipelineTreePanel vào panel 3 (ContractsListPage.tsx:249) — đây là lối thoát cho cây toàn-trình mà KHKK đang giữ ở panel 1

Tổng: 27 GIỐNG-được · 6 KHÁC-buộc-phải-khác · 2 ĐÃ GIỐNG (không việc).


§4. Data-gap — KHKK có đủ trường nuôi panel 3 + Section 5 không?

🔴 Kết-luận đầu tiên, quan trọng nhất: gần như toàn bộ dữ-liệu ĐÃ TỒN TẠI Ở DB VÀ ĐANG ĐƯỢC GHI. Khoảng trống nằm ở tầng đọc (DTO/endpoint), không phải tầng ghi. Đây là tin tốt: chi-phí BE nhỏ hơn nhiều so với ước-lượng "phải làm lại history".

# Thiếu cái gì Trạng-thái thật ở BE Ai cần Việc phải làm
D-1 Banner kết-thúc-sớm: endedByLevelFinalize + finalizeStepName + finalizeLevelName Cột EndedByLevelFinalize ĐÃ CÓ (Mig 71). ContractSigningPlanDetailDto (ContractSigningPlanFeatures.cs:145-181) KHÔNG expose — đã tự khai ở types/khkk.ts:299-300 ("cột Mig 71 nằm im chờ K3 tiêu thụ") P3-2 +3 field vào DetailDto (append cuối, đúng convention :174-176) + 3 field vào KhkkDetailDto
D-2 Lịch sử duyệt — danh-sách transition 🔴 ĐANG GHI THẬT: ContractSigningPlanWorkflowService.cs:534 db.ContractSigningPlanApprovals.Add(new ContractSigningPlanApproval{FromPhase,ToPhase,Decision,Comment,ApprovalWorkflowLevelId,ApprovedByUserId,ApprovedAt}). Entity ContractSigningPlanApproval.cs:17-31. Bảng ContractSigningPlanApprovals + index (ContractSigningPlanId, ApprovedAt) (Mig 69 :247-248). DTO không có Approvals P3-9 +List<ContractSigningPlanApprovalDto> Approvals vào DetailDto. Cẩn thận: entity chỉ có ApprovedByUserId, không có tên ⇒ projection phải join Users (BE đã có ResolveActorFullNameAsync, WorkflowService.cs:552)
D-3 Lịch sử thay đổi — changelog 🔴 ĐANG GHI THẬT ≥9 chỗ: WorkflowService.cs:547 + Features.cs:497,562,639,1078,1116,1224,1304. Có UserName đã denorm ⇒ không cần join. KHÔNG có endpoint đọcContractSigningPlansController.cs không có route changelogs nào P3-10 +GET /contract-signing-plans/{id}/changelogs policy KeHoachKyKet.Read (mirror PE PeDetailTabs.tsx:3225)
D-4 departmentName của Bước ContractSigningPlanWorkflowStepDto chỉ có DepartmentId — tự khai ở types/khkk.ts:275 ("KHÔNG có tên phòng") P2-5 P3-4 (2 chỗ) +DepartmentName vào StepDto (join Departments)
D-5 workflowVersion DetailDto có WorkflowCode + WorkflowName, không có version (Features.cs:160-161) P3-1 +1 field
D-6 Tên người trong lịch-sử duyệt (xem D-2) ApprovedByUserId không kèm tên P3-9 Join hoặc denorm giống Changelog.UserName
D-7 Purpose "file khi duyệt" Enum ContractSigningPlanAttachmentPurpose chỉ có DossierScan=1 · PlanDocument=2 · Other=99 (ContractSigningPlanAttachment.cs:7-12) — không có giá-trị cho "file người duyệt tải lên" P3-8 🔸 Quyết-định owner: (a) dựng khối chỉ-đọc lọc purpose=Other — 0 công BE; (b) +ApprovalAttachment=3 + cho upload trong dialog duyệt — đổi enum + endpoint. Đừng tự chọn (b)
D-8 stage2Content / stage2Count BuildStagesOptions (hooks/usePipelineStages.ts:163) chỉ có stage1Content/stage1Count; stage 2 dựng bằng leaves (link) :265. PipelineStageFolders đã hỗ-trợ content tổng-quát (PipelineStageFolders.tsx:88 + render :285-286) P1-1 Mở rộng ĐỐI-XỨNG: stage2Content/stage2Count — sửa 1 hook, 0 đụng component trình-bày
D-9 chưa đo được Cách usePipelineStages nạp phiếu KHKK để dựng leaves stage-2 (:265-315) — tôi chưa đọc đoạn này. Nó quyết-định card KHKK trong folder GĐ2 lấy dữ-liệu từ đâu (query sẵn hay phải thêm) P1-1 PHẢI đọc hooks/usePipelineStages.ts:150-320 trước khi code panel 1

Trả lời thẳng câu hỏi §4: workflow steps/levels đủ · ý-kiến per-cấp đủ (kể cả cờ "duyệt thay") · lịch-sử duyệt ⚠️ có ở DB, thiếu ở API · file-khi-duyệt chưa có khái-niệm.


§5. Kiến-trúc đề-xuất

Phương-án A — tạo components/khkk/ riêng, chép khuôn PE

  • Làm: mkdir components/khkk/KhkkListPanel.tsx · KhkkDetailContent.tsx (+ KhkkLevelOpinionsSection.tsx) · dời pages/khkk/KhkkWorkflowPanel.tsxcomponents/khkk/. KhkkListPage trở thành host 3-panel.
  • Được: 0 rủi-ro cho PE (đang chạy prod, 45 phiếu) và GĐ3. Mỗi module giữ UX riêng — đúng nguyên-tắc đã ghi trong CLAUDE.md ("duplicate giữa 2 app là CÓ CHỦ ĐÍCH"). Diff dễ review. Mirror 2-app đơn-giản (copy nguyên file).
  • Mất: trùng-lặp thật. Sửa "cách hiển-thị ý-kiến cấp duyệt" sau này phải sửa ≥3 nơi (PE · KHKK · HĐ).

Phương-án B — tổng-quát-hoá component PE thành components/approval/ dùng chung

  • Làm: trích StepOpinionsBox/StepOpinionEntry (PeDetailTabs.tsx:705-774) và bộ sơ-đồ Bước→Cấp (PeWorkflowPanel.tsx:381-478) thành component generic nhận steps[] · opinions[] · finalizeInfo.
  • Được: 1 nguồn sự-thật cho 3 module (PE · KHKK · HĐ).
  • Mất: 🔴 shape KHÁC NHAU THẬT. PE nhận approvalFlow status do BE precompute (PeWorkflowPanel.tsx:384); KHKK nhận cây thô, FE tự suy (KhkkWorkflowPanel.tsx:22-29,106-120). Generic hoá = phải hoặc (i) ép BE KHKK precompute, hoặc (ii) nhồi adapter 2 chiều. Cả hai đều đụng vào đường duyệt của PE đang chạy prod — rủi-ro không cân với lợi ích thẩm-mỹ. Thêm nữa PE có ≥6 nhánh riêng (NS PRO/CCM · price-picker · 4 return-mode · CCM-done · skip-to-final · DeptApprovals legacy) mà KHKK không có: generic sẽ đầy if (module === …).

🔴 CHỌN: Phương-án A, kèm 2 điều-kiện

  1. Điều-kiện 1 — lấy khuôn từ GĐ3, không phải từ PE. ContractsListPage.tsx (255 dòng) đã là bản PE-3-panel đã được gọt gọn, làm ở S168, cùng chuỗi giai-đoạn, đã qua gate. Chép từ đó rẻ hơn và ít bẫy hơn chép từ PurchaseEvaluationsListPage.tsx (807 dòng, mang theo deleted/pendingMe/awId/overlay/2 hệ <details>).
  2. Điều-kiện 2 — nợ kỹ-thuật khai ngay, không giấu. Ghi vào đầu file components/khkk/ một dòng: "Khối ý-kiến cấp duyệt là bản thứ 3 của cùng một hình (PE PeDetailTabs.tsx:705-774 · HĐ WorkflowHistoryPanel.tsx). Gộp được khi và chỉ khi BE 3 module thống-nhất shape status precompute." — biến trùng-lặp từ tai-nạn thành quyết-định có điều-kiện thoát.

Bố-cục đích (đề-xuất)

/khkk/list                     ← host 3-panel, khuôn ContractsListPage
├─ Panel 1  400px   KhkkListPanel        cây Năm>Dự án>Hạng mục>folder GĐ2 → card phiếu KHKK
│                                        + search + Select trạng-thái + Select nhóm N1-N8
├─ Panel 2  1fr     KhkkDetailContent    PePipelineStrip current={2}
│                                        Section 1 Thông tin kế hoạch
│                                        Section 2 Dòng giá per-NCC trúng thầu
│                                        Section 3 Căn cứ hồ sơ (b.8-9)
│                                        Section 4 File đính kèm
│                                        Section 5 Ý KIẾN CẤP DUYỆT   ← Ảnh 3 khoanh đỏ
└─ Panel 3  360px   KhkkWorkflowPanel    chưa chọn → PipelineTreePanel currentStage={2}
                                         (khuôn ContractsListPage.tsx:249)
                                         đã chọn  → 1 Quy trình duyệt + meta
                                                    2 Banner kết-thúc-sớm
                                                    3 Sơ-đồ Bước→Cấp (+chip phòng, +⚑, +mờ-sau-finalize)
                                                    4 Nút Duyệt/Trả lại/Từ chối + Dialog
                                                    5 📎 File đính kèm khi duyệt
                                                    6 Lịch sử duyệt (N)
                                                    7 Lịch sử thay đổi

/khkk/:id GIỮ NGUYÊN route (fallback <lg + deep-link + bookmark cũ) nhưng ruột đổi thành KhkkDetailContent + KhkkWorkflowPanel xếp dọc — đúng khuôn PurchaseEvaluationDetailPage (:798-805).


§6. Rủi-ro / bẫy

# Bẫy Bằng-chứng Phòng
R-1 🔴 fe-admin KHÔNG hiển thị menu KHKK. ADMIN_HIDDEN_KEYS chứa 'KeHoachKyKet' (K6/QĐ7 S164) ⇒ toàn nhánh Khkk_* bị cắt khỏi sidebar admin. Cũng cắt PurchaseEvaluationsContracts components/Layout.tsx:227-231 Ảnh owner gửi là fe-user, không phải fe-admin. ⇒ UAT phải nghiệm-thu trên fe-user:8080; mirror 2 app là BẮT BUỘC, không phải tuỳ chọn. Deep-link /khkk/list bên admin vẫn vào được (:212-216 — display-layer ≠ authz-layer, gotcha #82)
R-2 gotcha #85 — dùng ĐÚNG key policy của endpoint. Mọi route KHKK gate KeHoachKyKet.{Read/Create/Update/Delete}; riêng bắc-cầu HĐ gate Contracts.Create ContractSigningPlansController.cs:29,37,87,96,118,223,244 · hooks/usePipelineStages.ts:11-17,57 Panel mới không OR key con (Khkk_List…). Nếu thêm endpoint changelogs (D-3) ⇒ [Authorize(Policy="KeHoachKyKet.Read")], không để [Authorize] trần
R-3 Pattern 16-bis 4-place mirror. Menu-key mới/route mới phải có mặt ở: BE seed · Layout.tsx staticMap/regex · App.tsx route · resolvePath. Thiếu 1 ⇒ leaf DROP IM LẶNG (gotcha #50) Layout.tsx:37-45,181-188 · App.tsx:107-110 Việc này không đẻ menu-key mới (giữ /khkk/list) ⇒ rủi-ro thấp. Nhưng nếu ai thêm ?id= vào staticMap thì phải soát lại menu-highlight
R-4 🔴 Chip lọc CỐ Ý không ghi URL. KhkkListPage.tsx:12-13,211-216 — ghi URL sẽ làm sidebar sáng nhầm leaf (bug UAT S155) :82-102 Panel 1 mới: q/phase ghi URL kiểu PE sẽ hồi-sinh bug S155. 🔸 Đề-xuất: ?id= ghi URL (cần bookmark) nhưng phase/group giữ ở lớp override (state), y như hiện tại. Đây là chỗ dễ làm hỏng nhất khi "chép khuôn PE cho giống"
R-5 8 nhóm dùng CHUNG 1 route ⇒ mọi thứ đọc từ URL phải đọc mỗi render, không useState khởi-tạo-một-lần (đi leaf→leaf không remount) :15-19,70-102 (vá-5 K4b) Host 3-panel mới phải giữ nguyên cơ-chế navKey/lastNavKey (:95-101), đừng viết lại bằng useEffect
R-6 PipelineTreePanel currentStage + bodyMaxHClass. Trong host 3-panel (Panel-3 tự cuộn) phải truyền bodyMaxHClass="max-h-[28rem] lg:max-h-none" nếu không sẽ 2 thanh cuộn lồng nhau (~26px vượt) ContractsListPage.tsx:247-249 · PipelineTreePanel.tsx:93-97,221 Chép nguyên chuỗi đó. Nếu đặt cây ở panel 1 thì giữ mặc-định
R-7 Mirror SHA256 2 app. 4 file KHKK + PipelineTreePanel + usePipelineStages đều khai "MIRROR SHA256 identical" KhkkListPage.tsx:3-4 · types/khkk.ts:2 · PipelineTreePanel.tsx:6 Acceptance §7 A-9: sha256 từng cặp, không "nhìn thấy giống"
R-8 Không có phiếu V1-legacy ở KHKK. Module sinh 2026-07-29 (Mig 69), mọi phiếu đều V2 types/khkk.ts:1 · KhkkWorkflowPanel.tsx:261-266 (chỉ fallback "chưa cấu hình Bước/Cấp") Không port nhánh fallback V1 của PE (PeDetailTabs.tsx:434-436, PeWorkflowPanel.tsx:481-485) — sẽ là code chết
R-9 Route hiện có không được phá. /khkk/:id đang là đích của navigate ở list (:280), của nút "Danh sách" (KhkkDetailPage.tsx:400), và có thể của link ngoài App.tsx:110 Giữ route; chỉ đổi ruột. Thêm ?id= cho desktop, navigate cho <lg (khuôn ContractsListPage.tsx:90-96)
R-10 budgetFrozen display-gate. Phiếu DaDuyet đọc ApprovedAmount, phiếu chưa duyệt đọc ProposedAmount types/khkk.ts:296-301 · Features.cs:177-181 Card panel 1 hiện Σ tiền phải theo đúng cờ này, nếu không sẽ nói 2 con số khác nhau cho cùng 1 phiếu (đúng lỗi vá-8 K2 đã sửa ở bảng — KhkkListPage.tsx:247-250)
R-11 Cây tải tối đa 200 phiếu. PE_INDEX_PAGE_SIZE = 200, có dòng cảnh-báo khi chạm trần PipelineTreePanel.tsx:22,252-256 Nếu panel 1 dựng cây từ chỉ-mục PE thì phiếu KHKK của gói cũ hơn sẽ không xuất hiện. Phải giữ (và thấy) dòng cảnh-báo đó
R-12 KhkkWorkflowPanel đang ở pages/, không phải components/ ⇒ import chéo pages→pages (KhkkDetailPage.tsx:41) KhkkDetailPage.tsx:41 Dời file = đổi import ở ≥2 nơi × 2 app. Việc nhỏ nhưng dễ sót ở app thứ hai

§7. Checklist thực-thi ĐO ĐƯỢC

Mỗi mục có lệnh/grep kiểm được. Chạy tại repo-root. APP ∈ {fe-admin,fe-user} — mọi mục phải PASS cả hai.

Nhóm 0 — bắt buộc TRƯỚC khi viết dòng code đầu tiên

# Việc Acceptance ĐO ĐƯỢC
A-0 Đọc hooks/usePipelineStages.ts:150-320 để chốt nguồn dữ-liệu phiếu KHKK cho folder GĐ2 (§4 D-9) Ghi vào spec 1 dòng: query nào nuôi stage-2, có KhkkListItemDto sẵn hay phải thêm fetch. Không có dòng này ⇒ không được bắt đầu A-3

Nhóm 1 — BE (nhỏ, 1 lượt)

# Việc Acceptance
A-1 DetailDto +5 field: WorkflowVersion · EndedByLevelFinalize · FinalizeStepName · FinalizeLevelName · List<…ApprovalDto> Approvals; StepDto +DepartmentName grep -n "record ContractSigningPlanDetailDto" -A 70 src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs → thấy đủ 5 tên; grep -c "DepartmentName" …WorkflowStepDto ≥1
A-2 GET /contract-signing-plans/{id}/changelogs policy KeHoachKyKet.Read grep -n "changelogs" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs → 1 hit, và dòng [Authorize(Policy = "KeHoachKyKet.Read")] đứng ngay trên (gotcha #85 / #82)
A-2b Tên người trong Approvals không rỗng Gọi thật 1 phiếu đã duyệt: response approvals[].approvedByFullName ≠ null cho ≥1 phần tử. 🔴 Không nghiệm-thu bằng "code trông đúng"

Nhóm 2 — FE cấu-trúc

# Việc Acceptance
A-3 $APP/src/components/khkk/ tồn-tại, chứa KhkkListPanel.tsx · KhkkDetailContent.tsx · KhkkWorkflowPanel.tsx (dời từ pages/) ls $APP/src/components/khkk/ → 3 file; grep -rn "pages/khkk/KhkkWorkflowPanel" $APP/src0 hit
A-4 KhkkListPage là host 3-panel grep -n "lg:grid-cols-\[" $APP/src/pages/khkk/KhkkListPage.tsx → khớp ^.*lg:grid-cols-\[[0-9]+px_1fr_[0-9]+px\] ; và grep -c "h-\[calc(100vh-4rem)\]" = 1
A-5 Chọn phiếu = ?id= (desktop) / navigate (<lg), route /khkk/:id CÒN SỐNG grep -n "matchMedia('(min-width: 1024px)')" $APP/src/pages/khkk/KhkkListPage.tsx ≥1 · grep -n 'path="/khkk/:id"' $APP/src/App.tsx = 1
A-6 Bộ lọc trạng-thái/nhóm KHÔNG ghi vào URL (R-4) grep -n "setSearchParams|setSp(" $APP/src/pages/khkk/KhkkListPage.tsx → chỉ xuất hiện trong hàm set id; 0 hit cho 'phase'/'group' bên trong lời gọi đó

Nhóm 3 — FE nội-dung (từng khối của ảnh)

# Khối Acceptance
A-7a Panel 2 có PePipelineStrip current={2} grep -n "PePipelineStrip" $APP/src/components/khkk/KhkkDetailContent.tsx → 1 hit kèm current={2}
A-7b Panel 2 có 5 Section đánh số, Section CUỐI = ý-kiến cấp duyệt grep -oE 'title="[0-9]\. [^"]+"' $APP/src/components/khkk/KhkkDetailContent.tsx → 5 dòng, dòng cuối khớp ^title="5\. Ý kiến cấp duyệt
A-7c Ý-kiến nhóm theo BƯỚC + lưới 2 cột + meta x/y cấp · N NV grep -n "md:grid-cols-2" …KhkkDetailContent.tsx ≥1 grep -c "cấp đã duyệt" ≥1 grep -c "NV tham gia" ≥1
A-7d Ý-kiến KHÔNG CÒN ở panel 3 grep -c "Ý kiến cấp duyệt" $APP/src/components/khkk/KhkkWorkflowPanel.tsx = 0
A-8a Panel 3 có banner kết-thúc-sớm grep -c "không qua CEO" …KhkkWorkflowPanel.tsx ≥1
A-8b Panel 3 có chip tên phòng + badge ⚑ Duyệt thay CEO + làm mờ sau finalize 3 grep: departmentName ≥1 · Duyệt thay CEO ≥1 · opacity-50 ≥1
A-8c Panel 3 có 📎 File đính kèm khi duyệt grep -c "File đính kèm khi duyệt" ≥1
A-8d Panel 3 có Lịch sử duyệt (N) + Lịch sử thay đổi grep -c "Lịch sử duyệt" ≥1 grep -c "Lịch sử thay đổi" ≥1
A-8e Panel 3 chưa chọn ⇒ PipelineTreePanel currentStage={2} bodyMaxHClass=… (R-6) grep -n "bodyMaxHClass" $APP/src/pages/khkk/KhkkListPage.tsx → 1 hit chứa lg:max-h-none

Nhóm 4 — chống hồi-quy

# Việc Acceptance
A-9 Mirror 2 app byte-identical Với mỗi file mới/sửa: sha256sum fe-admin/src/<p> = sha256sum fe-user/src/<p>PASS 100%, không trừ file nào
A-10 PE và HĐ không bị đụng git diff --name-only0 đường-dẫn khớp components/pe/ · pages/pe/ · components/contracts/ · pages/contracts/. Nếu có ⇒ dừng, giải-trình
A-11 Build + typecheck sạch npm --prefix fe-admin run build npm --prefix fe-user run build → exit 0. 🔴 Chỉ tin build chạy SAU khi mọi agent nền dừng (gotcha #68)
A-12 Test BE không vỡ dotnet test SolutionErp.slnx → 0 failed
A-13 Quyền không bị nới im-lặng grep -rn "Authorize" src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs | grep -c "Policy" — số này phải ≥ số route, và 0 dòng [Authorize] trần (gotcha #82)
A-14 Nghiệm-thu THẬT trên fe-user:8080 (R-1), không phải fe-admin Chụp 3 màn khớp Ảnh 1/2/3; kiểm bằng cửa-sổ ẩn-danh (bài S96 #77: cache ≠ deploy)

Nhóm 5 — chờ owner, KHÔNG tự làm

# Việc Vì sao phải hỏi
A-15 Cho phép người duyệt upload file khi duyệt (D-7, thêm ApprovalAttachment=3) Đổi enum + endpoint + quy-tắc lưu-trữ hồ-sơ. Ảnh 2 có khối này nhưng owner chưa nói KHKK cần upload
A-16 Panel-2 chuyển sang readOnly cứng + dựng workspace KHKK riêng (P2-7) Làm nửa vời = cắt mất đường sửa duy nhất của phiếu Nháp
A-17 Overlay "Xem mở rộng" cho KHKK (P1-7) Chi-phí cao (focus-trap + 2×rAF + khoá scroll), owner chưa nêu. Đề-xuất P2

§8. Ba câu tôi muốn lead đọc kỹ nhất

  1. Không phải "sửa CSS", mà là "đưa GĐ2 về đúng lớp kiến-trúc mà GĐ1 và GĐ3 đang đứng". GĐ2 là mắt-xích lệch duy nhất trong 4 giai-đoạn (§0 Đ-2). Nói với owner theo cách này thì phạm-vi công-việc tự giải-thích.
  2. Dữ-liệu lịch-sử duyệt của KHKK ĐÃ ĐƯỢC GHI TỪ ĐẦU (ContractSigningPlanWorkflowService.cs:534 + :547) — chỉ chưa có cửa đọc. Đừng ước-lượng việc này như "phải xây lại history"; nó là ~5 field DTO + 1 endpoint. Đây là chỗ dễ ước-lượng sai đắt nhất theo cả hai chiều.
  3. Chỗ dễ làm hỏng nhất khi "chép PE cho giống" là R-4 (ghi bộ lọc vào URL làm sidebar sáng nhầm — bug S155 đã đốt một lần) và R-1 (nghiệm-thu nhầm app). Cả hai đều không nhìn thấy được trong ảnh owner gửi.

END investigator-codebase-invest-s171 — VERDICT=27 giống-được / 6 buộc-khác / 8 data-gap