From d27caafcf5324777858bbe49420672bf21405006 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 13 May 2026 19:08:08 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-PE:=20Chunk=20D=20=E2=80=94=20eOf?= =?UTF-8?q?fice=20Tr=E1=BA=A3=20l=E1=BA=A1i=20modes=20+=20Skip=20CEO=20+?= =?UTF-8?q?=20Approver=20edit=20Section=202=20(F1+F2+F3)=20mirror=202=20ap?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Types (fe-{admin,user}/src/types/purchaseEvaluation.ts): - ApprovalWorkflowOptions type (6 boolean Allow* flag) - WorkflowReturnMode const-object {OneLevel,OneStep,Assignee,Drafter} - PeDetailBundle +workflowOptions field (null nếu V1 legacy) PeWorkflowPanel.tsx F1 (mirror 2 app): - State returnMode + returnTargetUserId thêm vào transition mutation payload - Dialog Trả lại render radio list 1-4 mode enabled theo workflowOptions: • Trả về 1 Cấp trước (lùi pointer trong cùng Bước, peer review) • Trả về 1 Bước trước (Cấp cuối Bước trước nhận lại) • Trả về Người chỉ định (pick từ dropdown NV đã ký levelOpinions) • Trả về Người soạn thảo (default Drafter S17 fallback) - Banner amber rounded box dưới radio list mô tả hành vi mode chọn - onSuccess reset returnMode về Drafter + returnTargetUserId null PeDetailTabs.tsx F2 (mirror 2 app): - State skipToFinal + allowSkipToFinal (từ workflowOptions) - submitForApproval mutationFn accept opts.skipToFinal → POST body - Workspace action bar: thêm checkbox violet "Gửi thẳng Cấp cuối (skip trung gian)" hiển thị conditional theo allowSkipToFinal + canSubmitForApproval - Confirm dialog message dynamic: "Gửi thẳng" warning vs default tuần tự - Button label dynamic: "Lưu & Gửi thẳng CẤP CUỐI →" vs "Lưu & Gửi Duyệt →" PeDetailTabs.tsx F3 (mirror 2 app): - useAuth import + compute approverEditMode (phase=ChoDuyet + workflow.AllowApproverEditDetails + actor match currentApproval.approvers) - itemsReadOnly = readOnly && !approverEditMode → ItemsTab nhận - Banner violet "ⓘ Bạn được phép chỉnh sửa Hạng mục/NCC/Báo giá" khi approverEditMode + readOnly (Duyệt menu) — UX nhắc về quyền extended InfoTab + NccSelectorRow + BudgetFieldRow GIỮ strict isEditablePhase (KHÔNG trong F3 scope — Header section + Section 3 winner KHÔNG cho Approver edit). Verify: - npm run build × 2 app pass (fe-user 7.52s, fe-admin 499ms cached) - 0 TS6 err, warning chunk size pre-existing - BE Chunk B đã accept skipToFinal + returnMode + returnTargetUserId trong TransitionPurchaseEvaluationCommand → wire E2E complete Pending Chunk E: Docs schema-diagram §14 update + STATUS + HANDOFF + session log. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 49 +++++++- .../src/components/pe/PeWorkflowPanel.tsx | 112 ++++++++++++++++- fe-admin/src/types/purchaseEvaluation.ts | 21 ++++ fe-user/src/components/pe/PeDetailTabs.tsx | 56 ++++++++- fe-user/src/components/pe/PeWorkflowPanel.tsx | 113 +++++++++++++++++- fe-user/src/types/purchaseEvaluation.ts | 22 ++++ 6 files changed, 357 insertions(+), 16 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index bad9a6b..e0ec9c6 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -15,6 +15,7 @@ import { Select } from '@/components/ui/Select' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' +import { useAuth } from '@/contexts/AuthContext' import { PeAttachmentPurpose, PeAttachmentPurposeLabel, @@ -100,17 +101,33 @@ export function PeDetailTabs({ const canEditPhase = isEditablePhase(evaluation.phase) const opinionsReadOnly = readOnly || mode === 'workspace' + // Mig 28 (S21 t4) — F3: Approver edit Section 2 (Hạng mục + NCC + Báo giá). + const { user: currentUser } = useAuth() + const isAdmin = currentUser?.roles?.includes('Admin') ?? false + const v2Approvers = evaluation.currentApproval?.approvers ?? [] + const actorMatchesLevel = isAdmin + || (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id)) + const approverEditMode = evaluation.phase === PurchaseEvaluationPhase.ChoDuyet + && (evaluation.workflowOptions?.allowApproverEditDetails ?? false) + && actorMatchesLevel + const itemsReadOnly = readOnly && !approverEditMode + // "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition // sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing // (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace. + // Mig 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối. Workflow phải bật flag. + const [skipToFinal, setSkipToFinal] = useState(false) + const allowSkipToFinal = evaluation.workflowOptions?.allowDrafterSkipToFinal ?? false + const submitForApproval = useMutation({ - mutationFn: async () => { + mutationFn: async (opts: { skipToFinal: boolean }) => { const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai) if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt') return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, { targetPhase: next, decision: 1, comment: null, + skipToFinal: opts.skipToFinal, }) }, onSuccess: () => { @@ -192,7 +209,14 @@ export function PeDetailTabs({
- + {/* Mig 28 (S21 t4) — F3: itemsReadOnly cho phép approver edit Section 2 */} + {approverEditMode && readOnly && ( +
+ ⓘ Bạn được phép chỉnh sửa Hạng mục / NCC / Báo giá (workflow bật mode Approver edit). + Mọi thay đổi sẽ được ghi vào Lịch sử chỉnh sửa. +
+ )} +
@@ -251,18 +275,33 @@ export function PeDetailTabs({ > Lưu + {/* Mig 28 (S21 t4) — F2: Drafter skip checkbox */} + {allowSkipToFinal && canSubmitForApproval && ( + + )} diff --git a/fe-admin/src/components/pe/PeWorkflowPanel.tsx b/fe-admin/src/components/pe/PeWorkflowPanel.tsx index 09bf34e..35a73e1 100644 --- a/fe-admin/src/components/pe/PeWorkflowPanel.tsx +++ b/fe-admin/src/components/pe/PeWorkflowPanel.tsx @@ -18,6 +18,7 @@ import { PurchaseEvaluationPhase, PurchaseEvaluationPhaseColor, PurchaseEvaluationPhaseLabel, + WorkflowReturnMode, type PeDepartmentApproval, type PeDetailBundle, } from '@/types/purchaseEvaluation' @@ -33,10 +34,20 @@ export function PeWorkflowPanel({ }) { const [target, setTarget] = useState(null) const [comment, setComment] = useState('') + // Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17). + const [returnMode, setReturnMode] = useState(WorkflowReturnMode.Drafter) + const [returnTargetUserId, setReturnTargetUserId] = useState(null) const qc = useQueryClient() const { user: currentUser } = useAuth() const isAdmin = currentUser?.roles?.includes('Admin') ?? false + // Mig 28 — F1 workflow options. Null nếu V1 legacy → fallback chỉ "Trả về Drafter". + const wfOptions = evaluation.workflowOptions + // List approvers đã ký (cho mode Assignee dropdown pick) + const signedApprovers = (evaluation.levelOpinions ?? []) + .map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' })) + .filter((v, i, arr) => arr.findIndex(x => x.userId === v.userId) === i) + // Mig 24 — V2 schema chỉ cho phép approver trong CurrentApproval.approvers // duyệt cấp hiện tại. Nếu actor không khớp → disable nút "Duyệt forward" // (Trả lại / Từ chối vẫn enabled vì Service không kiểm Bước/Cấp với 2 @@ -75,10 +86,16 @@ export function PeWorkflowPanel({ && evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao) || (target === PurchaseEvaluationPhase.TraLai && evaluation.phase !== PurchaseEvaluationPhase.TraLai) + // Mig 28 (S21 t4) — F1: chỉ gửi returnMode khi target=TraLai + mode != null + const isTraLaiAction = target === PurchaseEvaluationPhase.TraLai + && evaluation.phase !== PurchaseEvaluationPhase.TraLai return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, { targetPhase: target, decision: isReject ? 2 : 1, comment: comment || null, + returnMode: isTraLaiAction ? returnMode : null, + returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee + ? returnTargetUserId : null, }) }, onSuccess: () => { @@ -88,6 +105,8 @@ export function PeWorkflowPanel({ qc.invalidateQueries({ queryKey: ['pe-dept-approvals', evaluation.id] }) setTarget(null) setComment('') + setReturnMode(WorkflowReturnMode.Drafter) + setReturnTargetUserId(null) }, onError: e => toast.error(getErrorMessage(e)), }) @@ -285,9 +304,96 @@ export function PeWorkflowPanel({ )} {isSendBack && ( -
- Phiếu sẽ về “Đang soạn thảo”. Drafter có thể sửa rồi trình lại — workflow tự jump tới phase này. -
+ <> + {/* Mig 28 (S21 t4) — F1 mode picker khi Trả lại. Show modes + enabled per workflow.options. Default Drafter (S17 fallback). */} + {(wfOptions?.allowReturnOneLevel + || wfOptions?.allowReturnOneStep + || wfOptions?.allowReturnToAssignee + || wfOptions?.allowReturnToDrafter + || !wfOptions) && ( +
+ +
+ {(wfOptions?.allowReturnOneLevel) && ( + + )} + {(wfOptions?.allowReturnOneStep) && ( + + )} + {(wfOptions?.allowReturnToAssignee) && ( + + )} + {(wfOptions?.allowReturnToDrafter !== false) && ( + + )} +
+
+ )} +
+ {returnMode === WorkflowReturnMode.Drafter + ? 'Phiếu sẽ về "Trả lại". Drafter có thể sửa rồi trình lại từ Cấp 1 Bước 1.' + : returnMode === WorkflowReturnMode.Assignee + ? 'Phiếu sẽ về Cấp/Bước của NV đã chọn (vẫn "Đã gửi duyệt"). NV nhận lại để duyệt tiếp.' + : 'Phiếu sẽ lùi pointer (vẫn "Đã gửi duyệt"). NV trước nhận lại để duyệt tiếp.'} +
+ )}