[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
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ƯỚCcdfd542khô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 froma734bf2Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -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,
|
||||
@ -2013,10 +2014,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 có 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 có 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">
|
||||
|
||||
Reference in New Issue
Block a user