[CLAUDE] Contract: F-1 o-tich opt-in level-finalize man duyet KHKK (DTO expose co + UI x2 app, khuon PE-live S97)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m21s

Gate-K3 F-1 MAJOR: BE ApplyLevelFinalize ?? true ma 0 UI gui false => tram CEO
chi di duoc bang admin bo co Designer. Va: expose AllowApproverFinalize vao
ContractSigningPlanWorkflowLevelDto (1 site :640) + panel KHKK x2 app suy
actingLevel mirror ResolveActingLevel (own-truoc, Admin fallback First) + o-tich
OPT-IN khuon PE-live S97 (G-1 reviewer: default false — fail-mode an toan len CEO;
ratify treo so CHO-ANH) + body 3-field chi khi approve.

Reviewer PASS-WITH-FLAGS 7 (sub-reviewer-f1-checkbox.md): G-1 flip ap, G-5 bo
"(CEO)" ap, G-6 tach commit ap (K4a rieng), G-2/G-3/G-4 no nhip-K4, G-7 vao
commit K4a. Suite 614/0 + filter KHKK 34/34 + npm x2 + SHA-pair b37cd292.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-01 09:05:51 +07:00
parent 2cc22fde0f
commit 7a903cf108
5 changed files with 153 additions and 14 deletions

View File

@ -12,9 +12,12 @@
// 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.
// 🔴 [F-1 S166 — SUPERSEDE tiếp] Câu cũ "panel không có control riêng cho đường kết-thúc-sớm" HẾT hiệu lực:
// gate-K3 chấm nó là F-1 MAJOR (BE `?? true` mà 0 UI gửi false ⇒ trạm CEO chỉ đi được bằng admin bỏ cờ
// Designer). Nay panel CÓ ô-tích "Cấp này KẾT THÚC" theo khuôn PE-LIVE S97 = OPT-IN (G-1 reviewer:
// PE `useState(false)` :61 — owner đã ĐẢO opt-out S96 → opt-in S97; nhãn cũ "khuôn S96" trích trúng
// chú-thích chết). Default KHÔNG tick ⇒ gửi false ⇒ trình tiếp; TÍCH ⇒ true ⇒ BE kết thúc tại cấp.
// Ratify default treo 1 câu ở sổ CHỜ-ANH (lật = 1 edit); hành vi mặc định trạm CCM ĐỔI so trước-F-1.
//
// 🔴 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`.
@ -79,6 +82,13 @@ export function KhkkWorkflowPanel({
const { user } = useAuth()
const [action, setAction] = useState<KhkkTransitionActionValue | null>(null)
const [comment, setComment] = useState('')
// [F-1 S166 · G-1 reviewer] Ô-tích level-finalize theo khuôn PE-LIVE S97 = OPT-IN
// (`PeWorkflowPanel.tsx:61` useState(false) — owner đảo opt-out S96 → opt-in S97 "cái này option
// thôi"; đừng trích comment S96 chết ở `:769-771` file đó). Default UNCHECKED = duyệt xong TRÌNH
// TIẾP cấp sau (fail-mode an toàn: không đọc thì phiếu vẫn lên CEO); TÍCH mới KẾT THÚC tại cấp.
// ⚠️ Đổi hành vi mặc định trạm CCM so trước-F-1 (BE `?? true` auto-finalize) — treo 1 câu ratify
// owner ở sổ CHỜ-ANH; lật lại = đổi 1 chữ false→true + 2 nhãn dưới.
const [applyLevelFinalize, setApplyLevelFinalize] = useState(false)
const phase = plan.phase as KhkkPhaseValue
const steps = plan.workflowSteps ?? []
@ -125,6 +135,13 @@ export function KhkkWorkflowPanel({
const actorInLevel = isAdmin || actorIsCurrentApprover
const blockedByLevel = isWaiting && !actorInLevel
// [F-1 S166] Ô-tích opt-out hiện theo cờ của ĐÚNG level BE sẽ chấm — mirror `ResolveActingLevel`
// (`ContractSigningPlanWorkflowService.cs:455-469`): own-level TRƯỚC (kể cả Admin có slot riêng),
// Admin không có own ⇒ level ĐẦU nhóm cùng-order. Soi cờ của level KHÁC là hiện ô-tích nói dối.
const actingLevel = currentLevels.find(l => l.approverUserId === user?.id)
?? (isAdmin ? currentLevels[0] : undefined)
const approverFinalizeEligible = actingLevel?.allowApproverFinalize === true
// Nút "Gửi duyệt": guard BE là `CreatedBy == actor DeptManager(cùng phòng) Admin`
// (spec W3 §②-5). FE KHÔNG suy được vế DeptManager — `UserInfo` chỉ có {id,email,fullName,roles}
// (`types/auth.ts:1-6`), không có phòng ban. CỐ Ý **không ẩn** nút theo vai: ẩn sẽ giấu nút với
@ -134,8 +151,16 @@ export function KhkkWorkflowPanel({
const transition = useMutation({
mutationFn: async (a: KhkkTransitionActionValue) => {
// 🔴 BODY LITERAL theo hợp-đồng LEAD chốt: đúng 2 field `action` + `comment`.
const body: KhkkTransitionInput = { action: a, comment: comment.trim() || null }
// 🔴 BODY theo hợp-đồng LEAD chốt W3 (`action` + `comment`) + [F-1 S166] field thứ 3
// `applyLevelFinalize` CHỈ đính kèm khi approve: cấp có cờ gửi giá trị ô-tích, cấp thường
// gửi true = no-op (mirror PE `PeWorkflowPanel.tsx:259`; BE `bool? = null` ⇒ absent = true).
const body: KhkkTransitionInput = {
action: a,
comment: comment.trim() || null,
...(a === KhkkTransitionAction.Approve
? { applyLevelFinalize: approverFinalizeEligible ? applyLevelFinalize : true }
: {}),
}
const res = await api.post<KhkkTransitionResult>(
`/contract-signing-plans/${plan.id}/transitions`,
body,
@ -295,7 +320,15 @@ export function KhkkWorkflowPanel({
<button
type="button"
disabled={blockedByLevel}
onClick={() => { if (!blockedByLevel) { setComment(''); setAction(KhkkTransitionAction.Approve) } }}
onClick={() => {
if (!blockedByLevel) {
setComment('')
// [F-1 S166 · G-1] reset về default KHÔNG-tick (opt-IN, khuôn PE S97) mỗi
// lần mở dialog — lựa chọn tích là per-lượt-duyệt, không dính lần trước.
setApplyLevelFinalize(false)
setAction(KhkkTransitionAction.Approve)
}
}}
title={blockedByLevel ? 'Cấp hiện tại chỉ người được phân công mới thao tác được.' : 'Duyệt cấp hiện tại'}
className={cn(
'inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] font-bold transition',
@ -408,6 +441,33 @@ export function KhkkWorkflowPanel({
Kế hoạch bị khoá hoàn toàn (không sửa / không duyệt tiếp). Người soạn phải lập kế hoạch mới.
</div>
)}
{/* [F-1 S166 · G-1/G-5 reviewer] Cấp cấu hình KẾT THÚC (AllowApproverFinalize) = ô-tích
OPT-IN, khuôn PE-LIVE S97 (`PeWorkflowPanel.tsx:61` useState(false) + JSX `:772-791`;
comment "opt-out S96" ngay trên khối JSX đó là chú-thích CHẾT — G-1 bắt đúng bẫy này):
default KHÔNG tick = duyệt xong TRÌNH TIẾP cấp sau; TÍCH = gửi applyLevelFinalize=true
⇒ BE kết thúc tại cấp. Không viết "(CEO)" — cấp-sau là gì tuỳ cấu hình quy trình (G-5).
KHÁC PE: không nhắc "chọn giá chốt" — KHKK chốt giá per-dòng do BE tự làm ở finalize
choke-point (`ApplyApprovedValuesOnFinalize`), panel không có price-picker. */}
{action === KhkkTransitionAction.Approve && approverFinalizeEligible && (
<div className="mb-3 rounded border border-emerald-300 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-800">
<label className="flex cursor-pointer items-start gap-2">
<input
type="checkbox"
className="mt-0.5"
checked={applyLevelFinalize}
onChange={e => setApplyLevelFinalize(e.target.checked)}
/>
<span>
<span className="font-medium">Cấp này KẾT THÚC quy trình</span>
<span className="mt-0.5 block text-[11px] text-emerald-700/80">
{applyLevelFinalize
? 'Đã tích — duyệt xong là kế hoạch "Đã duyệt", KẾT THÚC tại cấp này, KHÔNG trình tiếp cấp sau.'
: 'Mặc định: duyệt xong kế hoạch TRÌNH TIẾP lên cấp sau của quy trình. TÍCH vào nếu muốn KẾT THÚC ngay tại cấp này.'}
</span>
</span>
</label>
</div>
)}
<Label>
Ý kiến{commentRequired ? <span className="text-red-500"> *</span> : ' (tuỳ chọn)'}
</Label>

View File

@ -231,6 +231,9 @@ export interface KhkkWorkflowLevelDto {
name: string | null
approverUserId: string
approverFullName: string | null
// [F-1 S166] Cờ per-Cấp "được KẾT THÚC quy trình tại cấp này" (admin tick ở Designer).
// Panel dùng để hiện ô-tích opt-out khi duyệt — mirror BE `ResolveActingLevel`.
allowApproverFinalize: boolean
}
// `ContractSigningPlanWorkflowStepDto:112-117` — chỉ có `departmentId`, KHÔNG có tên phòng.
@ -331,6 +334,10 @@ export type KhkkTransitionActionValue =
export interface KhkkTransitionInput {
action: KhkkTransitionActionValue
comment: string | null
// [F-1 S166] Opt-out level-finalize (khuôn PE S96): chỉ gửi khi action=approve.
// BE `ContractSigningPlanTransitionBody.ApplyLevelFinalize` là `bool? = null` — absent/true
// = auto kết-thúc-tại-cấp khi Cấp có cờ; false = người duyệt BỎ tick ⇒ trình tiếp CEO.
applyLevelFinalize?: boolean
}
// Response 200 — 3 field, ĐÚNG tên hợp-đồng. `phase` là INT (mirror `KhkkPhase`).