[CLAUDE] Workflow: fix workflow picker 2 bug (P11-A Max re-review) + SetWorkflow endpoint
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m5s

Double-check chất lượng P11-A ở Max (agents trước chạy High + truncate 3×) →
phát hiện 2 bug THẬT trong workflow-picker FE của WorkflowAppDetailPage (core
approve/reject/return ĐÚNG, chỉ sub-flow chọn quy trình hỏng):

Bug #1 (HIGH) — pinWorkflow PUT /{id} chỉ gửi {approvalWorkflowId} → UpdateDraft
  validator (Reason NotEmpty, NumDays>0...) fail → 400. Nút "Lưu quy trình" vỡ.
Bug #2 (HIGH) — fetch workflow expect flat array nhưng endpoint trả
  AwAdminOverviewDto {types:[...]} → picker rỗng/crash. FE copy nhầm pattern hỏng
  của ProposalCreatePage thay vì PE/Contract proven.

Fix:
- BE: thêm endpoint chuyên dụng PUT /{id}/workflow + Set{Module}WorkflowCommand/Handler
  cho 4 module — chỉ set ApprovalWorkflowId trên draft Nhap/TraLai (verify ApplicableType
  per module), KHÔNG validate field khác. Single-responsibility, bulletproof.
- FE: sửa fetch mirror PE/Contract (data.types.find(t=>t.applicableType===X)?.history
  .filter(isUserSelectable)) + pin gọi endpoint mới. fe-admin+fe-user SHA256 identical.
- Test: +3 SetWorkflow (happy no-status-change / wrong ApplicableType Conflict / submitted
  guard) → 141→144 PASS.

Verify: BE build 0 error · 144 test PASS · FE build ×2 · SHA256 identical.
Bonus phát hiện: ProposalCreatePage (S37) có bug #2 có sẵn (latent, chưa exercise UAT)
  → flag spawn task riêng, KHÔNG fix trong commit này.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-30 10:14:34 +07:00
parent e7b66cd52b
commit 75df04ec82
11 changed files with 302 additions and 31 deletions

View File

@ -26,7 +26,7 @@ import {
type Kind = 'leave' | 'ot' | 'travel' | 'vehicle'
type ActionKind = 'approve' | 'reject' | 'return'
interface WorkflowOption { id: string; code: string; name: string }
interface WorkflowOption { id: string; code: string; name: string; isActive: boolean; isUserSelectable: boolean }
function formatDate(iso?: string): string {
if (!iso) return '—'
@ -140,12 +140,19 @@ export function WorkflowAppDetailPage() {
const hasWorkflow = !!d?.approvalWorkflowId
// Workflow picker — chỉ fetch khi draft chưa pin workflow.
// Endpoint trả AwAdminOverviewDto { types: [{ applicableType, history: [...] }] } —
// KHÔNG phải flat array. Mirror pattern ContractCreatePage/PeWorkspace: extract bucket
// theo applicableType rồi filter isUserSelectable (admin ghim cho user pick).
const workflows = useQuery({
queryKey: ['approval-workflows-v2', { applicableType: config?.applicableType, isUserSelectable: true }],
queryFn: async () =>
(await api.get<WorkflowOption[]>('/approval-workflows-v2', {
params: { applicableType: config.applicableType, isUserSelectable: true },
})).data,
queryKey: ['approval-workflows-v2', config?.applicableType],
queryFn: async () => {
const res = await api.get<{ types: { applicableType: number; history: WorkflowOption[] }[] }>(
'/approval-workflows-v2',
{ params: { applicableType: config.applicableType } },
)
const bucket = res.data.types.find((t) => t.applicableType === config.applicableType)
return (bucket?.history ?? []).filter((w) => w.isUserSelectable)
},
enabled: !!config && isDraft && !hasWorkflow,
})
@ -156,7 +163,9 @@ export function WorkflowAppDetailPage() {
const pinWorkflow = useMutation({
mutationFn: async (workflowId: string) => {
await api.put(`${config.endpoint}/${id}`, { approvalWorkflowId: workflowId })
// Endpoint chuyên dụng /workflow — chỉ set ApprovalWorkflowId trên draft.
// KHÔNG dùng PUT /{id} (UpdateDraft) vì nó validate Reason/NumDays... → 400.
await api.put(`${config.endpoint}/${id}/workflow`, { approvalWorkflowId: workflowId })
},
onSuccess: () => {
toast.success('Đã chọn quy trình duyệt')