- {/* 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 && (
-
- {/* 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
+ )}
+