diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index e73669b..b642435 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -1999,29 +1999,48 @@ function QuoteDialog({ } // ===== Tab: Duyệt ===== +// Plan AC S25 Bug 3 — Decision badge phân biệt Approve / Trả lại / Từ chối. +// 3/4 mode Trả lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase +// + toPhase badge giống hệt Approve. Decision badge bù visual phân biệt. +const PE_DECISION_REJECT = 2 +function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } { + if (decision === PE_DECISION_REJECT) { + // Reject phân biệt: TuChoi(99) = "Từ chối" / TraLai(98) hoặc ChoDuyet(10) = "Trả lại" + if (toPhase === 99) return { label: 'Từ chối', cls: 'bg-rose-100 text-rose-700 border border-rose-200' } + return { label: 'Trả lại', cls: 'bg-amber-100 text-amber-700 border border-amber-200' } + } + return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' } +} + function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { if (ev.approvals.length === 0) return

Chưa có bước duyệt nào.

return (
    - {ev.approvals.map(a => ( -
  1. -
    -
    - - {PurchaseEvaluationPhaseLabel[a.fromPhase]} - - - - {PurchaseEvaluationPhaseLabel[a.toPhase]} - + {ev.approvals.map(a => { + const dec = decisionBadge(a.decision, a.toPhase) + return ( +
  2. +
    +
    + + {dec.label} + + + {PurchaseEvaluationPhaseLabel[a.fromPhase]} + + + + {PurchaseEvaluationPhaseLabel[a.toPhase]} + +
    + {new Date(a.approvedAt).toLocaleString('vi-VN')}
    - {new Date(a.approvedAt).toLocaleString('vi-VN')} - -
    - {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} -
    -
  3. - ))} +
    + {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} +
    + + ) + })}
) } diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 4413698..84a22cf 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -1993,29 +1993,48 @@ function QuoteDialog({ } // ===== Tab: Duyệt ===== +// Plan AC S25 Bug 3 — Decision badge phân biệt Approve / Trả lại / Từ chối. +// 3/4 mode Trả lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase +// + toPhase badge giống hệt Approve. Decision badge bù visual phân biệt. +const PE_DECISION_REJECT = 2 +function decisionBadge(decision: number, toPhase: number): { label: string; cls: string } { + if (decision === PE_DECISION_REJECT) { + // Reject phân biệt: TuChoi(99) = "Từ chối" / TraLai(98) hoặc ChoDuyet(10) = "Trả lại" + if (toPhase === 99) return { label: 'Từ chối', cls: 'bg-rose-100 text-rose-700 border border-rose-200' } + return { label: 'Trả lại', cls: 'bg-amber-100 text-amber-700 border border-amber-200' } + } + return { label: 'Duyệt', cls: 'bg-emerald-100 text-emerald-700 border border-emerald-200' } +} + function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { if (ev.approvals.length === 0) return

Chưa có bước duyệt nào.

return (
    - {ev.approvals.map(a => ( -
  1. -
    -
    - - {PurchaseEvaluationPhaseLabel[a.fromPhase]} - - - - {PurchaseEvaluationPhaseLabel[a.toPhase]} - + {ev.approvals.map(a => { + const dec = decisionBadge(a.decision, a.toPhase) + return ( +
  2. +
    +
    + + {dec.label} + + + {PurchaseEvaluationPhaseLabel[a.fromPhase]} + + + + {PurchaseEvaluationPhaseLabel[a.toPhase]} + +
    + {new Date(a.approvedAt).toLocaleString('vi-VN')}
    - {new Date(a.approvedAt).toLocaleString('vi-VN')} - -
    - {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} -
    -
  3. - ))} +
    + {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} +
    + + ) + })}
) } diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs index 361b896..5fad08b 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs @@ -80,6 +80,13 @@ public class PurchaseEvaluationWorkflowService( // guard tránh request forge non-approver gọi PATCH direct. await EnsureCanRejectV2Async(evaluation, actorUserId, isAdmin, ct); + // Plan AC S25 Bug 3a — capture pre-call Step/Level để log Approval row + // chính xác (ApplyReturnModeAsync mutate pointer cho 3 mode OneLevel/ + // OneStep/Assignee). FE ApprovalsTab render `ev.approvals` — trước + // đây Reject KHÔNG add row → Lịch sử duyệt panel mất event Trả lại. + var fromStepIdx = evaluation.CurrentWorkflowStepIndex; + var fromLevelOrder = evaluation.CurrentApprovalLevelOrder; + if (targetPhase == PurchaseEvaluationPhase.TuChoi) { // Từ chối hoàn toàn — phiếu khoá vĩnh viễn (lock edit Mig 16). @@ -97,6 +104,25 @@ public class PurchaseEvaluationWorkflowService( ? returnSummary : $"{comment} [{returnSummary}]"; } + + // Plan AC S25 Bug 3a — add Approval row cho Lịch sử duyệt panel + // (FE ApprovalsTab). Decision=Reject + Comment carry mode + summary. + // From-position dùng pre-call Step/Level để show actor đã trả lại + // từ Bước/Cấp nào. + var fromPos = fromStepIdx.HasValue && fromLevelOrder.HasValue + ? $"[Bước {fromStepIdx.Value + 1} — Cấp {fromLevelOrder.Value}] " + : ""; + db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval + { + PurchaseEvaluationId = evaluation.Id, + FromPhase = fromPhase, + ToPhase = evaluation.Phase, + ApproverUserId = actorUserId, + Decision = ApprovalDecision.Reject, + Comment = $"{fromPos}{comment ?? ""}".Trim(), + ApprovedAt = dateTime.UtcNow, + }); + await LogTransitionAsync(evaluation, fromPhase, evaluation.Phase, actorUserId, decision, comment, ct); await db.SaveChangesAsync(ct); return; @@ -468,7 +494,10 @@ public class PurchaseEvaluationWorkflowService( } } - // Log approval + // Log approval. Plan AC S25 Bug 3b — enrich comment với prefix + // "[Duyệt vượt cấp]" khi skipToFinal=true để FE Lịch sử duyệt panel + // phân biệt rõ event vượt cấp với approve thường. + var skipPrefix = skipToFinal ? "[Duyệt vượt cấp tới Cấp cuối] " : ""; db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval { PurchaseEvaluationId = evaluation.Id, @@ -476,7 +505,7 @@ public class PurchaseEvaluationWorkflowService( ToPhase = evaluation.Phase, ApproverUserId = actorUserId, Decision = ApprovalDecision.Approve, - Comment = $"[Bước {currentIdx + 1} — Cấp {currentLevelOrder}] {comment ?? ""}", + Comment = $"{skipPrefix}[Bước {currentIdx + 1} — Cấp {currentLevelOrder}] {comment ?? ""}".Trim(), ApprovedAt = dateTime.UtcNow, });