[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,18 +1999,36 @@ function QuoteDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Tab: Duyệt =====
|
// ===== 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 }) {
|
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>
|
if (ev.approvals.length === 0) return <p className="text-sm text-slate-500">Chưa có bước duyệt nào.</p>
|
||||||
return (
|
return (
|
||||||
<ol className="space-y-2">
|
<ol className="space-y-2">
|
||||||
{ev.approvals.map(a => (
|
{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">
|
<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 justify-between">
|
||||||
<div>
|
<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])}>
|
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-2">→</span>
|
<span className="text-slate-400">→</span>
|
||||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||||
</span>
|
</span>
|
||||||
@ -2021,7 +2039,8 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1993,18 +1993,36 @@ function QuoteDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Tab: Duyệt =====
|
// ===== 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 }) {
|
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>
|
if (ev.approvals.length === 0) return <p className="text-sm text-slate-500">Chưa có bước duyệt nào.</p>
|
||||||
return (
|
return (
|
||||||
<ol className="space-y-2">
|
<ol className="space-y-2">
|
||||||
{ev.approvals.map(a => (
|
{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">
|
<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 justify-between">
|
||||||
<div>
|
<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])}>
|
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.fromPhase])}>
|
||||||
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
{PurchaseEvaluationPhaseLabel[a.fromPhase]}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-2">→</span>
|
<span className="text-slate-400">→</span>
|
||||||
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
<span className={cn('rounded px-1.5 py-0.5 text-[11px]', PurchaseEvaluationPhaseColor[a.toPhase])}>
|
||||||
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
{PurchaseEvaluationPhaseLabel[a.toPhase]}
|
||||||
</span>
|
</span>
|
||||||
@ -2015,7 +2033,8 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,6 +80,13 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
// guard tránh request forge non-approver gọi PATCH direct.
|
// guard tránh request forge non-approver gọi PATCH direct.
|
||||||
await EnsureCanRejectV2Async(evaluation, actorUserId, isAdmin, ct);
|
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)
|
if (targetPhase == PurchaseEvaluationPhase.TuChoi)
|
||||||
{
|
{
|
||||||
// Từ chối hoàn toàn — phiếu khoá vĩnh viễn (lock edit Mig 16).
|
// 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
|
? returnSummary
|
||||||
: $"{comment} [{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 LogTransitionAsync(evaluation, fromPhase, evaluation.Phase, actorUserId, decision, comment, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
return;
|
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
|
db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval
|
||||||
{
|
{
|
||||||
PurchaseEvaluationId = evaluation.Id,
|
PurchaseEvaluationId = evaluation.Id,
|
||||||
@ -476,7 +505,7 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
ToPhase = evaluation.Phase,
|
ToPhase = evaluation.Phase,
|
||||||
ApproverUserId = actorUserId,
|
ApproverUserId = actorUserId,
|
||||||
Decision = ApprovalDecision.Approve,
|
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,
|
ApprovedAt = dateTime.UtcNow,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user