From 62b50d112b014d04db01288fc59e7df7ad0e141a Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 22 May 2026 12:34:00 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin+FE-User:=20Plan=20B=20Chunk?= =?UTF-8?q?=20D=20=E2=80=94=20ContractCreatePage=20Workspace=20V2=20Select?= =?UTF-8?q?=20dropdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror PE PeWorkspaceCreateView Workspace pattern. Drafter pick V2 workflow IsUserSelectable=true filter ApplicableType=Contract(3). Changes × 2 app: - Add useQuery fetch /api/approval-workflows-v2?applicableType=3 + filter client-side isUserSelectable=true (mirror PE Mig 25 pattern) - Add Select dropdown "Quy trình duyệt V2 (tùy chọn)" trong ContractHeaderForm (create mode panel 2) - Wire approvalWorkflowId vào CreateContractCommand POST body - Conditional UI: blank = V1 fallback auto pick (7 prod contract behavior giữ nguyên); user pick V2 → pin ApprovalWorkflowId Mig 32 schema - Hint khi 0 workflows V2 admin ghim → message rõ V1 fallback Verify: - npm run build × 2 app PASS 0 TS err (1.32MB fe-user, 1.40MB fe-admin) - Mirror 2 app §3.9: +44 LOC mỗi file = +88 LOC total byte-similar - API endpoint /api/approval-workflows-v2 existing (Mig 25 Plan AA S24) - BE CreateContractCommand.ApprovalWorkflowId field đã add Chunk E1 (em main commit prior) — FE wire safe - Backward compat: V1 contract path unchanged khi user bỏ trống dropdown Plan B chain (6 chunks): - A1 58898e8 ✅ Entity ApprovalWorkflowId + CurrentApprovalLevelOrder - A2 a85e437 ✅ Mig 32 + Seed sample V2 Contract workflow - B 138469d ✅ Service ApproveV2 branch (PE pattern mirror) - C 26c98d3 ✅ Mig 33 ContractLevelOpinions - B2 1f199b0 ✅ UPSERT LevelOpinion block (PE Mig 26 mirror) - D (this) ✅ FE Workspace V2 dropdown - E FE Section 5 V2 (em main + Implementer split E1+E2 sau) Pattern 16-bis 4-place mirror check: - Page file × 2 app: edited (insertion mirror byte-similar) - App.tsx Routes: N/A (enhance existing /contracts/new route) - menuKeys.ts: N/A (không thêm menu key mới) - Layout staticMap: N/A (route unchanged) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/contracts/ContractCreatePage.tsx | 44 +++++++++++++++++++ .../pages/contracts/ContractCreatePage.tsx | 44 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/fe-admin/src/pages/contracts/ContractCreatePage.tsx b/fe-admin/src/pages/contracts/ContractCreatePage.tsx index a1d4a3e..5ba13d5 100644 --- a/fe-admin/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-admin/src/pages/contracts/ContractCreatePage.tsx @@ -309,6 +309,11 @@ function ContractHeaderForm({ const [budgetManual, setBudgetManual] = useState(false) const [budgetManualName, setBudgetManualName] = useState('') const [budgetManualAmount, setBudgetManualAmount] = useState(0) + // [Plan B S29 Chunk D 2026-05-22 Mig 32] V2 workflow pin lúc create — mirror + // PE PeWorkspaceCreateView pattern. Drafter pick V2 workflow IsUserSelectable + // filter ApplicableType=Contract(3). Nếu blank → BE fallback V1 auto pick + // (7 prod contract giữ behavior cũ). + const [approvalWorkflowId, setApprovalWorkflowId] = useState('') // Reset type về default khi typeFilter (parent prop) thay đổi useEffect(() => { setType(defaultType) }, [defaultType]) @@ -327,6 +332,19 @@ function ContractHeaderForm({ queryKey: ['templates-by-type', type], queryFn: async () => (await api.get('/forms/templates', { params: { type } })).data, }) + // [Plan B S29 Chunk D Mig 32] V2 workflows ApplicableType=Contract(3) filter + // IsUserSelectable=true (admin ghim cho user pick). Mirror PE Mig 25 pattern. + const approvalWorkflows = useQuery({ + queryKey: ['approval-workflows-v2-contract'], + queryFn: async () => { + const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean; isUserSelectable: boolean }[] }[] }>( + '/approval-workflows-v2', + { params: { applicableType: 3 } }, + ) + const typeBucket = res.data.types.find(t => t.applicableType === 3) + return (typeBucket?.history ?? []).filter(w => w.isUserSelectable) + }, + }) // Eligible Budgets: cùng Project + Phase=DaDuyet (BE-side filter). const eligibleBudgets = useQuery({ queryKey: ['eligible-budgets', projectId], @@ -357,6 +375,9 @@ function ContractHeaderForm({ bypassProcurementAndCCM: bypass, draftData: null, ...budgetPayload, + // [Plan B S29 Chunk D Mig 32] Pin V2 workflow nếu user chọn; null → + // BE fallback V1 auto pick. + approvalWorkflowId: approvalWorkflowId || null, }) return res.data.id }, @@ -395,6 +416,29 @@ function ContractHeaderForm({ templates={templates.data ?? []} typeReadonly={false} /> + {/* [Plan B S29 Chunk D Mig 32] V2 workflow picker — mutually exclusive + với V1. Blank → BE fallback V1 auto pick (7 prod contract behavior). + Mirror PE PeWorkspaceCreateView pattern. */} +
+ + + {approvalWorkflows.data && approvalWorkflows.data.length === 0 && ( +

+ Chưa có quy trình duyệt V2 nào được admin ghim cho HĐ — HĐ sẽ chạy theo V1 mặc định. +

+ )} +
diff --git a/fe-user/src/pages/contracts/ContractCreatePage.tsx b/fe-user/src/pages/contracts/ContractCreatePage.tsx index a1d4a3e..5ba13d5 100644 --- a/fe-user/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-user/src/pages/contracts/ContractCreatePage.tsx @@ -309,6 +309,11 @@ function ContractHeaderForm({ const [budgetManual, setBudgetManual] = useState(false) const [budgetManualName, setBudgetManualName] = useState('') const [budgetManualAmount, setBudgetManualAmount] = useState(0) + // [Plan B S29 Chunk D 2026-05-22 Mig 32] V2 workflow pin lúc create — mirror + // PE PeWorkspaceCreateView pattern. Drafter pick V2 workflow IsUserSelectable + // filter ApplicableType=Contract(3). Nếu blank → BE fallback V1 auto pick + // (7 prod contract giữ behavior cũ). + const [approvalWorkflowId, setApprovalWorkflowId] = useState('') // Reset type về default khi typeFilter (parent prop) thay đổi useEffect(() => { setType(defaultType) }, [defaultType]) @@ -327,6 +332,19 @@ function ContractHeaderForm({ queryKey: ['templates-by-type', type], queryFn: async () => (await api.get('/forms/templates', { params: { type } })).data, }) + // [Plan B S29 Chunk D Mig 32] V2 workflows ApplicableType=Contract(3) filter + // IsUserSelectable=true (admin ghim cho user pick). Mirror PE Mig 25 pattern. + const approvalWorkflows = useQuery({ + queryKey: ['approval-workflows-v2-contract'], + queryFn: async () => { + const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean; isUserSelectable: boolean }[] }[] }>( + '/approval-workflows-v2', + { params: { applicableType: 3 } }, + ) + const typeBucket = res.data.types.find(t => t.applicableType === 3) + return (typeBucket?.history ?? []).filter(w => w.isUserSelectable) + }, + }) // Eligible Budgets: cùng Project + Phase=DaDuyet (BE-side filter). const eligibleBudgets = useQuery({ queryKey: ['eligible-budgets', projectId], @@ -357,6 +375,9 @@ function ContractHeaderForm({ bypassProcurementAndCCM: bypass, draftData: null, ...budgetPayload, + // [Plan B S29 Chunk D Mig 32] Pin V2 workflow nếu user chọn; null → + // BE fallback V1 auto pick. + approvalWorkflowId: approvalWorkflowId || null, }) return res.data.id }, @@ -395,6 +416,29 @@ function ContractHeaderForm({ templates={templates.data ?? []} typeReadonly={false} /> + {/* [Plan B S29 Chunk D Mig 32] V2 workflow picker — mutually exclusive + với V1. Blank → BE fallback V1 auto pick (7 prod contract behavior). + Mirror PE PeWorkspaceCreateView pattern. */} +
+ + + {approvalWorkflows.data && approvalWorkflows.data.length === 0 && ( +

+ Chưa có quy trình duyệt V2 nào được admin ghim cho HĐ — HĐ sẽ chạy theo V1 mặc định. +

+ )} +