[CLAUDE] FE-User FE-Admin: Plan AC2 — FE merge view recover historical Reject events PE cũ
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m22s

Bro UAT 2026-05-19 phản hồi sau Plan AC deploy: phiếu cũ PE/2026/A/032 vẫn KHÔNG
show events Trả lại pre-deploy (Bro test trả lại Phan Văn Chương → Trà từ TRƯỚC
cdfd542 không có trong Lịch sử duyệt).

Root cause: Plan AC chỉ add Approval row cho events POST-deploy. Events
pre-deploy chỉ có Changelog (LogTransitionAsync) — Approval table miss.

Fix Plan AC2 — FE merge view (Option 2A bro chọn):

ApprovalsTab fetch BOTH approvals + changelogs (cùng endpoint HistoryTab dùng):
- Reconstruct synthetic PeApproval rows từ Changelog Workflow+Reject events:
  - Filter: entityType=Workflow(5) + summary "→ TraLai"/"→ TuChoi" OR
    contextNote chứa "Trả về"/"không lùi được" (3 mode OneLevel/OneStep/Assignee
    giữ ChoDuyet → distinguish qua ContextNote keywords)
  - Parse fromPhase/toPhase từ summary regex "Chuyển phase X → Y"
  - id prefix "syn-" để distinct vs real Approval rows
- Dedupe synthetic vs real Reject Approval (post-Plan AC) qua
  approverUserId + timestamp 5s bucket key
- Merge approvals + dedupedSynthetic → sort by approvedAt → render

Reversible: KHÔNG touch DB, KHÔNG migration. FE-only fix recover history
cho mọi PE cũ trước deploy.

Mirror 2 app §3.9 identical logic.

Verify:
- npm build × fe-user + fe-admin PASS 0 TS err
- BE/test unchanged from a734bf2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-19 11:00:05 +07:00
parent a734bf2b8b
commit 25837b6220
2 changed files with 110 additions and 6 deletions

View File

@ -2,7 +2,7 @@
// NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình.
// Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel
// → PeApprovalsSection + PeHistorySection).
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
@ -30,6 +30,7 @@ import {
PurchaseEvaluationTypeLabel,
getPeDisplayStatus,
isEditablePhase,
type PeApproval,
type PeAttachment,
type PeChangelog,
type PeDepartmentOpinion,
@ -2007,10 +2008,61 @@ function decisionBadge(decision: number, toPhase: number): { label: string; cls:
}
function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
if (ev.approvals.length === 0) return <p className="text-sm text-slate-500">Chưa bước duyệt nào.</p>
// Plan AC2 S25 — FE merge view: fetch changelogs + reconstruct synthetic
// Reject rows từ pre-Plan AC historical data (PE cũ deploy trước 2026-05-19
// KHÔNG có Approval row cho Reject vì BE cũ chỉ log Changelog). Merge approvals
// + synthetic + dedupe timestamp 5s bucket cùng approverUserId.
const changelogs = useQuery({
queryKey: ['pe-changelog', ev.id],
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
})
const merged = useMemo(() => {
const phaseEnumMap: Record<string, number> = {
DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99,
}
const PE_ENTITY_WORKFLOW = 5
const syntheticRejects: PeApproval[] = (changelogs.data ?? [])
.filter(c => {
if (c.entityType !== PE_ENTITY_WORKFLOW) return false
if (c.summary?.includes('→ TraLai') || c.summary?.includes('→ TuChoi')) return true
// 3 mode (OneLevel/OneStep/Assignee) giữ ChoDuyet → distinguish qua ContextNote keywords
const note = c.contextNote ?? ''
return note.includes('Trả về') || note.includes('không lùi được')
})
.map<PeApproval>(c => {
const m = c.summary?.match(/Chuyển phase (\w+) → (\w+)/)
const fromPhase = m ? (phaseEnumMap[m[1]] ?? 10) : 10
const toPhase = m ? (phaseEnumMap[m[2]] ?? 10) : 10
return {
id: `syn-${c.id}`,
fromPhase,
toPhase,
approverUserId: c.userId ?? null,
approverName: c.userName ?? null,
decision: 2,
comment: c.contextNote ?? c.summary ?? null,
approvedAt: c.createdAt,
}
})
const realRejectKeys = new Set(
ev.approvals
.filter(a => a.decision === 2)
.map(a => `${a.approverUserId ?? ''}-${Math.floor(new Date(a.approvedAt).getTime() / 5000)}`),
)
const dedupedSynthetic = syntheticRejects.filter(s =>
!realRejectKeys.has(`${s.approverUserId ?? ''}-${Math.floor(new Date(s.approvedAt).getTime() / 5000)}`),
)
return [...ev.approvals, ...dedupedSynthetic]
.sort((a, b) => new Date(a.approvedAt).getTime() - new Date(b.approvedAt).getTime())
}, [ev.approvals, changelogs.data])
if (merged.length === 0) return <p className="text-sm text-slate-500">Chưa bước duyệt nào.</p>
return (
<ol className="space-y-2">
{ev.approvals.map(a => {
{merged.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">