[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:
pqhuy1987
2026-05-13 20:09:31 +07:00
parent 63234b2cce
commit 5ccb2a7057
6 changed files with 48 additions and 38 deletions

View File

@ -108,16 +108,17 @@ export function PeDetailTabs({
const actorMatchesLevel = isAdmin
|| (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
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
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.
// Mig 29 (S21 t5) — F2: per-Drafter user flag (User Management page).
const [skipToFinal, setSkipToFinal] = useState(false)
const allowSkipToFinal = evaluation.workflowOptions?.allowDrafterSkipToFinal ?? false
const allowSkipToFinal = evaluation.drafterAllowSkipToFinal ?? false
const submitForApproval = useMutation({
mutationFn: async (opts: { skipToFinal: boolean }) => {

View File

@ -41,8 +41,8 @@ export function PeWorkflowPanel({
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
// Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại).
const levelOptions = evaluation.currentLevelOptions
// List approvers đã ký (cho mode Assignee dropdown pick)
const signedApprovers = (evaluation.levelOpinions ?? [])
.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
enabled per workflow.options. Default Drafter (S17 fallback). */}
{(wfOptions?.allowReturnOneLevel
|| wfOptions?.allowReturnOneStep
|| wfOptions?.allowReturnToAssignee
|| wfOptions?.allowReturnToDrafter
|| !wfOptions) && (
{(levelOptions?.allowReturnOneLevel
|| levelOptions?.allowReturnOneStep
|| levelOptions?.allowReturnToAssignee
|| levelOptions?.allowReturnToDrafter
|| !levelOptions) && (
<div className="mb-3 space-y-1.5">
<Label className="text-[12px]">Chọn cách Trả lại</Label>
<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">
<input
type="radio"
@ -329,7 +329,7 @@ export function PeWorkflowPanel({
</span>
</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">
<input
type="radio"
@ -343,7 +343,7 @@ export function PeWorkflowPanel({
</span>
</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">
<input
type="radio"
@ -369,7 +369,7 @@ export function PeWorkflowPanel({
</span>
</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">
<input
type="radio"

View File

@ -347,13 +347,13 @@ export type PeDepartmentApproval = {
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 = {
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowDrafterSkipToFinal: boolean
allowApproverEditDetails: boolean
}
@ -397,8 +397,10 @@ export type PeDetailBundle = {
approvalWorkflowCode: string | null
approvalWorkflowName: string | null
approvalWorkflowVersion: number | null
// Mig 28 (S21 t4) — 6 advanced options của workflow pin. Null nếu V1 legacy.
workflowOptions: ApprovalWorkflowOptions | null
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot).
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)
currentApproval: PeCurrentApproval | null
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level

View File

@ -110,15 +110,16 @@ export function PeDetailTabs({
const actorMatchesLevel = isAdmin
|| (currentUser?.id != null && v2Approvers.some(a => a.userId === currentUser.id))
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
// itemsReadOnly = readOnly trừ khi approver mode F3 mở
const itemsReadOnly = readOnly && !approverEditMode
// Mig 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối. Workflow phải bật flag.
// Default false (gửi tuần tự như cũ). Sync state với confirm dialog handler.
// Mig 29 (S21 t5) — F2: per-Drafter user flag (KHÔNG còn workflow-level).
// Admin cấu hình ở User Management page → BE resolve qua DrafterUserId.
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
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing

View File

@ -41,8 +41,9 @@ export function PeWorkflowPanel({
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
// Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại). Null nếu
// 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)
const signedApprovers = (evaluation.levelOpinions ?? [])
.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
enabled per workflow.options. Default Drafter (S17 fallback). */}
{(wfOptions?.allowReturnOneLevel
|| wfOptions?.allowReturnOneStep
|| wfOptions?.allowReturnToAssignee
|| wfOptions?.allowReturnToDrafter
|| !wfOptions) && (
{(levelOptions?.allowReturnOneLevel
|| levelOptions?.allowReturnOneStep
|| levelOptions?.allowReturnToAssignee
|| levelOptions?.allowReturnToDrafter
|| !levelOptions) && (
<div className="mb-3 space-y-1.5">
<Label className="text-[12px]">Chọn cách Trả lại</Label>
<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">
<input
type="radio"
@ -324,7 +325,7 @@ export function PeWorkflowPanel({
</span>
</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">
<input
type="radio"
@ -338,7 +339,7 @@ export function PeWorkflowPanel({
</span>
</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">
<input
type="radio"
@ -364,7 +365,7 @@ export function PeWorkflowPanel({
</span>
</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">
<input
type="radio"

View File

@ -344,13 +344,14 @@ export type PeDepartmentApproval = {
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 = {
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowDrafterSkipToFinal: boolean
allowApproverEditDetails: boolean
}
@ -394,9 +395,13 @@ export type PeDetailBundle = {
approvalWorkflowCode: string | null
approvalWorkflowName: string | null
approvalWorkflowVersion: number | null
// Mig 28 (S21 t4) — 6 advanced options của workflow pin. Null nếu V1 legacy.
// FE filter Trả lại dropdown + Skip submit + Edit Section 2 conditional.
workflowOptions: ApprovalWorkflowOptions | null
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null
// nếu V1 legacy hoặc pointer chưa init. FE render Trả lại dropdown + Edit
// 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)
currentApproval: PeCurrentApproval | null
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level