[CLAUDE] PurchaseEvaluation: User chọn quy trình duyệt V2 lúc tạo phiếu (Mig 23)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s

User feedback: thay field "Loại quy trình (theo menu — khóa)" disabled
→ Select dropdown cho User pick quy trình ApprovalWorkflowsV2 (Mig 22)
ngay từ workspace tạo mới. Hiển thị "Mã + Tên + Version".

BE Domain:
- PurchaseEvaluation +ApprovalWorkflowId Guid? (nullable, FK Restrict)
- EF Configuration: Index + FK Restrict to ApprovalWorkflows
- Migration 23 `AddApprovalWorkflowIdToPurchaseEvaluation` (1 ALTER +
  1 IX + 1 FK), applied cả _Design + _Dev LocalDB
- Field WorkflowDefinitionId (Mig 21 legacy) giữ song song để Service
  PE chạy logic cũ tới khi Session sau wire qua schema mới

BE Application:
- CreatePurchaseEvaluationCommand +ApprovalWorkflowId? Guid? optional
  param (default null)
- Validate: nếu set, phải tồn tại + ApplicableType khớp PE.Type
  (DuyetNcc=1 → ApprovalWorkflowApplicableType.DuyetNcc, etc)
- Handler set entity.ApprovalWorkflowId từ request
- UpdatePurchaseEvaluationDraftCommand mirror — cho User đổi quy trình
  khi sửa Nháp/Trả lại (validate same)
- PurchaseEvaluationDetailBundleDto +ApprovalWorkflowId/Code/Name/Version
- GetPurchaseEvaluationByIdQuery handler load workflow info join
- Update Phase guard: cho sửa cả DangSoanThao + TraLai (Trả lại =
  editable per Session 17 spec)

FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: PeDetail +approvalWorkflowId/Code/Name/Version
- PeWorkspaceCreateView.tsx:
  - Replace field disabled "Loại quy trình" → Select bắt buộc
  - useQuery `/api/approval-workflows-v2?applicableType=N` filter theo
    defaultType (1=DuyetNcc / 2=DuyetNccPhuongAn)
  - Display option: "QT-DN-V2-001 v01 — Quy trình Duyệt NCC (đang áp dụng)"
  - List cả version active + archived (UAT cần test compare)
  - Empty state hint amber "Chưa có quy trình, vào /system/approval-workflows-v2"
  - canSubmit require approvalWorkflowId set
  - POST payload include approvalWorkflowId

Verify: dotnet build OK · 81 test pass · npm build × 2 OK · Mig 23 applied
cả 2 LocalDB.

Logic Service PE chưa wire qua ApprovalWorkflowId — vẫn pin
WorkflowDefinitionId Mig 21 legacy chạy. Session sau wire Service iterate
ApprovalWorkflowSteps + match approver theo schema V2 + drop legacy.
This commit is contained in:
pqhuy1987
2026-05-08 14:34:54 +07:00
parent d642fd361e
commit 0a40c65421
11 changed files with 4044 additions and 23 deletions

View File

@ -59,6 +59,8 @@ export function PeWorkspaceCreateView({
budgetManual: false,
budgetManualName: '',
budgetManualAmount: 0,
// Mig 23 — Pin quy trình duyệt V2 (User tự chọn lúc tạo)
approvalWorkflowId: '',
})
// Payment terms: select preset OR "Khác" → text input
const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__
@ -69,6 +71,22 @@ export function PeWorkspaceCreateView({
queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items,
})
// Mig 23 — fetch list quy trình duyệt V2 cho User chọn (filter theo
// ApplicableType khớp với defaultType: 1=DuyetNcc / 2=DuyetNccPhuongAn).
const approvalWorkflows = useQuery({
queryKey: ['approval-workflows-v2-active', defaultType],
queryFn: async () => {
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>(
'/approval-workflows-v2',
{ params: { applicableType: defaultType } },
)
// Trả về tất cả version (active + archived) cho User pick — UAT cần
// flexibility chọn cả version cũ test compare.
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
return typeBucket?.history ?? []
},
})
const eligibleBudgets = useQuery({
queryKey: ['eligible-budgets', form.projectId],
queryFn: async () => {
@ -93,6 +111,7 @@ export function PeWorkspaceCreateView({
diaDiem: form.diaDiem || null,
moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null,
approvalWorkflowId: form.approvalWorkflowId || null,
...budgetPayload,
})
return res.data.id
@ -105,7 +124,7 @@ export function PeWorkspaceCreateView({
onError: e => toast.error(getErrorMessage(e)),
})
const canSubmit = !!form.tenGoiThau && !!form.projectId && !create.isPending
const canSubmit = !!form.tenGoiThau && !!form.projectId && !!form.approvalWorkflowId && !create.isPending
return (
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
@ -126,15 +145,31 @@ export function PeWorkspaceCreateView({
{/* Section 1 — Thông tin gói thầu (editable) */}
<Section title="1. Thông tin gói thầu">
<div className="grid gap-3 md:grid-cols-2">
<div>
<Label className="text-[11px]">Loại quy trình (theo menu khóa)</Label>
<Input
value={PurchaseEvaluationTypeLabel[form.type]}
disabled
className="bg-slate-100 font-medium"
/>
<div className="md:col-span-2">
<Label className="text-[11px]">
Quy trình duyệt * <span className="text-[10px] font-normal text-slate-400">(theo {PurchaseEvaluationTypeLabel[form.type]})</span>
</Label>
<Select
value={form.approvalWorkflowId}
onChange={e => setForm({ ...form, approvalWorkflowId: e.target.value })}
required
>
<option value=""> Chọn quy trình duyệt </option>
{approvalWorkflows.data?.map(w => (
<option key={w.id} value={w.id}>
{w.code} v{String(w.version).padStart(2, '0')} {w.name}
{w.isActive ? ' (đang áp dụng)' : ''}
</option>
))}
</Select>
{approvalWorkflows.data && approvalWorkflows.data.length === 0 && (
<p className="mt-1 text-[11px] text-amber-700">
Chưa quy trình duyệt cho loại {PurchaseEvaluationTypeLabel[form.type]}. Vào{' '}
<span className="font-mono">/system/approval-workflows-v2</span> đ tạo trước.
</p>
)}
</div>
<div className="md:col-span-1">
<div className="md:col-span-2">
<Label className="text-[11px]">a. Tên gói thầu *</Label>
<Input
value={form.tenGoiThau}