[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
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:
@ -2066,6 +2066,39 @@ function ApprovalsTab({ 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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 merged = useMemo(() => {
|
||||||
const phaseEnumMap: Record<string, number> = {
|
const phaseEnumMap: Record<string, number> = {
|
||||||
DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99,
|
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>
|
<span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-slate-500">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
@ -2141,6 +2174,39 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
queryKey: ['pe-changelog', ev.id],
|
queryKey: ['pe-changelog', ev.id],
|
||||||
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
|
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>
|
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-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).
|
// 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 => (
|
{filtered.map(l => (
|
||||||
<li key={l.id} className="border-l-2 border-slate-200 pl-3 py-1">
|
<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">
|
<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>
|
<span>{new Date(l.createdAt).toLocaleString('vi-VN')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-800">{l.summary}</div>
|
<div className="text-slate-800">{l.summary}</div>
|
||||||
|
|||||||
@ -2060,6 +2060,39 @@ function ApprovalsTab({ 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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 merged = useMemo(() => {
|
||||||
const phaseEnumMap: Record<string, number> = {
|
const phaseEnumMap: Record<string, number> = {
|
||||||
DangSoanThao: 1, ChoDuyet: 10, DaDuyet: 20, TraLai: 98, TuChoi: 99,
|
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>
|
<span className="text-xs text-slate-500 shrink-0">{new Date(a.approvedAt).toLocaleString('vi-VN')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-slate-500">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
@ -2135,6 +2168,43 @@ function HistoryTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
queryKey: ['pe-changelog', ev.id],
|
queryKey: ['pe-changelog', ev.id],
|
||||||
queryFn: async () => (await api.get<PeChangelog[]>(`/purchase-evaluations/${ev.id}/changelogs`)).data,
|
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>
|
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-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).
|
// 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 => (
|
{filtered.map(l => (
|
||||||
<li key={l.id} className="border-l-2 border-slate-200 pl-3 py-1">
|
<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">
|
<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>
|
<span>{new Date(l.createdAt).toLocaleString('vi-VN')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-800">{l.summary}</div>
|
<div className="text-slate-800">{l.summary}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user