[CLAUDE] PurchaseEvaluation: Plan AB Chunk A — fix Changelog visibility Bug 1 Budget Adjust + Bug 2 Return Mode
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m6s
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m6s
- BE ApplyReturnModeAsync 4 mode add Changelog.Add() common path (refactor Drafter early return) - FE PeDetailTabs.tsx HistoryTab filter extend cover Header+ngân sách (B1) + Workflow+Trả lại (B2) - FE empty placeholder + comment update reflect new filter scope - Mirror 2 app §3.9 Bug 1: Budget Adjust handler đã log (Header+Update) nhưng FE filter strict TraLai-only Bug 2: Return mode Service không log Changelog — chỉ approval phase transition Verify: - Build clean 0 err - npm build × 2 app pass 0 TS err - 111 test baseline preserve (UAT skip test-after defer) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -2033,24 +2033,31 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
|
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
|
||||||
})
|
})
|
||||||
if (logs.isLoading) return <p className="text-sm text-slate-500">Đang tải…</p>
|
if (logs.isLoading) return <p className="text-sm text-slate-500">Đang tải…</p>
|
||||||
// User UAT 2026-05-08: chỉ track events liên quan Trả lại + Gửi duyệt lại.
|
// User UAT 2026-05-08: chỉ track events Trả lại + Gửi duyệt lại.
|
||||||
// Bỏ trạng thái duyệt (Cấp 1 → Cấp 2 → DaDuyet) + bỏ thay đổi trước Trả lại.
|
// User UAT 2026-05-19: + track Budget Adjust (Bug 1) + 4 mode Trả lại (Bug 2).
|
||||||
// Filter giữ:
|
// Filter giữ:
|
||||||
// - Workflow transition về TraLai (phaseAtChange = TraLai = 98)
|
// - Workflow transition về TraLai (phaseAtChange = TraLai = 98)
|
||||||
// - Workflow transition từ TraLai → khác (Drafter gửi lại — summary chứa "TraLai →")
|
// - Workflow transition từ TraLai → khác (Drafter gửi lại — summary "TraLai →")
|
||||||
// - Mọi thay đổi nội dung khi phaseAtChange = TraLai (sửa trong giai đoạn chờ gửi lại)
|
// - Workflow Trả lại 4 mode (summary chứa "Trả lại" — Plan AB S25 fix Bug 2)
|
||||||
|
// - Header Budget Adjust (summary chứa "ngân sách" — Plan AB S25 fix Bug 1)
|
||||||
|
// - Mọi thay đổi nội dung khi phaseAtChange = TraLai (Drafter sửa trước gửi lại)
|
||||||
// BE giữ data đầy đủ (audit trail) — chỉ filter ở UI, reversible.
|
// BE giữ data đầy đủ (audit trail) — chỉ filter ở UI, reversible.
|
||||||
const PE_PHASE_TRALAI = 98
|
const PE_PHASE_TRALAI = 98
|
||||||
const PE_ENTITY_WORKFLOW = 5
|
const PE_ENTITY_WORKFLOW = 5
|
||||||
|
const PE_ENTITY_HEADER = 1
|
||||||
const filtered = (logs.data ?? []).filter(l => {
|
const filtered = (logs.data ?? []).filter(l => {
|
||||||
if (l.entityType === PE_ENTITY_WORKFLOW) {
|
if (l.entityType === PE_ENTITY_WORKFLOW) {
|
||||||
if (l.phaseAtChange === PE_PHASE_TRALAI) return true
|
if (l.phaseAtChange === PE_PHASE_TRALAI) return true
|
||||||
if (l.summary?.includes('TraLai →')) return true
|
if (l.summary?.includes('TraLai →')) return true
|
||||||
|
if (l.summary?.includes('Trả lại')) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (l.entityType === PE_ENTITY_HEADER && l.summary?.toLowerCase().includes('ngân sách')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return l.phaseAtChange === PE_PHASE_TRALAI
|
return l.phaseAtChange === PE_PHASE_TRALAI
|
||||||
})
|
})
|
||||||
if (filtered.length === 0) return <p className="text-sm text-slate-500">Chưa có lịch sử trả lại / gửi duyệt lại.</p>
|
if (filtered.length === 0) return <p className="text-sm text-slate-500">Chưa có lịch sử trả lại / điều chỉnh ngân sách / gửi duyệt lại.</p>
|
||||||
return (
|
return (
|
||||||
<ol className="space-y-1.5 text-sm">
|
<ol className="space-y-1.5 text-sm">
|
||||||
{filtered.map(l => (
|
{filtered.map(l => (
|
||||||
|
|||||||
@ -2027,24 +2027,31 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
|
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
|
||||||
})
|
})
|
||||||
if (logs.isLoading) return <p className="text-sm text-slate-500">Đang tải…</p>
|
if (logs.isLoading) return <p className="text-sm text-slate-500">Đang tải…</p>
|
||||||
// User UAT 2026-05-08: chỉ track events liên quan Trả lại + Gửi duyệt lại.
|
// User UAT 2026-05-08: chỉ track events Trả lại + Gửi duyệt lại.
|
||||||
// Bỏ trạng thái duyệt (Cấp 1 → Cấp 2 → DaDuyet) + bỏ thay đổi trước Trả lại.
|
// User UAT 2026-05-19: + track Budget Adjust (Bug 1) + 4 mode Trả lại (Bug 2).
|
||||||
// Filter giữ:
|
// Filter giữ:
|
||||||
// - Workflow transition về TraLai (phaseAtChange = TraLai = 98)
|
// - Workflow transition về TraLai (phaseAtChange = TraLai = 98)
|
||||||
// - Workflow transition từ TraLai → khác (Drafter gửi lại — summary chứa "TraLai →")
|
// - Workflow transition từ TraLai → khác (Drafter gửi lại — summary "TraLai →")
|
||||||
// - Mọi thay đổi nội dung khi phaseAtChange = TraLai (sửa trong giai đoạn chờ gửi lại)
|
// - Workflow Trả lại 4 mode (summary chứa "Trả lại" — Plan AB S25 fix Bug 2)
|
||||||
|
// - Header Budget Adjust (summary chứa "ngân sách" — Plan AB S25 fix Bug 1)
|
||||||
|
// - Mọi thay đổi nội dung khi phaseAtChange = TraLai (Drafter sửa trước gửi lại)
|
||||||
// BE giữ data đầy đủ (audit trail) — chỉ filter ở UI, reversible.
|
// BE giữ data đầy đủ (audit trail) — chỉ filter ở UI, reversible.
|
||||||
const PE_PHASE_TRALAI = 98
|
const PE_PHASE_TRALAI = 98
|
||||||
const PE_ENTITY_WORKFLOW = 5
|
const PE_ENTITY_WORKFLOW = 5
|
||||||
|
const PE_ENTITY_HEADER = 1
|
||||||
const filtered = (logs.data ?? []).filter(l => {
|
const filtered = (logs.data ?? []).filter(l => {
|
||||||
if (l.entityType === PE_ENTITY_WORKFLOW) {
|
if (l.entityType === PE_ENTITY_WORKFLOW) {
|
||||||
if (l.phaseAtChange === PE_PHASE_TRALAI) return true
|
if (l.phaseAtChange === PE_PHASE_TRALAI) return true
|
||||||
if (l.summary?.includes('TraLai →')) return true
|
if (l.summary?.includes('TraLai →')) return true
|
||||||
|
if (l.summary?.includes('Trả lại')) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (l.entityType === PE_ENTITY_HEADER && l.summary?.toLowerCase().includes('ngân sách')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return l.phaseAtChange === PE_PHASE_TRALAI
|
return l.phaseAtChange === PE_PHASE_TRALAI
|
||||||
})
|
})
|
||||||
if (filtered.length === 0) return <p className="text-sm text-slate-500">Chưa có lịch sử trả lại / gửi duyệt lại.</p>
|
if (filtered.length === 0) return <p className="text-sm text-slate-500">Chưa có lịch sử trả lại / điều chỉnh ngân sách / gửi duyệt lại.</p>
|
||||||
return (
|
return (
|
||||||
<ol className="space-y-1.5 text-sm">
|
<ol className="space-y-1.5 text-sm">
|
||||||
{filtered.map(l => (
|
{filtered.map(l => (
|
||||||
|
|||||||
@ -276,23 +276,28 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
$"Cấp Approver hiện tại không bật mode '{mode}'. Liên hệ Admin Designer để config Level slot.");
|
$"Cấp Approver hiện tại không bật mode '{mode}'. Liên hệ Admin Designer để config Level slot.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode Drafter — Session 17 default (Phase=TraLai clear pointer)
|
var summary = string.Empty;
|
||||||
|
|
||||||
|
// Mode Drafter — Session 17 default (Phase=TraLai clear pointer).
|
||||||
|
// Plan AB S25 — KHÔNG return early, fallthrough vào log block dưới cuối để
|
||||||
|
// Changelog uniform 4 mode (Bug 2 fix: trước chỉ TransitionAsync log phase
|
||||||
|
// transition generic, không log return mode detail → FE History tab miss).
|
||||||
if (mode == WorkflowReturnMode.Drafter)
|
if (mode == WorkflowReturnMode.Drafter)
|
||||||
{
|
{
|
||||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||||
evaluation.CurrentWorkflowStepIndex = null;
|
evaluation.CurrentWorkflowStepIndex = null;
|
||||||
evaluation.CurrentApprovalLevelOrder = null;
|
evaluation.CurrentApprovalLevelOrder = null;
|
||||||
evaluation.SlaDeadline = null;
|
evaluation.SlaDeadline = null;
|
||||||
return "Trả về Người soạn thảo";
|
summary = "Trả về Người soạn thảo";
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// 3 mode còn lại — yêu cầu pointer hợp lệ
|
// 3 mode còn lại — yêu cầu pointer hợp lệ
|
||||||
if (evaluation.CurrentWorkflowStepIndex is not int curStepIdx
|
if (evaluation.CurrentWorkflowStepIndex is not int curStepIdx
|
||||||
|| evaluation.CurrentApprovalLevelOrder is not int curLevel)
|
|| evaluation.CurrentApprovalLevelOrder is not int curLevel)
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
$"Mode '{mode}' yêu cầu phiếu đang ChoDuyet + pointer init. " +
|
$"Mode '{mode}' yêu cầu phiếu đang ChoDuyet + pointer init. " +
|
||||||
$"State hiện tại: Step={evaluation.CurrentWorkflowStepIndex}, Level={evaluation.CurrentApprovalLevelOrder}.");
|
$"State hiện tại: Step={evaluation.CurrentWorkflowStepIndex}, Level={evaluation.CurrentApprovalLevelOrder}.");
|
||||||
var summary = string.Empty;
|
|
||||||
|
|
||||||
switch (mode)
|
switch (mode)
|
||||||
{
|
{
|
||||||
@ -374,6 +379,38 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
|
|
||||||
// 3 mode trên đều giữ Phase=ChoDuyet — reset SLA cho approver mới.
|
// 3 mode trên đều giữ Phase=ChoDuyet — reset SLA cho approver mới.
|
||||||
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan AB S25 Bug 2 fix — single Changelog.Add() cover all 4 mode uniform.
|
||||||
|
// EntityType=Workflow + Action=Update để FE History filter discriminate qua
|
||||||
|
// summary substring "Trả lại" (Workflow entity + summary != phase-transition
|
||||||
|
// pattern "Phase X → Y" — distinct từ TransitionAsync caller line 100).
|
||||||
|
// KHÔNG SaveChangesAsync call mới — TransitionAsync caller có downstream save.
|
||||||
|
var modeName = mode switch
|
||||||
|
{
|
||||||
|
WorkflowReturnMode.Drafter => "Người soạn thảo",
|
||||||
|
WorkflowReturnMode.OneLevel => "1 Cấp",
|
||||||
|
WorkflowReturnMode.OneStep => "1 Bước",
|
||||||
|
WorkflowReturnMode.Assignee => "Người chỉ định",
|
||||||
|
_ => mode.ToString(),
|
||||||
|
};
|
||||||
|
string? actorName = null;
|
||||||
|
if (actorUserId is Guid actorUid)
|
||||||
|
{
|
||||||
|
var actor = await userManager.FindByIdAsync(actorUid.ToString());
|
||||||
|
actorName = actor?.FullName ?? actor?.Email;
|
||||||
|
}
|
||||||
|
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = evaluation.Id,
|
||||||
|
EntityType = PurchaseEvaluationEntityType.Workflow,
|
||||||
|
Action = ChangelogAction.Update,
|
||||||
|
PhaseAtChange = evaluation.Phase,
|
||||||
|
UserId = actorUserId,
|
||||||
|
UserName = actorName ?? "Hệ thống",
|
||||||
|
Summary = $"Trả lại ({modeName}): {summary}",
|
||||||
|
});
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user