[CLAUDE] Contract: K3 SP-002 8 workflow nhom KHKK-N1-8 + port level-finalize + designer (0-mig) + 5 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m10s

Seed 8 workflow type-10 theo ROSTER LIVE prod (QT-DN-V2-001, lead verify tay
@SQLEXPRESS: 3 tram PRO->CCM->CEO, 11 nguoi, resolve EMAIL, idempotent per-Code;
co Finalize CHI cap chuong.phan). Port nhanh level-finalize mirror PE :866-876
(EndedByLevelFinalize K2-de-K3-tieu + ApplyApprovedValuesOnFinalize truoc Phase;
opt-out applyLevelFinalize 3-tang interface/service/controller, default true).
Sweep ep-false va-3: GO CREATE ep-false Finalize type-10 (OG-9 supersede PHA-VO
S155); GIU PUT-Conflict (luat versioning CHUNG - spec liet nham, khai lech);
GIU ep-false SkipToFinal type-10 (K3-test O-1 chong config-lie mới) + FE disable
o SkipToFinal cho KHKK (gate-K3 F-2). Rao (v) pin-dung-nhom regex KHKK-N[1-8].
+8 MenuKeys AwV2_KhkkN* (All 56->64, policies 224->256 may-dem) + 8 menu-row +
resolvePath fe-admin phep-am Khkk_ 0-hit. 5 test K3 (T-a..T-e, anti-tautology).
Gate K3 PASS-WITH-FLAGS 8f/0-blocker (sub-reviewer-gate-k3.md): F-1 checkbox
opt-out man duyet = KHAI HANDOFF dau-viec-#1 lan sau (tram CEO tam thoi qua
Designer bo co); F-2 va; F-3 dong truoc do. Suite 614/0 (45D+569I). K3-BE 2 luot
engine-exit -> che nho + lead-solo (bai: task-hep-Write-som song).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-31 18:21:39 +07:00
parent d6cb5469b3
commit 15349e8ee0
15 changed files with 766 additions and 83 deletions

View File

@ -173,6 +173,14 @@ function resolvePath(key: string): string | null {
if (code === 'DuyetNcc' || code === 'DuyetNccPhuongAn' || code === 'Contract') {
return `/system/approval-workflows-v2/${code}`
}
// [K3 S164/S165] Họ MỚI `AwV2_KhkkN1..N8` — 8 quy trình duyệt theo NHÓM của
// Kế hoạch ký kết HĐ (mỗi nhóm 1 đội hình 3 trạm PRO → CCM → CEO). Đây là "chỗ
// thứ 4" Pattern 16-bis: BE seed leaf mà thiếu nhánh này thì `resolvePath` trả
// null ⇒ MenuLeaf DROP IM LẶNG khỏi sidebar (gotcha #50).
// 🔴 CHỈ nới họ `AwV2_*` (Designer, fe-admin CÓ trang). KHÔNG nới họ `Khkk_*`
// — fe-admin cố ý KHÔNG có `WorkflowMatrixViewPage` (xem ghi chú khối `Pe_*`
// bên trên); nới ở đó = đẻ leaf chết rơi vào catch-all "chưa được build".
if (/^KhkkN[1-8]$/.test(code)) return `/system/approval-workflows-v2/${code}`
}
return null

View File

@ -4,9 +4,17 @@
// Khuôn: `components/pe/PeWorkflowPanel.tsx` — GIỮ sơ đồ Bước→Cấp ✓/●/○, banner "đến lượt bạn"
// (`blockedByV2Level` :109-114), 3 nút màu (emerald/amber/red), Dialog + ô ý kiến.
// ĐƠN-GIẢN-HOÁ CÓ CHỦ ĐÍCH (spec W3 §②-3): KHÔNG price-picker, KHÔNG ngân sách, KHÔNG file-khi-duyệt,
// KHÔNG 4 return-mode (W3 = TraLai toàn phần), KHÔNG 2 đường kết-thúc-sớm (AllowApproverFinalize /
// CeoApprovalThreshold — anh phán "PHÁ VỠ" S155; W1 đã rào ở Designer type-10). Chốt giá per-Line là
// việc BE làm ở finalize choke-point, KHÔNG phải việc của panel này.
// KHÔNG 4 return-mode (W3 = TraLai toàn phần). Chốt giá per-Line là việc BE làm ở finalize
// choke-point, KHÔNG phải việc của panel này.
//
// 🔴 [K3 S164/S165 — SUPERSEDE] Câu cũ ở đây khai "KHÔNG 2 đường kết-thúc-sớm (AllowApproverFinalize /
// CeoApprovalThreshold — anh phán «PHÁ VỠ» S155; W1 đã rào ở Designer type-10)". KHAI ĐÓ ĐÃ HẾT HIỆU LỰC:
// owner ĐẢO phán quyết S155 (@S164 — "a Chương quyết cái gì cần CEO") ⇒ BE **đã port nhánh finalize cho
// KHKK ở K3 cùng wave**, và Designer type-10 đã GỠ rào (`ApprovalWorkflowsV2Page.tsx` — hằng
// `earlyFinalizeLocked` xoá hẳn, 2 ô tick sống lại). Đừng đọc file này thành "BE không hỗ trợ".
// Panel VẪN không có control riêng cho đường kết-thúc-sớm — nhưng vì lý do KHÁC HẲN: `AllowApproverFinalize`
// là cấu-hình PER-CẤP do admin tick ở Designer, BE đọc lúc duyệt để quyết dừng hay trình tiếp CEO. Người
// duyệt chỉ bấm "Duyệt" như thường; không có gì để panel render thêm.
//
// 🔴 KHÁC PE Ở CHỖ ĐẮT NHẤT: PE nhận `approvalFlow` đã có `status` Done/Current/Pending do BE precompute.
// KHKK BE trả CÂY THÔ (`ContractSigningPlanFeatures.cs:112-117` + `:105-110`) — KHÔNG có `status`.

View File

@ -142,14 +142,23 @@ const LEVEL_ORDERS: LevelOrder[] = [1, 2, 3]
// [W1 KHKK S161] "Kế hoạch ký kết HĐ" = slot 10 của `ApprovalWorkflowApplicableType`
// (`ApprovalWorkflow.cs:66` trước đó dừng ở `TravelRequest = 9`). Đặt TÊN cho số 10
// vì nó dùng ở HAI nơi trong file: map deep-link ngay bên dưới + rào 2 ô "duyệt sớm"
// trong Designer — số trần rải 2 chỗ là kiểu sửa 1 quên 1.
// vì nó dùng ở HAI nơi trong file: map deep-link ngay bên dưới + gợi ý mã quy trình
// mặc định — số trần rải 2 chỗ là kiểu sửa 1 quên 1.
// [K3 S164 — GỠ KHOÁ] Chỗ dùng THỨ BA của hằng này (rào 2 ô "duyệt sớm" trong
// Designer) ĐÃ GỠ, xem khối ghi chú ngay dưới.
const TYPE_INT_CONTRACT_SIGNING_PLAN = 10
// Câu giải thích DÙNG CHUNG cho tooltip 2 ô bị khoá + dòng ghi chú (1 chuỗi, không
// chép 3 ln rồi lệch chữ).
const KHKK_EARLY_FINALIZE_NOTE =
'Chưa hỗ trợ cho quy trình Kế hoạch ký kết HĐ — backend chưa xử lý đường "duyệt sớm", tick vào sẽ KHÔNG có tác dụng.'
// [K3 S164 — SUPERSEDE W1 S161] 🔴 Duyệt sớm ĐÃ MỞ cho KHKK từ K3 (owner đảo phán
// quyết S155 @S164: TP.CCM quyết lên CEO hay kết thúc). Trước đó file này có hằng
// `KHKK_EARLY_FINALIZE_NOTE` + cờ `earlyFinalizeLocked` ép 2 ô "Duyệt thẳng Cấp cuối"
// và "Duyệt là KẾT THÚC" về mờ-và-false cho type 10, vì backend chưa port đường
// finalize. BE đã port nhánh finalize ở K3 CÙNG WAVE ⇒ cờ hết là "cờ chết", khoá lại
// thành khoá SAI (chặn đúng cái owner vừa yêu cầu mở). Gỡ ở CẢ HAI lớp đã rào —
// render (banner + `disabled`) và payload (`levelFlagsPayload` ép false) — vì chỉ gỡ
// 1 lớp thì màn hình và dữ liệu nói hai đằng.
// Banner giải thích đi kèm cũng XOÁ (không đổi chữ): nội dung nó chỉ để cảnh báo
// khoá, hết khoá là hết việc; còn câu supersede mang từ vựng nội bộ ("K3", "S155/
// S164") nên thuộc về comment này, không đẩy lên UI admin.
// FE typeCode → BE int (giống MenuKeys ApplicableType)
const TYPE_CODE_TO_INT: Record<string, number> = {
@ -239,12 +248,11 @@ function makeDefaultLevelEntry(order: LevelOrder, approverUserId: string): EditL
// - 2 cờ ĐỔI-ĐƯỜNG-ĐI (SkipToFinal, Finalize): PUT chỉ ECHO — gửi lệch ⇒ 409
// (ApprovalWorkflowV2AdminFeatures.cs:580-585). Vẫn PHẢI gửi đúng giá trị đang lưu.
//
// [W1 KHKK S161] `lockEarlyFinalize` = quy trình thuộc loại CHƯA hỗ trợ 2 đường
// "duyệt sớm" (hiện chỉ Kế hoạch ký kết HĐ, type 10) ⇒ ép 2 cờ đó về `false` TẠI ĐÂY,
// không ch khoá ô tick ở màn hình: khoá render mà payload vẫn bê giá trị cũ thì
// clone/dữ liệu lạ vẫn đẩy `true` lên BE. Echo của PUT vẫn khớp DB vì mọi bản type 10
// đều do chính màn này tạo ra với 2 cờ = false ⇒ không sinh 409.
function levelFlagsPayload(e: EditLevelEntry, lockEarlyFinalize: boolean) {
// [K3 S164 — GỠ KHOÁ] Tham số thứ 2 `lockEarlyFinalize` (W1 S161, ép 2 cờ "duyệt sớm"
// về `false` cho type 10) ĐÃ BỎ: mọi loại quy trình nay gửi đúng giá trị admin tick.
// Xoá hẳn tham số ch không để nó nằm chết — `noUnusedParameters` bật, và param mồ côi
// ở hàm 2 call-site là mồi cho lượt sau tưởng còn rào.
function levelFlagsPayload(e: EditLevelEntry) {
return {
allowReturnOneLevel: e.allowReturnOneLevel,
allowReturnOneStep: e.allowReturnOneStep,
@ -252,8 +260,8 @@ function levelFlagsPayload(e: EditLevelEntry, lockEarlyFinalize: boolean) {
allowReturnToDrafter: e.allowReturnToDrafter,
allowApproverEditDetails: e.allowApproverEditDetails,
allowApproverEditBudget: e.allowApproverEditBudget,
allowApproverSkipToFinal: lockEarlyFinalize ? false : e.allowApproverSkipToFinal,
allowApproverFinalize: lockEarlyFinalize ? false : e.allowApproverFinalize,
allowApproverSkipToFinal: e.allowApproverSkipToFinal,
allowApproverFinalize: e.allowApproverFinalize,
allowApproverDelete: e.allowApproverDelete,
}
}
@ -630,13 +638,10 @@ function Designer({
const isEdit = editTarget !== null
const source = editTarget ?? cloneFrom
// [W1 KHKK S161 — chặn "cờ chết"] 2 ô "duyệt sớm" (Duyệt thẳng Cấp cuối / Duyệt là
// KẾT THÚC) ĐỔI ĐƯỜNG ĐI của phiếu. Module Kế hoạch ký kết HĐ chưa port 2 đường này
// ở backend ⇒ để ô tick sống = admin tưởng đã bật mà phiếu vẫn chạy y như cũ (cờ
// chết đánh lừa người cấu hình). Khoá ở CẢ HAI lớp: render (disabled + ghi chú) và
// payload (levelFlagsPayload ép false).
const earlyFinalizeLocked = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN
// [K3 S164 — GỠ KHOÁ] `earlyFinalizeLocked` (W1 S161) ĐÃ BỎ HẲN: 2 ô "duyệt sớm"
// nay sống cho MỌI loại quy trình, kể cả Kế hoạch ký kết HĐ (type 10) — BE đã port
// đường finalize ở K3 cùng wave, và owner chốt TP.CCM là người quyết lên CEO hay
// kết thúc. Lý do đầy đủ ở khối ghi chú đầu file (gần `TYPE_INT_CONTRACT_SIGNING_PLAN`).
const initialSteps: EditStep[] = useMemo(
() => (source ? copyFromDefinition(source) : [makeEmptyStep(1)]),
[source],
@ -780,7 +785,7 @@ function Designer({
name: `Cấp ${e.order}`,
approverUserId: e.approverUserId,
// 9 cờ qua ĐÚNG 1 hàm dùng chung với POST — xem levelFlagsPayload().
...levelFlagsPayload(e, earlyFinalizeLocked),
...levelFlagsPayload(e),
})),
})),
})
@ -812,7 +817,7 @@ function Designer({
// field (KHÔNG spread `...e`) ⇒ thiếu 1 cờ thì TS vẫn build sạch, ô tick
// vẫn hiện, chỉ BE không bao giờ nhận. Từ khi có call-site thứ hai (PUT),
// 9 cờ gom vào levelFlagsPayload() dùng CHUNG — thêm cờ mới chỉ sửa 1 nơi.
...levelFlagsPayload(e, earlyFinalizeLocked),
...levelFlagsPayload(e),
})),
})),
})
@ -1278,51 +1283,32 @@ function Designer({
/>
<span>Cho phép chỉnh sửa Section ngân sách lúc đang duyệt</span>
</label>
{/* [W1 KHKK S161] Ghi chú vì sao 2 ô ngay dưới bị mờ. Chỉ hiện
cho loại quy trình chưa hỗ trợ "duyệt sớm" (type 10 — Kế hoạch
ký kết HĐ); các loại khác giao diện KHÔNG đổi 1 pixel. */}
{earlyFinalizeLocked && (
<div className="col-span-2 rounded border border-slate-200 bg-slate-50 px-1.5 py-1 text-[10.5px] leading-relaxed text-slate-500">
{KHKK_EARLY_FINALIZE_NOTE}
</div>
)}
{/* Mig 31 (S23 t1) — F2 AllowApproverSkipToFinal admin opt-in per-slot.
Approver tick = skip thẳng Cấp cuối khi đang ChoDuyet. */}
<label
className={
earlyFinalizeLocked
? 'col-span-2 flex cursor-not-allowed items-center gap-1 text-[11px] text-slate-400'
: 'col-span-2 flex items-center gap-1 text-[11px] text-slate-700'
}
title={earlyFinalizeLocked ? KHKK_EARLY_FINALIZE_NOTE : undefined}
>
Approver tick = skip thẳng Cấp cuối khi đang ChoDuyet.
[K3 S165 — gate-K3 F-2] KHKK (type-10) CHỈ mở ô "KẾT THÚC" (dưới):
service KHKK CHƯA port nhánh SkipToFinal + BE CREATE vẫn ép-false cờ
này cho type-10 ⇒ ô này DISABLE cho KHKK kẻo tick bị nuốt im lặng
(class "UI nói một đằng DB một nẻo" — đúng thứ W1 S161 cảnh báo). */}
<label className={`col-span-2 flex items-center gap-1 text-[11px] ${applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN ? 'text-slate-400' : 'text-slate-700'}`}>
<input
type="checkbox"
className="h-3 w-3"
// [W1 KHKK S161] Khoá thì hiện KHÔNG tick — khớp đúng cái
// payload gửi lên (levelFlagsPayload ép false), không để màn
// hình nói một đằng dữ liệu một nẻo.
checked={earlyFinalizeLocked ? false : entry.allowApproverSkipToFinal}
disabled={earlyFinalizeLocked}
checked={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN ? false : entry.allowApproverSkipToFinal}
disabled={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN}
title={applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN ? 'Kế hoạch ký kết chưa hỗ trợ duyệt vượt cấp — service chưa port nhánh này (K3)' : undefined}
onChange={e => updateField('allowApproverSkipToFinal', e.target.checked)}
/>
<span>Cho phép duyệt thẳng Cấp cuối khi đang duyệt</span>
</label>
{/* [Mig 58 CCM item 4 — anh Kiệt FDC] Duyệt là KẾT THÚC: NV slot
này duyệt xong → DaDuyet luôn, KHÔNG trình tiếp CEO (vd Cấp anh Chương). */}
<label
className={
earlyFinalizeLocked
? 'col-span-2 flex cursor-not-allowed items-center gap-1 text-[11px] text-slate-400'
: 'col-span-2 flex items-center gap-1 text-[11px] text-emerald-700'
}
title={earlyFinalizeLocked ? KHKK_EARLY_FINALIZE_NOTE : undefined}
>
này duyệt xong → DaDuyet luôn, KHÔNG trình tiếp CEO (vd Cấp anh Chương).
[K3 S164] Với Kế hoạch ký kết HĐ đây CHÍNH LÀ ô owner cần: tick cho
Cấp TP.CCM (anh Chương) ⇒ anh ấy quyết kết thúc hay trình tiếp CEO. */}
<label className="col-span-2 flex items-center gap-1 text-[11px] text-emerald-700">
<input
type="checkbox"
className="h-3 w-3"
checked={earlyFinalizeLocked ? false : entry.allowApproverFinalize}
disabled={earlyFinalizeLocked}
checked={entry.allowApproverFinalize}
onChange={e => updateField('allowApproverFinalize', e.target.checked)}
/>
<span>Duyệt KẾT THÚC quy trình (không trình tiếp CEO)</span>