[CLAUDE] PurchaseEvaluation: ràng trường bắt buộc — form tạo hiện thiếu gì + checklist xanh/đỏ gửi duyệt (anh Kiệt FDC UAT R3)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m50s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m50s
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 <noreply@anthropic.com>
This commit is contained in:
@ -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 && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-slate-200 bg-amber-50/40 px-5 py-2 text-[11px]">
|
||||
<span className="font-semibold text-slate-600">Cần đủ để gửi duyệt:</span>
|
||||
{submitChecklist.map(c => (
|
||||
<span key={c.label} className={cn('inline-flex items-center gap-1', c.ok ? 'text-emerald-600' : 'font-semibold text-red-600')}>
|
||||
{c.ok ? '✓' : '✗'} {c.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 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({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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 (
|
||||
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
@ -298,17 +306,26 @@ export function PeWorkspaceCreateView({
|
||||
ở Detail tabs sau khi tạo phiếu. */}
|
||||
</div>
|
||||
|
||||
{/* Action bar */}
|
||||
<div className="flex items-center justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
|
||||
{onCancel && (
|
||||
<Button variant="ghost" onClick={onCancel} className="text-xs">Hủy</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => create.mutate()}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{create.isPending ? 'Đang tạo…' : 'Tạo phiếu'}
|
||||
</Button>
|
||||
{/* 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. */}
|
||||
<div className="flex items-center justify-between gap-3 border-t border-slate-200 bg-slate-50 px-5 py-3">
|
||||
<div className="min-w-0 text-[11px]">
|
||||
{missingCreate.length > 0 ? (
|
||||
<span className="text-red-600">⚠ Chưa đủ để tạo phiếu — còn thiếu: <strong>{missingCreate.join(' · ')}</strong></span>
|
||||
) : (
|
||||
<span className="font-medium text-emerald-600">✓ Đủ thông tin bắt buộc — có thể tạo phiếu</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{onCancel && (
|
||||
<Button variant="ghost" onClick={onCancel} className="text-xs">Hủy</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => create.mutate()}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{create.isPending ? 'Đang tạo…' : 'Tạo phiếu'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user