[CLAUDE] PurchaseEvaluation: UAT dot 2 - an Tra lai/Tu choi khi tu duyet phieu minh soan + quick-add NCC ngay form + NCC go-tim sort A-Z + upload nhieu file 1 lan
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m25s

- PeWorkflowPanel x2: nguoi duyet == nguoi soan (drafterUserId == currentUser.id)
  -> an ca "Tra lai" + "Tu choi" (anh chot: tra cho chinh minh vo nghia, huy phieu
  = nho cap khac tu choi / xoa phieu Nhap).
- SuppliersController: POST tao NCC mo cho moi user dang nhap (anh chot - nghiep vu
  di thau phat sinh NTP moi lien tuc); PUT/DELETE van khoa Admin+CatalogManager (S57).
- PeDetailTabs AddSupplierDialog x2: Select -> SearchableSelect (go-tim bo dau,
  sort A-Z theo ma) + nut "+ NCC moi" quick-create (Ma/Ten/Loai/SDT/Email) ->
  POST /suppliers -> auto-select vao phieu.
- Upload file bao gia + bang so sanh x2: input multiple + upload tuan tu tung file
  (UAT "moi lan chi chon duoc 1 file").
- SHA256 mirror x2 app, build tsc+vite x2 PASS, BE 0 err, test 240/240 PASS local.
This commit is contained in:
pqhuy1987
2026-06-11 17:51:28 +07:00
parent faed59f4c4
commit 9c330d26c4
5 changed files with 245 additions and 66 deletions

View File

@ -35,8 +35,6 @@ export function PeWorkflowPanel({
const [target, setTarget] = useState<number | null>(null)
const [comment, setComment] = useState('')
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
// S23 t2 fix: default sang first available F1 mode (mode đang gửi duyệt) khi
// admin tick — Drafter (= trả về Người soạn) chỉ default khi không có F1 nào.
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
// Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối. Default false (admin opt-in
@ -46,8 +44,7 @@ export function PeWorkflowPanel({
const { user: currentUser } = useAuth()
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
// Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại). Null nếu
// V1 legacy hoặc pointer chưa init → fallback chỉ "Trả về Drafter".
// Mig 29 (S21 t5) — F1 options per-Level (Cấp Approver hiện tại).
const levelOptions = evaluation.currentLevelOptions
// S23 t2 fix bro UAT: khi admin tick AllowReturnToAssignee/OneLevel/OneStep
@ -69,16 +66,18 @@ export function PeWorkflowPanel({
// List approvers đã ký (cho mode Assignee dropdown pick)
const signedApprovers = (evaluation.levelOpinions ?? [])
.map(o => ({ userId: o.approverUserId, fullName: o.approverFullName ?? 'NV' }))
// Dedupe by userId (1 NV có thể ký nhiều cấp nếu workflow đặt như vậy)
.filter((v, i, arr) => arr.findIndex(x => x.userId === v.userId) === i)
// Mig 24 — V2 schema chỉ cho phép approver trong CurrentApproval.approvers
// thao tác cấp hiện tại. UAT S22+1: disable cả 3 button (Duyệt + Trả lại
// + Từ chối) khi actor không match. BE mirror EnsureCanRejectV2Async.
// thao tác cấp hiện tại. UAT S22+1 feedback: "không quyền thao tác = ko quyền
// mọi hành động" — disable cả 3 button (Duyệt + Trả lại + Từ chối) khi actor
// không match currentLevel.ApproverUserId. BE mirror guard trong
// EnsureCanRejectV2Async (defense-in-depth — UI disable + BE reject).
// Admin bypass.
const v2Approvers = evaluation.currentApproval?.approvers ?? []
const actorInV2Level = isAdmin
|| (currentUser?.id && v2Approvers.some(a => a.userId === currentUser.id))
// V2 active = phiếu pin V2 + Phase=ChoDuyet + có currentApproval
const isV2Pending = !!evaluation.currentApproval
const blockedByV2Level = isV2Pending && !actorInV2Level
@ -221,13 +220,15 @@ export function PeWorkflowPanel({
</ol>
)}
{/* Phiếu V1 legacy không có flow → fallback hiển thị phase summary đơn giản */}
{!flow && (
<div className="rounded border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] text-slate-600">
Phiếu này dùng quy trình workflow chi tiết không khả dụng.
</div>
)}
{/* Mig 24 — V2 banner Bước/Cấp + danh sách NV duyệt */}
{/* Mig 24 — V2 banner: hiển thị Bước/Cấp hiện tại + danh sách NV được duyệt.
Nếu actor không có trong list → banner amber + nút Duyệt sẽ disable. */}
{isV2Pending && evaluation.currentApproval && !readOnly && (
<div className={cn(
'rounded border px-3 py-2 text-[11px]',
@ -263,6 +264,11 @@ export function PeWorkflowPanel({
&& evaluation.phase !== PurchaseEvaluationPhase.TraLai
const isCancel = p === PurchaseEvaluationPhase.TuChoi
const isForwardApprove = !isSendBack && !isCancel
// S59 anh chốt (UAT: "nhân viên tạo phiếu thì trả lại và từ chối cho ai?"):
// người duyệt CHÍNH LÀ người soạn phiếu → ẩn cả Trả lại + Từ chối
// (trả cho chính mình vô nghĩa — đang sửa inline được; hủy phiếu sai
// = nhờ cấp khác Từ chối, phiếu Nháp có nút Xóa riêng).
if ((isSendBack || isCancel) && evaluation.drafterUserId === currentUser?.id) return null
// Mig 24 + UAT S22+1 — disable cả 3 button khi actor không match
// currentLevel.ApproverUserId. "Không quyền = ko quyền mọi hành động."
const isDisabled = blockedByV2Level
@ -301,9 +307,9 @@ export function PeWorkflowPanel({
{target !== null && (() => {
const isCancel = target === PurchaseEvaluationPhase.TuChoi
// isSendBack sync với button label L205-207 + payload isReject L64-68
// (gotcha #45). Include cả DangSoanThao (legacy Mig 16) lẫn TraLai
// (Session 17 spec) — cả 2 là Trả lại Drafter sửa.
// isSendBack sync với button label + payload isReject (gotcha #45).
// Include cả DangSoanThao (legacy Mig 16) lẫn TraLai (Session 17 spec)
// — cả 2 là Trả lại Drafter sửa.
const isSendBack = (target === PurchaseEvaluationPhase.DangSoanThao
|| target === PurchaseEvaluationPhase.TraLai)
&& evaluation.phase !== PurchaseEvaluationPhase.DangSoanThao