[CLAUDE] PurchaseEvaluation: Plan AC — fix Lịch sử duyệt panel show Trả lại + Duyệt vượt cấp
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m27s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m27s
Bro UAT 2026-05-19 screenshot: panel "Lịch sử duyệt" KHÔNG show Return mode events (Bro Trả lại từ Phan Văn Chương → Trà missing) + KHÔNG distinct event Duyệt vượt cấp (skipToFinal F2). Root cause: - PurchaseEvaluationApprovals.Add() chỉ ở Approve branch (line 472 V2 + 660 V1) - Reject branch line 75-103 NEVER adds Approval row — chỉ log Changelog - skipToFinal advance branch line 532-572 dùng existing line 472 row nhưng comment KHÔNG distinct "vượt cấp" semantic vs approve thường Fix Plan AC: 1. BE Service.cs Reject branch (line 75-103): capture pre-call Step/Level trước ApplyReturnModeAsync mutate pointer, add Approval row sau khi mutate: Decision=Reject + FromPhase + ToPhase=evaluation.Phase + Comment carry from-position + mode summary. Cover cả Trả lại (TraLai+pointer-mode) + Từ chối (TuChoi terminal). 2. BE Service.cs line 472 Approve branch: enrich Comment với prefix "[Duyệt vượt cấp tới Cấp cuối]" khi skipToFinal=true để Lịch sử duyệt distinguish vượt cấp với approve thường. 3. FE PeDetailTabs.tsx × 2 app ApprovalsTab: add Decision badge phân biệt Approve (emerald) / Trả lại (amber) / Từ chối (rose). Vì 3/4 mode Trả lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase→toPhase badge giống Approve. Decision badge bù visual phân biệt. Verify: - dotnet build clean 0 err 2 warn (pre-existing DocxRenderer) - dotnet test 111/111 PASS - npm build × fe-user + fe-admin PASS 0 TS err Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -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 <p className="text-sm text-slate-500">Chưa có bước duyệt nào.</p>
|
||||
return (
|
||||
<ol className="space-y-2">
|
||||
{ev.approvals.map(a => (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||
</span>
|
||||
<span className="mx-2">→</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||
</span>
|
||||
{ev.approvals.map(a => {
|
||||
const dec = decisionBadge(a.decision, a.toPhase)
|
||||
return (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium', dec.cls)}>
|
||||
{dec.label}
|
||||
</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||
</span>
|
||||
<span className="text-slate-400">→</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 <p className="text-sm text-slate-500">Chưa có bước duyệt nào.</p>
|
||||
return (
|
||||
<ol className="space-y-2">
|
||||
{ev.approvals.map(a => (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||
</span>
|
||||
<span className="mx-2">→</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||
</span>
|
||||
{ev.approvals.map(a => {
|
||||
const dec = decisionBadge(a.decision, a.toPhase)
|
||||
return (
|
||||
<li key={a.id} className="rounded border border-slate-200 bg-white p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px] font-medium', dec.cls)}>
|
||||
{dec.label}
|
||||
</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||
</span>
|
||||
<span className="text-slate-400">→</span>
|
||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user