[CLAUDE] FE-User FE-Admin: Plan AF — userMap fallback resolve historical entries pre-Plan AE
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m23s

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) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-19 13:04:59 +07:00
parent 9ea62be6a7
commit 506cada86b
2 changed files with 140 additions and 4 deletions

View File

@ -2066,6 +2066,39 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
queryFn: async () => (await api.get<PeChangelog[]>(`/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<string, string>()
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<string, number> = {
DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99,
@ -2126,7 +2159,7 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
<span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
</div>
<div className="mt-1 text-xs text-slate-500">
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
{resolveActorName(a)}{a.comment && ` · ${a.comment}`}
</div>
</li>
)
@ -2141,6 +2174,39 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) {
queryKey: ['pe-changelog', ev.id],
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
})
// Plan AF S25 — userMap fallback cho historical entries pre-Plan AE
const userMap = useMemo(() => {
const m = new Map<string, string>()
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 <p className="text-sm text-slate-500">Đang tải</p>
// 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 => (
<li key={l.id} className="border-l-2 border-slate-200 pl-3 py-1">
<div className="flex items-center justify-between text-xs text-slate-500">
<span>{l.userName ?? 'Hệ thống'}</span>
<span>{resolveUserName(l)}</span>
<span>{new Date(l.createdAt).toLocaleString('vi-VN')}</span>
</div>
<div className="text-slate-800">{l.summary}</div>

View File

@ -2060,6 +2060,39 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
queryFn: async () => (await api.get<PeChangelog[]>(`/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<string, string>()
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<string, number> = {
DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99,
@ -2120,7 +2153,7 @@ function ApprovalsTab({ ev }: { ev: PeDetailBundle }) {
<span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
</div>
<div className="mt-1 text-xs text-slate-500">
{a.approverName ?? 'Hệ thống'}{a.comment && ` · ${a.comment}`}
{resolveActorName(a)}{a.comment && ` · ${a.comment}`}
</div>
</li>
)
@ -2135,6 +2168,43 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) {
queryKey: ['pe-changelog', ev.id],
queryFn: async () => (await api.get<PeChangelog[]>(`/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<string, string>()
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 <p className="text-sm text-slate-500">Đang tải</p>
// 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 => (
<li key={l.id} className="border-l-2 border-slate-200 pl-3 py-1">
<div className="flex items-center justify-between text-xs text-slate-500">
<span>{l.userName ?? 'Hệ thống'}</span>
<span>{resolveUserName(l)}</span>
<span>{new Date(l.createdAt).toLocaleString('vi-VN')}</span>
</div>
<div className="text-slate-800">{l.summary}</div>