[CLAUDE] FE-Admin FE-User: Chunk E — K6 Workspace DROP Drafter checkbox + ADD Approver toggle (Mig 31 F2 refactor)
Plan K Chunk E mirror 2 app rule §3.9. Refactor F2 UX flow:
DROP fe-admin + fe-user Workspace Drafter checkbox:
- PeDetailTabs.tsx Workspace action bar: REMOVE "Gửi thẳng Cấp cuối (skip trung gian)"
violet label + state skipToFinal + allowSkipToFinal lookup + skipToFinal payload
- submitForApproval mutation signature simplify: opts: { skipToFinal: boolean } → void
- Confirm dialog text + button label drop skipToFinal conditional
ADD fe-admin + fe-user Approver toggle trong PeWorkflowPanel dialog:
- State skipToFinalApprover default false
- Visible khi Approve forward (NOT Cancel + NOT SendBack) + currentLevelOptions?.allowApproverSkipToFinal
- Checkbox violet panel với description "Phiếu sẽ tiến thẳng tới Đã duyệt (terminal)"
- Amber warning khi checked: "Hành động KHÔNG quay lại được"
- Mutation payload +skipToFinal: !isReject && skipToFinalApprover
- onSuccess reset state
Type ApprovalWorkflowOptions × 2 app: +allowApproverSkipToFinal: boolean (7th)
Type PeDetailBundle × 2 app: REMOVE drafterAllowSkipToFinal field + comment Mig 29+30+31
UX design Dialog approach (consistent với Trả lại Mode picker pattern):
- Skip thẳng Cấp cuối = destructive action → confirm dialog amber warning
- Mirror Mig 28 Trả lại 4 mode picker UX consistency
- Em main solo K6 per UX flow decision criteria
Per bro decision Plan K S23 t1: "Chỗ cấu hình cho phép skip → duyệt thẳng cho phép
trong trạng thái đang duyệt" + "Tất cả đều cấu hình ngay trong chỗ setup quy trình duyệt".
Verify:
- npm run build × 2 app pass clean (0 TS err)
- Pre-existing warnings unchanged (chunk size + INEFFECTIVE_DYNAMIC_IMPORT)
- Bundle hash rotated × 2 app
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -117,19 +117,17 @@ export function PeDetailTabs({
|
|||||||
// "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
|
// "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
|
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
|
||||||
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
||||||
// Mig 29 (S21 t5) — F2: per-Drafter user flag (User Management page).
|
// Mig 31 (S23 t1) — F2 Drafter-from-Nháp semantic deprecated. skipToFinal moved
|
||||||
const [skipToFinal, setSkipToFinal] = useState(false)
|
// sang Approver scope ChoDuyet (per-Level slot — xem PeWorkflowPanel).
|
||||||
const allowSkipToFinal = evaluation.drafterAllowSkipToFinal ?? false
|
|
||||||
|
|
||||||
const submitForApproval = useMutation({
|
const submitForApproval = useMutation({
|
||||||
mutationFn: async (opts: { skipToFinal: boolean }) => {
|
mutationFn: async () => {
|
||||||
const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
|
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')
|
if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
|
||||||
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
||||||
targetPhase: next,
|
targetPhase: next,
|
||||||
decision: 1,
|
decision: 1,
|
||||||
comment: null,
|
comment: null,
|
||||||
skipToFinal: opts.skipToFinal,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -283,33 +281,19 @@ export function PeDetailTabs({
|
|||||||
>
|
>
|
||||||
Lưu
|
Lưu
|
||||||
</Button>
|
</Button>
|
||||||
{/* Mig 28 (S21 t4) — F2: Drafter skip checkbox */}
|
|
||||||
{allowSkipToFinal && canSubmitForApproval && (
|
|
||||||
<label className="flex cursor-pointer items-center gap-1.5 rounded border border-violet-200 bg-violet-50 px-2 py-1 text-[11px] text-violet-800">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="h-3 w-3"
|
|
||||||
checked={skipToFinal}
|
|
||||||
onChange={e => setSkipToFinal(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Gửi thẳng Cấp cuối (skip trung gian)</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!forwardPhase) return
|
if (!forwardPhase) return
|
||||||
const confirmMsg = skipToFinal
|
const confirmMsg = `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
||||||
? `Gửi THẲNG CẤP CUỐI bỏ qua các Cấp trung gian? Hệ thống sẽ ghi audit "Drafter skip" — không quay lại được trừ khi approver Trả lại.`
|
|
||||||
: `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
submitForApproval.mutate({ skipToFinal })
|
submitForApproval.mutate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
||||||
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{submitForApproval.isPending ? 'Đang gửi…' : skipToFinal ? 'Lưu & Gửi thẳng CẤP CUỐI →' : 'Lưu & Gửi Duyệt →'}
|
{submitForApproval.isPending ? 'Đang gửi…' : 'Lưu & Gửi Duyệt →'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,6 +37,9 @@ export function PeWorkflowPanel({
|
|||||||
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
||||||
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
||||||
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối. Default false (admin opt-in
|
||||||
|
// per slot tick → checkbox visible trong dialog Approve, default unchecked).
|
||||||
|
const [skipToFinalApprover, setSkipToFinalApprover] = useState(false)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
@ -97,6 +100,9 @@ export function PeWorkflowPanel({
|
|||||||
returnMode: isTraLaiAction ? returnMode : null,
|
returnMode: isTraLaiAction ? returnMode : null,
|
||||||
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
||||||
? returnTargetUserId : null,
|
? returnTargetUserId : null,
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver scope ChoDuyet duyệt thẳng Cấp cuối.
|
||||||
|
// BE check matchingLevel.AllowApproverSkipToFinal (admin opt-in per slot).
|
||||||
|
skipToFinal: !isReject && skipToFinalApprover,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -108,6 +114,7 @@ export function PeWorkflowPanel({
|
|||||||
setComment('')
|
setComment('')
|
||||||
setReturnMode(WorkflowReturnMode.Drafter)
|
setReturnMode(WorkflowReturnMode.Drafter)
|
||||||
setReturnTargetUserId(null)
|
setReturnTargetUserId(null)
|
||||||
|
setSkipToFinalApprover(false)
|
||||||
},
|
},
|
||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
@ -397,6 +404,32 @@ export function PeWorkflowPanel({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* Mig 31 (S23 t1) — F2 Approver toggle: chỉ visible khi Approve forward
|
||||||
|
+ admin tick AllowApproverSkipToFinal cho slot Cấp hiện tại. */}
|
||||||
|
{!isCancel && !isSendBack && levelOptions?.allowApproverSkipToFinal && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="flex cursor-pointer items-start gap-2 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[12px] text-violet-800 hover:bg-violet-100/60">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-0.5"
|
||||||
|
checked={skipToFinalApprover}
|
||||||
|
onChange={e => setSkipToFinalApprover(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
|
||||||
|
<span className="mt-0.5 block text-[11px] text-violet-700/80">
|
||||||
|
Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) — bỏ qua mọi Cấp/Bước còn lại.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCancel && !isSendBack && skipToFinalApprover && (
|
||||||
|
<div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
|
||||||
|
⚠ Hành động KHÔNG quay lại được (trừ khi Drafter reset toàn bộ). Phiếu sẽ
|
||||||
|
skip qua tất cả Cấp/Bước còn lại và chuyển thẳng "Đã duyệt".
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Label>Ghi chú (tùy chọn)</Label>
|
<Label>Ghi chú (tùy chọn)</Label>
|
||||||
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -347,8 +347,9 @@ export type PeDepartmentApproval = {
|
|||||||
isBypassed: boolean
|
isBypassed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp Approver hiện tại (per-NV slot).
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options của
|
||||||
// F2 (Drafter skip) MOVED sang per-User (xem PeDetailBundle.drafterAllowSkipToFinal).
|
// Cấp Approver hiện tại (per-NV slot). F2 refactor sang Approver scope ChoDuyet
|
||||||
|
// (storage cũ Users.AllowDrafterSkipToFinal đã drop).
|
||||||
export type ApprovalWorkflowOptions = {
|
export type ApprovalWorkflowOptions = {
|
||||||
allowReturnOneLevel: boolean
|
allowReturnOneLevel: boolean
|
||||||
allowReturnOneStep: boolean
|
allowReturnOneStep: boolean
|
||||||
@ -356,6 +357,7 @@ export type ApprovalWorkflowOptions = {
|
|||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
||||||
|
allowApproverSkipToFinal: boolean // Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
||||||
@ -398,10 +400,9 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot).
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options của
|
||||||
|
// Cấp hiện tại (per-NV slot). Null nếu V1 legacy hoặc pointer chưa init.
|
||||||
currentLevelOptions: ApprovalWorkflowOptions | null
|
currentLevelOptions: ApprovalWorkflowOptions | null
|
||||||
// Mig 29 — F2 per-Drafter flag
|
|
||||||
drafterAllowSkipToFinal: boolean
|
|
||||||
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
currentApproval: PeCurrentApproval | null
|
currentApproval: PeCurrentApproval | null
|
||||||
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
||||||
|
|||||||
@ -117,24 +117,21 @@ export function PeDetailTabs({
|
|||||||
// itemsReadOnly = readOnly trừ khi approver mode F3 mở
|
// itemsReadOnly = readOnly trừ khi approver mode F3 mở
|
||||||
const itemsReadOnly = readOnly && !approverEditMode
|
const itemsReadOnly = readOnly && !approverEditMode
|
||||||
|
|
||||||
// Mig 29 (S21 t5) — F2: per-Drafter user flag (KHÔNG còn workflow-level).
|
// Mig 31 (S23 t1) — F2 Drafter-from-Nháp semantic deprecated. skipToFinal
|
||||||
// Admin cấu hình ở User Management page → BE resolve qua DrafterUserId.
|
// moved sang Approver scope ChoDuyet (per-Level slot — xem PeWorkflowPanel).
|
||||||
const [skipToFinal, setSkipToFinal] = useState(false)
|
// Drafter SUBMIT chạy normal init pointer Step 0 Cấp 1.
|
||||||
const allowSkipToFinal = evaluation.drafterAllowSkipToFinal ?? false
|
|
||||||
|
|
||||||
// "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
|
// "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
|
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
|
||||||
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
||||||
const submitForApproval = useMutation({
|
const submitForApproval = useMutation({
|
||||||
mutationFn: async (opts: { skipToFinal: boolean }) => {
|
mutationFn: async () => {
|
||||||
const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
|
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')
|
if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
|
||||||
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
||||||
targetPhase: next,
|
targetPhase: next,
|
||||||
decision: 1,
|
decision: 1,
|
||||||
comment: null,
|
comment: null,
|
||||||
// Mig 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối
|
|
||||||
skipToFinal: opts.skipToFinal,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -289,34 +286,19 @@ export function PeDetailTabs({
|
|||||||
>
|
>
|
||||||
Lưu
|
Lưu
|
||||||
</Button>
|
</Button>
|
||||||
{/* Mig 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối checkbox.
|
|
||||||
Chỉ hiện khi workflow.AllowDrafterSkipToFinal=true. */}
|
|
||||||
{allowSkipToFinal && canSubmitForApproval && (
|
|
||||||
<label className="flex cursor-pointer items-center gap-1.5 rounded border border-violet-200 bg-violet-50 px-2 py-1 text-[11px] text-violet-800">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="h-3 w-3"
|
|
||||||
checked={skipToFinal}
|
|
||||||
onChange={e => setSkipToFinal(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Gửi thẳng Cấp cuối (skip trung gian)</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!forwardPhase) return
|
if (!forwardPhase) return
|
||||||
const confirmMsg = skipToFinal
|
const confirmMsg = `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
||||||
? `Gửi THẲNG CẤP CUỐI bỏ qua các Cấp trung gian? Hệ thống sẽ ghi audit "Drafter skip" — không quay lại được trừ khi approver Trả lại.`
|
|
||||||
: `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
submitForApproval.mutate({ skipToFinal })
|
submitForApproval.mutate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
||||||
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{submitForApproval.isPending ? 'Đang gửi…' : skipToFinal ? 'Lưu & Gửi thẳng CẤP CUỐI →' : 'Lưu & Gửi Duyệt →'}
|
{submitForApproval.isPending ? 'Đang gửi…' : 'Lưu & Gửi Duyệt →'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,6 +37,9 @@ export function PeWorkflowPanel({
|
|||||||
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
||||||
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
||||||
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối. Default false (admin opt-in
|
||||||
|
// per slot tick → checkbox visible trong dialog Approve, default unchecked).
|
||||||
|
const [skipToFinalApprover, setSkipToFinalApprover] = useState(false)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
@ -96,6 +99,9 @@ export function PeWorkflowPanel({
|
|||||||
returnMode: isTraLaiAction ? returnMode : null,
|
returnMode: isTraLaiAction ? returnMode : null,
|
||||||
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
||||||
? returnTargetUserId : null,
|
? returnTargetUserId : null,
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver scope ChoDuyet duyệt thẳng Cấp cuối.
|
||||||
|
// BE check matchingLevel.AllowApproverSkipToFinal (admin opt-in per slot).
|
||||||
|
skipToFinal: !isReject && skipToFinalApprover,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -107,6 +113,7 @@ export function PeWorkflowPanel({
|
|||||||
setComment('')
|
setComment('')
|
||||||
setReturnMode(WorkflowReturnMode.Drafter)
|
setReturnMode(WorkflowReturnMode.Drafter)
|
||||||
setReturnTargetUserId(null)
|
setReturnTargetUserId(null)
|
||||||
|
setSkipToFinalApprover(false)
|
||||||
},
|
},
|
||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
@ -394,6 +401,32 @@ export function PeWorkflowPanel({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* Mig 31 (S23 t1) — F2 Approver toggle: chỉ visible khi Approve forward
|
||||||
|
+ admin tick AllowApproverSkipToFinal cho slot Cấp hiện tại. */}
|
||||||
|
{!isCancel && !isSendBack && levelOptions?.allowApproverSkipToFinal && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="flex cursor-pointer items-start gap-2 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[12px] text-violet-800 hover:bg-violet-100/60">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-0.5"
|
||||||
|
checked={skipToFinalApprover}
|
||||||
|
onChange={e => setSkipToFinalApprover(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
|
||||||
|
<span className="mt-0.5 block text-[11px] text-violet-700/80">
|
||||||
|
Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) — bỏ qua mọi Cấp/Bước còn lại.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCancel && !isSendBack && skipToFinalApprover && (
|
||||||
|
<div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
|
||||||
|
⚠ Hành động KHÔNG quay lại được (trừ khi Drafter reset toàn bộ). Phiếu sẽ
|
||||||
|
skip qua tất cả Cấp/Bước còn lại và chuyển thẳng "Đã duyệt".
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Label>Ghi chú (tùy chọn)</Label>
|
<Label>Ghi chú (tùy chọn)</Label>
|
||||||
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -354,6 +354,7 @@ export type ApprovalWorkflowOptions = {
|
|||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
||||||
|
allowApproverSkipToFinal: boolean // Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
||||||
@ -396,13 +397,11 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options của
|
||||||
// nếu V1 legacy hoặc pointer chưa init. FE render Trả lại dropdown + Edit
|
// Cấp hiện tại (per-NV slot). Null nếu V1 legacy hoặc pointer chưa init. FE
|
||||||
// Section 2 conditional theo flag của slot Approver đang duyệt.
|
// render Trả lại dropdown + Edit Section 2/4 + Duyệt thẳng Cấp cuối
|
||||||
|
// conditional theo flag của slot Approver đang duyệt.
|
||||||
currentLevelOptions: ApprovalWorkflowOptions | null
|
currentLevelOptions: ApprovalWorkflowOptions | null
|
||||||
// Mig 29 — F2 per-Drafter: cờ AllowDrafterSkipToFinal của Drafter user pin
|
|
||||||
// phiếu. Workspace conditional render checkbox "Gửi thẳng Cấp cuối".
|
|
||||||
drafterAllowSkipToFinal: boolean
|
|
||||||
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
currentApproval: PeCurrentApproval | null
|
currentApproval: PeCurrentApproval | null
|
||||||
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
||||||
|
|||||||
Reference in New Issue
Block a user