[CLAUDE] FE-PE: S21 t5 Chunk C — eOffice read currentLevelOptions + drafterAllowSkipToFinal (per-NV) mirror 2 app
Types refactor `fe-{admin,user}/src/types/purchaseEvaluation.ts`:
- `ApprovalWorkflowOptions` REMOVE allowDrafterSkipToFinal (F2 đã move per-User).
Còn 5 flag (F1 4 mode + F3 EditDetails).
- `PeDetailBundle`:
- RENAME `workflowOptions` → `currentLevelOptions` (clearer semantic per-slot)
- ADD `drafterAllowSkipToFinal: boolean` (BE resolve từ DrafterUserId → User entity)
PeWorkflowPanel.tsx (mirror 2 app):
- RENAME local var `wfOptions` → `levelOptions`
- READ `evaluation.currentLevelOptions` (Cấp hiện tại)
- 4 mode radio render conditional theo levelOptions.allowReturnXxx (unchanged
logic, just rename source)
PeDetailTabs.tsx (mirror 2 app):
- F3 approverEditMode: READ `evaluation.currentLevelOptions?.allowApproverEditDetails`
thay vì workflowOptions.allowApproverEditDetails (semantic per-NV slot)
- F2 allowSkipToFinal: READ `evaluation.drafterAllowSkipToFinal` thay vì
workflowOptions.allowDrafterSkipToFinal (semantic per-Drafter user)
Backward compat verified:
- Phiếu cũ trước Mig 29 vẫn return currentLevelOptions populated (BE backfill
Mig 29 đã copy 5 Allow* per Level)
- drafterAllowSkipToFinal: BE backfill chỉ TRUE cho user từng Drafter PE link
workflow.AllowDrafterSkipToFinal=true (preserve admin config S21 t4)
- Phiếu V1 legacy: currentLevelOptions=null → FE fallback chỉ Drafter mode
Verify:
- npm run build × 2 app pass (fe-user 450ms + fe-admin 439ms, cache hot)
- 0 TS6 err, warning chunk size pre-existing
Pending Chunk D: Docs (schema-diagram §14 update + STATUS + HANDOFF + session log).
Note: User Management page chưa có F2 checkbox UX (defer commit sau khi admin
UAT request — BE field đã có, FE chỉ cần thêm 1 toggle vào UserEdit dialog).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -108,16 +108,17 @@ export function PeDetailTabs({
|
|||||||
const actorMatchesLevel = isAdmin
|
const actorMatchesLevel = isAdmin
|
||||||
|| (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
|
|| (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
|
||||||
const approverEditMode = evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
|
const approverEditMode = evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
|
||||||
&& (evaluation.workflowOptions?.allowApproverEditDetails ?? false)
|
// Mig 29 (S21 t5) — read F3 từ currentLevelOptions (per-NV slot)
|
||||||
|
&& (evaluation.currentLevelOptions?.allowApproverEditDetails ?? false)
|
||||||
&& actorMatchesLevel
|
&& actorMatchesLevel
|
||||||
const itemsReadOnly = readOnly && !approverEditMode
|
const itemsReadOnly = readOnly && !approverEditMode
|
||||||
|
|
||||||
// "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 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối. Workflow phải bật flag.
|
// Mig 29 (S21 t5) — F2: per-Drafter user flag (User Management page).
|
||||||
const [skipToFinal, setSkipToFinal] = useState(false)
|
const [skipToFinal, setSkipToFinal] = useState(false)
|
||||||
const allowSkipToFinal = evaluation.workflowOptions?.allowDrafterSkipToFinal ?? false
|
const allowSkipToFinal = evaluation.drafterAllowSkipToFinal ?? false
|
||||||
|
|
||||||
const submitForApproval = useMutation({
|
const submitForApproval = useMutation({
|
||||||
mutationFn: async (opts: { skipToFinal: boolean }) => {
|
mutationFn: async (opts: { skipToFinal: boolean }) => {
|
||||||
|
|||||||
@ -41,8 +41,8 @@ export function PeWorkflowPanel({
|
|||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
|
|
||||||
// Mig 28 — F1 workflow options. Null nếu V1 legacy → fallback chỉ "Trả về Drafter".
|
// Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại).
|
||||||
const wfOptions = evaluation.workflowOptions
|
const levelOptions = evaluation.currentLevelOptions
|
||||||
// List approvers đã ký (cho mode Assignee dropdown pick)
|
// List approvers đã ký (cho mode Assignee dropdown pick)
|
||||||
const signedApprovers = (evaluation.levelOpinions ?? [])
|
const signedApprovers = (evaluation.levelOpinions ?? [])
|
||||||
.map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' }))
|
.map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' }))
|
||||||
@ -307,15 +307,15 @@ export function PeWorkflowPanel({
|
|||||||
<>
|
<>
|
||||||
{/* Mig 28 (S21 t4) — F1 mode picker khi Trả lại. Show modes
|
{/* Mig 28 (S21 t4) — F1 mode picker khi Trả lại. Show modes
|
||||||
enabled per workflow.options. Default Drafter (S17 fallback). */}
|
enabled per workflow.options. Default Drafter (S17 fallback). */}
|
||||||
{(wfOptions?.allowReturnOneLevel
|
{(levelOptions?.allowReturnOneLevel
|
||||||
|| wfOptions?.allowReturnOneStep
|
|| levelOptions?.allowReturnOneStep
|
||||||
|| wfOptions?.allowReturnToAssignee
|
|| levelOptions?.allowReturnToAssignee
|
||||||
|| wfOptions?.allowReturnToDrafter
|
|| levelOptions?.allowReturnToDrafter
|
||||||
|| !wfOptions) && (
|
|| !levelOptions) && (
|
||||||
<div className="mb-3 space-y-1.5">
|
<div className="mb-3 space-y-1.5">
|
||||||
<Label className="text-[12px]">Chọn cách Trả lại</Label>
|
<Label className="text-[12px]">Chọn cách Trả lại</Label>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{(wfOptions?.allowReturnOneLevel) && (
|
{(levelOptions?.allowReturnOneLevel) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -329,7 +329,7 @@ export function PeWorkflowPanel({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{(wfOptions?.allowReturnOneStep) && (
|
{(levelOptions?.allowReturnOneStep) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -343,7 +343,7 @@ export function PeWorkflowPanel({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{(wfOptions?.allowReturnToAssignee) && (
|
{(levelOptions?.allowReturnToAssignee) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -369,7 +369,7 @@ export function PeWorkflowPanel({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{(wfOptions?.allowReturnToDrafter !== false) && (
|
{(levelOptions?.allowReturnToDrafter !== false) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|||||||
@ -347,13 +347,13 @@ export type PeDepartmentApproval = {
|
|||||||
isBypassed: boolean
|
isBypassed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 28 (S21 t4) — 6 advanced options của workflow pin.
|
// Mig 29 (S21 t5) — 5 Allow* options của Cấp Approver hiện tại (per-NV slot).
|
||||||
|
// F2 (Drafter skip) MOVED sang per-User (xem PeDetailBundle.drafterAllowSkipToFinal).
|
||||||
export type ApprovalWorkflowOptions = {
|
export type ApprovalWorkflowOptions = {
|
||||||
allowReturnOneLevel: boolean
|
allowReturnOneLevel: boolean
|
||||||
allowReturnOneStep: boolean
|
allowReturnOneStep: boolean
|
||||||
allowReturnToAssignee: boolean
|
allowReturnToAssignee: boolean
|
||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowDrafterSkipToFinal: boolean
|
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,8 +397,10 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 28 (S21 t4) — 6 advanced options của workflow pin. Null nếu V1 legacy.
|
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot).
|
||||||
workflowOptions: 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
|
||||||
|
|||||||
@ -110,15 +110,16 @@ export function PeDetailTabs({
|
|||||||
const actorMatchesLevel = isAdmin
|
const actorMatchesLevel = isAdmin
|
||||||
|| (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
|
|| (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
|
||||||
const approverEditMode = evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
|
const approverEditMode = evaluation.phase === PurchaseEvaluationPhase.ChoDuyet
|
||||||
&& (evaluation.workflowOptions?.allowApproverEditDetails ?? false)
|
// Mig 29 (S21 t5) — read F3 từ currentLevelOptions (per-NV slot)
|
||||||
|
&& (evaluation.currentLevelOptions?.allowApproverEditDetails ?? false)
|
||||||
&& actorMatchesLevel
|
&& actorMatchesLevel
|
||||||
// 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 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối. Workflow phải bật flag.
|
// Mig 29 (S21 t5) — F2: per-Drafter user flag (KHÔNG còn workflow-level).
|
||||||
// Default false (gửi tuần tự như cũ). Sync state với confirm dialog handler.
|
// Admin cấu hình ở User Management page → BE resolve qua DrafterUserId.
|
||||||
const [skipToFinal, setSkipToFinal] = useState(false)
|
const [skipToFinal, setSkipToFinal] = useState(false)
|
||||||
const allowSkipToFinal = evaluation.workflowOptions?.allowDrafterSkipToFinal ?? false
|
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
|
||||||
|
|||||||
@ -41,8 +41,9 @@ export function PeWorkflowPanel({
|
|||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
|
|
||||||
// Mig 28 — F1 workflow options. Null nếu V1 legacy → fallback chỉ "Trả về Drafter".
|
// Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại). Null nếu
|
||||||
const wfOptions = evaluation.workflowOptions
|
// V1 legacy hoặc pointer chưa init → fallback chỉ "Trả về Drafter".
|
||||||
|
const levelOptions = evaluation.currentLevelOptions
|
||||||
// List approvers đã ký (cho mode Assignee dropdown pick)
|
// List approvers đã ký (cho mode Assignee dropdown pick)
|
||||||
const signedApprovers = (evaluation.levelOpinions ?? [])
|
const signedApprovers = (evaluation.levelOpinions ?? [])
|
||||||
.map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' }))
|
.map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' }))
|
||||||
@ -302,15 +303,15 @@ export function PeWorkflowPanel({
|
|||||||
<>
|
<>
|
||||||
{/* Mig 28 (S21 t4) — F1 mode picker khi Trả lại. Show modes
|
{/* Mig 28 (S21 t4) — F1 mode picker khi Trả lại. Show modes
|
||||||
enabled per workflow.options. Default Drafter (S17 fallback). */}
|
enabled per workflow.options. Default Drafter (S17 fallback). */}
|
||||||
{(wfOptions?.allowReturnOneLevel
|
{(levelOptions?.allowReturnOneLevel
|
||||||
|| wfOptions?.allowReturnOneStep
|
|| levelOptions?.allowReturnOneStep
|
||||||
|| wfOptions?.allowReturnToAssignee
|
|| levelOptions?.allowReturnToAssignee
|
||||||
|| wfOptions?.allowReturnToDrafter
|
|| levelOptions?.allowReturnToDrafter
|
||||||
|| !wfOptions) && (
|
|| !levelOptions) && (
|
||||||
<div className="mb-3 space-y-1.5">
|
<div className="mb-3 space-y-1.5">
|
||||||
<Label className="text-[12px]">Chọn cách Trả lại</Label>
|
<Label className="text-[12px]">Chọn cách Trả lại</Label>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{(wfOptions?.allowReturnOneLevel) && (
|
{(levelOptions?.allowReturnOneLevel) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -324,7 +325,7 @@ export function PeWorkflowPanel({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{(wfOptions?.allowReturnOneStep) && (
|
{(levelOptions?.allowReturnOneStep) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -338,7 +339,7 @@ export function PeWorkflowPanel({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{(wfOptions?.allowReturnToAssignee) && (
|
{(levelOptions?.allowReturnToAssignee) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -364,7 +365,7 @@ export function PeWorkflowPanel({
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{(wfOptions?.allowReturnToDrafter !== false) && (
|
{(levelOptions?.allowReturnToDrafter !== false) && (
|
||||||
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
<label className="flex items-start gap-2 rounded border border-amber-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|||||||
@ -344,13 +344,14 @@ export type PeDepartmentApproval = {
|
|||||||
isBypassed: boolean
|
isBypassed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 28 (S21 t4) — 6 advanced options của workflow pin.
|
// Mig 29 (S21 t5) — 5 Allow* options của Cấp Approver hiện tại (per-NV slot).
|
||||||
|
// FE filter Trả lại dropdown + Edit Section 2 enabled theo flag của slot này.
|
||||||
|
// F2 (Drafter skip) MOVED sang per-User (xem PeDetailBundle.drafterAllowSkipToFinal).
|
||||||
export type ApprovalWorkflowOptions = {
|
export type ApprovalWorkflowOptions = {
|
||||||
allowReturnOneLevel: boolean
|
allowReturnOneLevel: boolean
|
||||||
allowReturnOneStep: boolean
|
allowReturnOneStep: boolean
|
||||||
allowReturnToAssignee: boolean
|
allowReturnToAssignee: boolean
|
||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowDrafterSkipToFinal: boolean
|
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,9 +395,13 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 28 (S21 t4) — 6 advanced options của workflow pin. Null nếu V1 legacy.
|
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null
|
||||||
// FE filter Trả lại dropdown + Skip submit + Edit Section 2 conditional.
|
// nếu V1 legacy hoặc pointer chưa init. FE render Trả lại dropdown + Edit
|
||||||
workflowOptions: ApprovalWorkflowOptions | null
|
// Section 2 conditional theo flag của slot Approver đang duyệt.
|
||||||
|
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