From e3e40ae346648245d01c484903cfb1e02cfb8eab Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 23 Jun 2026 14:10:38 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20PurchaseEvaluation:=20r=C3=A0ng=20tr?= =?UTF-8?q?=C6=B0=E1=BB=9Dng=20b=E1=BA=AFt=20bu=E1=BB=99c=20=E2=80=94=20fo?= =?UTF-8?q?rm=20t=E1=BA=A1o=20hi=E1=BB=87n=20thi=E1=BA=BFu=20g=C3=AC=20+?= =?UTF-8?q?=20checklist=20xanh/=C4=91=E1=BB=8F=20g=E1=BB=ADi=20duy?= =?UTF-8?q?=E1=BB=87t=20(anh=20Ki=E1=BB=87t=20FDC=20UAT=20R3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anh chốt "không chọn quy trình thì không cho tạo phiếu, ràng mấy cái rủi ro lại hết": - Form TẠO: nút "Tạo phiếu" disabled khi thiếu (Quy trình duyệt / Hạng mục / Dự án) + hint ĐỎ liệt kê rõ còn thiếu gì (✓ xanh khi đủ). Defense-in-depth với BE validator NotEmpty (S83). - Detail (D4): checklist xanh ✓ "đã khai" / đỏ ✗ "chưa khai" cho 4 trường bắt buộc gửi duyệt (NCC winner · giá>0 · ngân sách · bảng so sánh) hiện phía trên nút "Lưu & Gửi Duyệt" (mirror missingForApproval predicate, khớp annotation anh Kiệt "xanh=đã khai, đỏ=chưa khai"). FE-only 2 component × 2 app SHA-identical. Both build PASS. Co-Authored-By: Claude Opus 4.8 --- fe-admin/src/components/pe/PeDetailTabs.tsx | 28 ++++++++++++- .../components/pe/PeWorkspaceCreateView.tsx | 41 +++++++++++++------ fe-user/src/components/pe/PeDetailTabs.tsx | 28 ++++++++++++- .../components/pe/PeWorkspaceCreateView.tsx | 41 +++++++++++++------ 4 files changed, 110 insertions(+), 28 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 1437454..49c6565 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -224,6 +224,20 @@ export function PeDetailTabs({ return missing }, [evaluation]) + // [D4/R3 anh Kiệt — "xanh=đã khai, đỏ=chưa khai => trường bắt buộc khai"] Checklist trực-quan + // các trường BẮT BUỘC để gửi duyệt (mirror missingForApproval predicate). Hiện phía trên nút. + const submitChecklist = useMemo(() => { + const supplierOk = evaluation.selectedSupplierId != null + const gia = supplierOk ? computeGiaChaoThau(evaluation) : null + const effBudget = evaluation.budgetPeriodAmount ?? evaluation.budgetSummary?.proInitialAmount ?? null + return [ + { label: 'Đơn vị NCC/TP được chọn', ok: supplierOk }, + { label: 'Giá chào thầu của đơn vị > 0', ok: gia != null && gia > 0 }, + { label: 'Ngân sách kỳ này', ok: effBudget != null && effBudget > 0 }, + { label: 'Bảng so sánh đính kèm', ok: !!evaluation.attachments?.some(a => a.purchaseEvaluationSupplierId === null && a.purpose !== PeAttachmentPurpose.ApprovalAttachment) }, + ] + }, [evaluation]) + const canSubmitForApproval = mode === 'workspace' && canEditPhase && !readOnly @@ -411,9 +425,18 @@ export function PeDetailTabs({ - Lưu & Gửi Duyệt → (POST /transitions → next phase) — bên phải brand User 2026-05-07. */} {mode === 'workspace' && canEditPhase && !readOnly && ( -
+ <> +
+ Cần đủ để gửi duyệt: + {submitChecklist.map(c => ( + + {c.ok ? '✓' : '✗'} {c.label} + + ))} +
+
- {/* Xóa phiếu — CHỈ DangSoanThao (bản nháp). TraLai không cho xóa + {/* Xóa phiếu — CHỉ DangSoanThao (bản nháp). TraLai không cho xóa (đã có lịch sử workflow). Soft-delete qua DELETE /pe/:id endpoint (AuditableEntity IsDeleted=true, không xóa hoàn toàn DB). */} {evaluation.phase === PurchaseEvaluationPhase.DangSoanThao && ( @@ -461,6 +484,7 @@ export function PeDetailTabs({
+ )}
) diff --git a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx index f42518e..48e3d0f 100644 --- a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx +++ b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx @@ -139,7 +139,15 @@ export function PeWorkspaceCreateView({ onError: e => toast.error(getErrorMessage(e)), }) - const canSubmit = !!form.tenGoiThau && !!form.projectId && !!form.workItemId && !!form.approvalWorkflowId && !create.isPending + // [D4/R3 anh Kiệt — "ràng mấy cái rủi ro lại hết"] Trường BẮT BUỘC để tạo phiếu — thiếu cái + // nào thì liệt kê cho user thấy (nút "Tạo phiếu" disabled + hint đỏ). KHÔNG chọn Quy trình + // duyệt → KHÔNG tạo được phiếu (anh chốt; defense-in-depth với BE validator NotEmpty S83). + const missingCreate = [ + !form.approvalWorkflowId && 'Quy trình duyệt', + !form.workItemId && 'Hạng mục công việc (tên gói thầu)', + !form.projectId && 'Dự án', + ].filter(Boolean) as string[] + const canSubmit = missingCreate.length === 0 && !!form.tenGoiThau && !create.isPending return (
@@ -298,17 +306,26 @@ export function PeWorkspaceCreateView({ ở Detail tabs sau khi tạo phiếu. */}
- {/* Action bar */} -
- {onCancel && ( - - )} - + {/* Action bar — [D4/R3 anh Kiệt] hint trái hiện RÕ còn thiếu gì (ràng rủi ro), nút phải. */} +
+
+ {missingCreate.length > 0 ? ( + ⚠ Chưa đủ để tạo phiếu — còn thiếu: {missingCreate.join(' · ')} + ) : ( + ✓ Đủ thông tin bắt buộc — có thể tạo phiếu + )} +
+
+ {onCancel && ( + + )} + +
) diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 1437454..49c6565 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -224,6 +224,20 @@ export function PeDetailTabs({ return missing }, [evaluation]) + // [D4/R3 anh Kiệt — "xanh=đã khai, đỏ=chưa khai => trường bắt buộc khai"] Checklist trực-quan + // các trường BẮT BUỘC để gửi duyệt (mirror missingForApproval predicate). Hiện phía trên nút. + const submitChecklist = useMemo(() => { + const supplierOk = evaluation.selectedSupplierId != null + const gia = supplierOk ? computeGiaChaoThau(evaluation) : null + const effBudget = evaluation.budgetPeriodAmount ?? evaluation.budgetSummary?.proInitialAmount ?? null + return [ + { label: 'Đơn vị NCC/TP được chọn', ok: supplierOk }, + { label: 'Giá chào thầu của đơn vị > 0', ok: gia != null && gia > 0 }, + { label: 'Ngân sách kỳ này', ok: effBudget != null && effBudget > 0 }, + { label: 'Bảng so sánh đính kèm', ok: !!evaluation.attachments?.some(a => a.purchaseEvaluationSupplierId === null && a.purpose !== PeAttachmentPurpose.ApprovalAttachment) }, + ] + }, [evaluation]) + const canSubmitForApproval = mode === 'workspace' && canEditPhase && !readOnly @@ -411,9 +425,18 @@ export function PeDetailTabs({ - Lưu & Gửi Duyệt → (POST /transitions → next phase) — bên phải brand User 2026-05-07. */} {mode === 'workspace' && canEditPhase && !readOnly && ( -
+ <> +
+ Cần đủ để gửi duyệt: + {submitChecklist.map(c => ( + + {c.ok ? '✓' : '✗'} {c.label} + + ))} +
+
- {/* Xóa phiếu — CHỈ DangSoanThao (bản nháp). TraLai không cho xóa + {/* Xóa phiếu — CHỉ DangSoanThao (bản nháp). TraLai không cho xóa (đã có lịch sử workflow). Soft-delete qua DELETE /pe/:id endpoint (AuditableEntity IsDeleted=true, không xóa hoàn toàn DB). */} {evaluation.phase === PurchaseEvaluationPhase.DangSoanThao && ( @@ -461,6 +484,7 @@ export function PeDetailTabs({
+ )}
) diff --git a/fe-user/src/components/pe/PeWorkspaceCreateView.tsx b/fe-user/src/components/pe/PeWorkspaceCreateView.tsx index f42518e..48e3d0f 100644 --- a/fe-user/src/components/pe/PeWorkspaceCreateView.tsx +++ b/fe-user/src/components/pe/PeWorkspaceCreateView.tsx @@ -139,7 +139,15 @@ export function PeWorkspaceCreateView({ onError: e => toast.error(getErrorMessage(e)), }) - const canSubmit = !!form.tenGoiThau && !!form.projectId && !!form.workItemId && !!form.approvalWorkflowId && !create.isPending + // [D4/R3 anh Kiệt — "ràng mấy cái rủi ro lại hết"] Trường BẮT BUỘC để tạo phiếu — thiếu cái + // nào thì liệt kê cho user thấy (nút "Tạo phiếu" disabled + hint đỏ). KHÔNG chọn Quy trình + // duyệt → KHÔNG tạo được phiếu (anh chốt; defense-in-depth với BE validator NotEmpty S83). + const missingCreate = [ + !form.approvalWorkflowId && 'Quy trình duyệt', + !form.workItemId && 'Hạng mục công việc (tên gói thầu)', + !form.projectId && 'Dự án', + ].filter(Boolean) as string[] + const canSubmit = missingCreate.length === 0 && !!form.tenGoiThau && !create.isPending return (
@@ -298,17 +306,26 @@ export function PeWorkspaceCreateView({ ở Detail tabs sau khi tạo phiếu. */}
- {/* Action bar */} -
- {onCancel && ( - - )} - + {/* Action bar — [D4/R3 anh Kiệt] hint trái hiện RÕ còn thiếu gì (ràng rủi ro), nút phải. */} +
+
+ {missingCreate.length > 0 ? ( + ⚠ Chưa đủ để tạo phiếu — còn thiếu: {missingCreate.join(' · ')} + ) : ( + ✓ Đủ thông tin bắt buộc — có thể tạo phiếu + )} +
+
+ {onCancel && ( + + )} + +
)