From 506cada86b3ebbd3ea762fa6430711d155202a8e Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 19 May 2026 13:04:59 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-User=20FE-Admin:=20Plan=20AF=20?= =?UTF-8?q?=E2=80=94=20userMap=20fallback=20resolve=20historical=20entries?= =?UTF-8?q?=20pre-Plan=20AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bro UAT 2026-05-19 post-Plan AE: phiếu cũ entries vẫn show "Hệ thống" thay vì user name. Plan AE chỉ forward fix — entries CŨ pre-deploy có userName="" empty, FE fallback "Hệ thống". Fix Plan AF — Option A bro chốt (FE fallback lookup, no DB write): ApprovalsTab + HistoryTab build userMap useMemo từ PeDetailBundle data có sẵn (KHÔNG cần extra fetch /api/users admin permission): - ev.drafterUserId + ev.drafterName - ev.approvals[].approverUserId + approverName - ev.approvalFlow.steps[].levels[].approvers[].userId + fullName - ev.levelOpinions[].signedByUserId + signedByFullName - ev.departmentOpinions[].userId + userName resolveUserName / resolveActorName helper: 1. Trust entry.userName nếu non-empty 2. Lookup userMap qua entry.userId 3. Fallback 'Hệ thống' nếu không match Cover gần hết users tham gia phiếu (drafter + approver + signer). Edge case: user edit phiếu nhưng KHÔNG xuất hiện trong workflow → vẫn fallback. Pattern reusable: synthetic data recovery cho audit trail từ embedded domain data sources, no extra API contract change. Mirror 2 app §3.9 identical logic. Verify: - npm build × fe-user PASS 0 TS err (9.12s) - npm build × fe-admin PASS 0 TS err (8.91s) - BE unchanged from 9ea62be Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 70 ++++++++++++++++++- fe-user/src/components/pe/PeDetailTabs.tsx | 74 ++++++++++++++++++++- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 216aed1..1a75a92 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -2066,6 +2066,39 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { queryFn: async () => (await api.get(`/purchase-evaluations/${ev.id}/changelogs`)).data, }) + // Plan AF S25 — userMap fallback cho historical entries pre-Plan AE + // (userName="" empty/null). Cover real approvals + synthetic reject rows. + const userMap = useMemo(() => { + const m = new Map() + if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName) + ev.approvals.forEach(a => { + if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName) + }) + ev.approvalFlow?.steps?.forEach(s => + s.levels?.forEach(l => + l.approvers?.forEach(ap => { + if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName) + }), + ), + ) + ev.levelOpinions?.forEach(o => { + if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName) + }) + ev.departmentOpinions?.forEach(o => { + if (o.userId && o.userName) m.set(o.userId, o.userName) + }) + return m + }, [ev]) + + const resolveActorName = (a: PeApproval): string => { + if (a.approverName && a.approverName.trim() !== '') return a.approverName + if (a.approverUserId) { + const name = userMap.get(a.approverUserId) + if (name) return name + } + return 'Hệ thống' + } + const merged = useMemo(() => { const phaseEnumMap: Record = { DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99, @@ -2126,7 +2159,7 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { {new Date(a.approvedAt).toLocaleString('vi-VN')}
- {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} + {resolveActorName(a)}{a.comment && ` · ${a.comment}`}
) @@ -2141,6 +2174,39 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) { queryKey: ['pe-changelog', ev.id], queryFn: async () => (await api.get(`/purchase-evaluations/${ev.id}/changelogs`)).data, }) + + // Plan AF S25 — userMap fallback cho historical entries pre-Plan AE + const userMap = useMemo(() => { + const m = new Map() + if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName) + ev.approvals.forEach(a => { + if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName) + }) + ev.approvalFlow?.steps?.forEach(s => + s.levels?.forEach(l => + l.approvers?.forEach(ap => { + if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName) + }), + ), + ) + ev.levelOpinions?.forEach(o => { + if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName) + }) + ev.departmentOpinions?.forEach(o => { + if (o.userId && o.userName) m.set(o.userId, o.userName) + }) + return m + }, [ev]) + + const resolveUserName = (l: PeChangelog): string => { + if (l.userName && l.userName.trim() !== '') return l.userName + if (l.userId) { + const name = userMap.get(l.userId) + if (name) return name + } + return 'Hệ thống' + } + if (logs.isLoading) return

Đang tải…

// User UAT 2026-05-08: chỉ track events Trả lại + Gửi duyệt lại. // User UAT 2026-05-19: + track Budget Adjust (Bug 1) + 4 mode Trả lại (Bug 2). @@ -2172,7 +2238,7 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) { {filtered.map(l => (
  • - {l.userName ?? 'Hệ thống'} + {resolveUserName(l)} {new Date(l.createdAt).toLocaleString('vi-VN')}
    {l.summary}
    diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index e87e3a8..df37ab4 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -2060,6 +2060,39 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { queryFn: async () => (await api.get(`/purchase-evaluations/${ev.id}/changelogs`)).data, }) + // Plan AF S25 — userMap fallback cho historical entries pre-Plan AE + // (userName="" empty/null). Cover real approvals + synthetic reject rows. + const userMap = useMemo(() => { + const m = new Map() + if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName) + ev.approvals.forEach(a => { + if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName) + }) + ev.approvalFlow?.steps?.forEach(s => + s.levels?.forEach(l => + l.approvers?.forEach(ap => { + if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName) + }), + ), + ) + ev.levelOpinions?.forEach(o => { + if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName) + }) + ev.departmentOpinions?.forEach(o => { + if (o.userId && o.userName) m.set(o.userId, o.userName) + }) + return m + }, [ev]) + + const resolveActorName = (a: PeApproval): string => { + if (a.approverName && a.approverName.trim() !== '') return a.approverName + if (a.approverUserId) { + const name = userMap.get(a.approverUserId) + if (name) return name + } + return 'Hệ thống' + } + const merged = useMemo(() => { const phaseEnumMap: Record = { DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99, @@ -2120,7 +2153,7 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) { {new Date(a.approvedAt).toLocaleString('vi-VN')}
    - {a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`} + {resolveActorName(a)}{a.comment && ` · ${a.comment}`}
  • ) @@ -2135,6 +2168,43 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) { queryKey: ['pe-changelog', ev.id], queryFn: async () => (await api.get(`/purchase-evaluations/${ev.id}/changelogs`)).data, }) + + // Plan AF S25 — userMap fallback cho historical entries pre-Plan AE deploy + // (userName="" empty hoặc null). Build map từ data có sẵn PeDetailBundle: + // drafter + approvals + approvalFlow + levelOpinions + departmentOpinions. + // KHÔNG cần extra fetch /api/users (admin permission). Cover gần hết users + // tham gia phiếu. + const userMap = useMemo(() => { + const m = new Map() + if (ev.drafterUserId && ev.drafterName) m.set(ev.drafterUserId, ev.drafterName) + ev.approvals.forEach(a => { + if (a.approverUserId && a.approverName) m.set(a.approverUserId, a.approverName) + }) + ev.approvalFlow?.steps?.forEach(s => + s.levels?.forEach(l => + l.approvers?.forEach(ap => { + if (ap.userId && ap.fullName) m.set(ap.userId, ap.fullName) + }), + ), + ) + ev.levelOpinions?.forEach(o => { + if (o.signedByUserId && o.signedByFullName) m.set(o.signedByUserId, o.signedByFullName) + }) + ev.departmentOpinions?.forEach(o => { + if (o.userId && o.userName) m.set(o.userId, o.userName) + }) + return m + }, [ev]) + + const resolveUserName = (l: PeChangelog): string => { + if (l.userName && l.userName.trim() !== '') return l.userName + if (l.userId) { + const name = userMap.get(l.userId) + if (name) return name + } + return 'Hệ thống' + } + if (logs.isLoading) return

    Đang tải…

    // User UAT 2026-05-08: chỉ track events Trả lại + Gửi duyệt lại. // User UAT 2026-05-19: + track Budget Adjust (Bug 1) + 4 mode Trả lại (Bug 2). @@ -2166,7 +2236,7 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) { {filtered.map(l => (
  • - {l.userName ?? 'Hệ thống'} + {resolveUserName(l)} {new Date(l.createdAt).toLocaleString('vi-VN')}
    {l.summary}