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