[CLAUDE] Domain+App+Infra+Admin: W1 KHKK — Mig 69 ContractSigningPlans 7 bang + policy KeHoachKyKet + seeder UPGRADE-if-exists + CodeGen KHKK + Designer type-10
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m40s

- Domain/ContractSigningPlans: 7 entity + 3 enum (Phase 1/2/3/98/99; DossierItem Kind/Status start=1); ApplicableType += ContractSigningPlan=10 (slot 10, append-only)
- Mig 69 AddContractSigningPlans: 7 CreateTable / 20 CreateIndex / 0 ALTER; UNIQUE filtered (PlanId,SupplierId) WHERE IsDeleted=0 (#57); FK 6 con Cascade 1-hop + 2 Restrict, 0 multiple-cascade-path; database-agent review PASS 8/8 + DB11
- MenuKeys.All += KeHoachKyKet (policy KeHoachKyKet.* x4 sinh runtime; menu-row prod KHONG doi — 142 giu); STATUS canonical 54->55 / 216->220
- DbInitializer.SeedKeHoachKyKetAccessAsync: UPGRADE-if-exists (F-S1 — khong insert-NO-OP) + vế CanDelete (Drafter/Admin), 13/13 role CanCreate+CanUpdate, chay SAU revoke; revoker khong phu Khkk_* (reviewer Truc-1 PASS)
- ContractSigningPlanCodeGenerator: KHKK/{YYYY}/{Seq:D3} (owner OK @S161) qua WorkflowAppCodeSequences dung chung, SERIALIZABLE
- AWV2AdminFeatures: 3 site count/retained/orphan + nhanh ContractSigningPlanLevelOpinions (chong no FK 500 khi xoa Level); +label VN type-10 (F-7); +ep false 2 co early-finalize server-side cho type-10 (F-8 — containment tang BE, FE da ep o levelFlagsPayload)
- fe-admin Designer: deep-link type-10 + an 2 checkbox early-finalize (anh phan "PHA VO" S155) — containment 2 lop render+payload
- tests: +4 (566 total, 0 fail) — schema-invariant 7 bang + codegen format (fault-inject 2 chieu chung co rang)

Reviewer: PASS_WITH_FLAGS 10 issue -> 8 fixed cung commit, 2 giu trang thai (F-1 acceptance đo-CO cho prod; F-4 ke thua y PE prod)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-29 20:04:39 +07:00
parent 909134c922
commit a8027c04fe
30 changed files with 8987 additions and 22 deletions

View File

@ -140,16 +140,34 @@ type ApproverUser = { id: string; fullName: string; email: string; departmentId:
const MAX_LEVELS_PER_STEP = 3
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.
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 lần 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.'
// FE typeCode → BE int (giống MenuKeys ApplicableType)
const TYPE_CODE_TO_INT: Record<string, number> = {
DuyetNcc: 1,
DuyetNccPhuongAn: 2,
Contract: 3,
// [W1 KHKK S161] Mở deep-link `/system/approval-workflows-v2/ContractSigningPlan`.
// KHÔNG chép nhãn tiếng Việt vào FE: nhãn hiển thị lấy từ BE qua
// `applicableTypeLabel` (AwAdminOverviewDto) — file này không có bảng nhãn riêng.
ContractSigningPlan: TYPE_INT_CONTRACT_SIGNING_PLAN,
}
const DEFAULT_CODE_BY_TYPE: Record<number, string> = {
1: 'QT-DN-V2-001',
2: 'QT-DN-PA-V2-001',
3: 'QT-HD-V2-001',
// Chỉ là giá trị GỢI Ý điền sẵn ô "Mã quy trình" — admin sửa được trước khi lưu.
// Không khai thì rơi về fallback chung 'QT-V2-001' (hằng fallback trong TypePanel — trỏ TÊN, đừng neo số dòng: reviewer F-9).
[TYPE_INT_CONTRACT_SIGNING_PLAN]: 'QT-KHKK-V2-001',
}
function makeEmptyStep(stepNo: number, deptId: string | null = null): EditStep {
@ -220,7 +238,13 @@ function makeDefaultLevelEntry(order: LevelOrder, approverUserId: string): EditL
// - 7 cờ QUYỀN-TẠI-CHỖ (4×Return + EditDetails + EditBudget + Delete): PUT ghi được.
// - 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.
function levelFlagsPayload(e: EditLevelEntry) {
//
// [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) {
return {
allowReturnOneLevel: e.allowReturnOneLevel,
allowReturnOneStep: e.allowReturnOneStep,
@ -228,8 +252,8 @@ function levelFlagsPayload(e: EditLevelEntry) {
allowReturnToDrafter: e.allowReturnToDrafter,
allowApproverEditDetails: e.allowApproverEditDetails,
allowApproverEditBudget: e.allowApproverEditBudget,
allowApproverSkipToFinal: e.allowApproverSkipToFinal,
allowApproverFinalize: e.allowApproverFinalize,
allowApproverSkipToFinal: lockEarlyFinalize ? false : e.allowApproverSkipToFinal,
allowApproverFinalize: lockEarlyFinalize ? false : e.allowApproverFinalize,
allowApproverDelete: e.allowApproverDelete,
}
}
@ -606,6 +630,13 @@ 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
const initialSteps: EditStep[] = useMemo(
() => (source ? copyFromDefinition(source) : [makeEmptyStep(1)]),
[source],
@ -749,7 +780,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),
...levelFlagsPayload(e, earlyFinalizeLocked),
})),
})),
})
@ -781,7 +812,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),
...levelFlagsPayload(e, earlyFinalizeLocked),
})),
})),
})
@ -1247,24 +1278,51 @@ 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="col-span-2 flex items-center gap-1 text-[11px] text-slate-700">
<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}
>
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowApproverSkipToFinal}
// [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}
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="col-span-2 flex items-center gap-1 text-[11px] text-emerald-700">
<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}
>
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowApproverFinalize}
checked={earlyFinalizeLocked ? false : entry.allowApproverFinalize}
disabled={earlyFinalizeLocked}
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>