// Create view cho workspace mode "new" — sectioned layout giống PeDetailTabs // (5 section visible) nhưng trống hết. Section 1 + 2 editable (header + budget). // Section 3-5 locked với placeholder "Lưu phiếu trước". Sau save → caller // onSaved navigate sang ?id={newId} → workspace switch sang detail view (full // PeDetailTabs với inline edit Section 1 + BudgetFieldRow + Suppliers/Items). // // Pattern user 2026-05-07: "Thêm mới list ra hết trường dữ liệu giống chỉnh // sửa nhưng trống, mở rộng từng phần. Save header xong mới cho nhập chi tiết." import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Lock } from 'lucide-react' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' import { Select } from '@/components/ui/Select' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { PurchaseEvaluationTypeLabel } from '@/types/purchaseEvaluation' import { BudgetPhase, type BudgetListItem } from '@/types/budget' import type { Paged, Project } from '@/types/master' // VND format helpers (mirror PeDetailTabs.tsx — session 20) const parseVnd = (s: string): number => Number(s.replace(/[^\d]/g, '')) || 0 const formatVndInput = (n: number): string => (n > 0 ? n.toLocaleString('vi-VN') : '') // S57bis — Hạng mục công việc (WorkItem master). Reuse catalog endpoint // /catalogs/work-items (cùng query key CatalogsPage + ContractDetailsTab dùng). type WorkItemOption = { id: string code: string name: string category?: string | null isActive?: boolean } // Preset điều khoản thanh toán phổ biến — user chọn 1 trong list, hoặc "Khác" // để nhập tay. Save as plain text (không JSON như cũ — code-style không phù // hợp UI cho end-user). User 2026-05-07 chỉnh. const PAYMENT_PRESETS = [ '100% sau khi nghiệm thu', 'Tạm ứng 30% / Thanh toán 70% sau nghiệm thu', 'Tạm ứng 50% / Thanh toán 50% sau nghiệm thu', 'TGN-30 ngày (Thanh toán giao nhận 30 ngày)', 'TGN-45 ngày', 'TGN-60 ngày', 'Tiến độ theo từng đợt', 'Bảo hành 5% trong 12 tháng', ] as const const PAYMENT_CUSTOM = '__custom__' export function PeWorkspaceCreateView({ defaultType, onSaved, onCancel, }: { defaultType: number /** Callback sau khi POST thành công với (newId, type). Caller navigate. */ onSaved: (id: string, type: number) => void onCancel?: () => void }) { const qc = useQueryClient() const [form, setForm] = useState({ type: defaultType, tenGoiThau: '', projectId: '', workItemId: '', diaDiem: '', moTa: '', paymentTerms: '', budgetId: '', // Mig 17 — manual budget fallback budgetManual: false, budgetManualName: '', budgetManualAmount: 0, // Mig 23 — Pin quy trình duyệt V2 (User tự chọn lúc tạo) approvalWorkflowId: '', }) // Payment terms: select preset OR "Khác" → text input const [paymentMode, setPaymentMode] = useState('') // '' / preset / __custom__ const isPaymentCustom = paymentMode === PAYMENT_CUSTOM const projects = useQuery({ queryKey: ['all-projects'], queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items, }) // S57bis — list Hạng mục công việc (active only — filter client nếu BE trả isActive). const workItems = useQuery({ queryKey: ['catalogs', 'work-items'], queryFn: async () => (await api.get('/catalogs/work-items')).data, select: rows => rows.filter(r => r.isActive !== false), }) // Mig 23 — fetch list quy trình duyệt V2 cho User chọn (filter theo // ApplicableType khớp với defaultType: 1=DuyetNcc / 2=DuyetNccPhuongAn). // Mig 25 — chỉ hiện workflows admin đã ghim "cho user chọn" (IsUserSelectable=true). const approvalWorkflows = useQuery({ queryKey: ['approval-workflows-v2-active', defaultType], queryFn: async () => { const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean; isUserSelectable: boolean }[] }[] }>( '/approval-workflows-v2', { params: { applicableType: defaultType } }, ) const typeBucket = res.data.types.find(t => t.applicableType === defaultType) return (typeBucket?.history ?? []).filter(w => w.isUserSelectable) }, }) const eligibleBudgets = useQuery({ queryKey: ['eligible-budgets', form.projectId], queryFn: async () => { const res = await api.get>('/budgets', { params: { pageSize: 100, projectId: form.projectId, phase: BudgetPhase.DaDuyet }, }) return res.data.items }, enabled: !!form.projectId, }) const budgetPayload = form.budgetManual ? { budgetId: null, budgetManualName: form.budgetManualName || null, budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null } : { budgetId: form.budgetId || null, budgetManualName: null, budgetManualAmount: null } const create = useMutation({ mutationFn: async () => { const res = await api.post<{ id: string }>('/purchase-evaluations', { type: form.type, tenGoiThau: form.tenGoiThau, projectId: form.projectId, workItemId: form.workItemId || null, diaDiem: form.diaDiem || null, moTa: form.moTa || null, paymentTerms: form.paymentTerms || null, approvalWorkflowId: form.approvalWorkflowId || null, ...budgetPayload, }) return res.data.id }, onSuccess: id => { toast.success('Đã tạo phiếu — mở chi tiết để thêm NCC + hạng mục.') qc.invalidateQueries({ queryKey: ['pe-list'] }) onSaved(id, form.type) }, onError: e => toast.error(getErrorMessage(e)), }) const canSubmit = !!form.tenGoiThau && !!form.projectId && !!form.workItemId && !!form.approvalWorkflowId && !create.isPending return (
{/* Header bar */}

Tạo phiếu Duyệt NCC mới

Nhập Section 1 + 2 → bấm “Tạo phiếu” → sau đó NCC / Hạng mục / Báo giá / Ý kiến mở khóa.

{onCancel && ( )}
{/* Section 1 — Thông tin gói thầu (editable) */}
{approvalWorkflows.data && approvalWorkflows.data.length === 0 && (

⚠ Chưa có quy trình duyệt cho loại {PurchaseEvaluationTypeLabel[form.type]}. Vào{' '} /system/approval-workflows-v2 để tạo trước.

)}
setForm({ ...form, tenGoiThau: e.target.value })} placeholder="vd Cung cấp bê tông" />
{workItems.data && workItems.data.length === 0 && (

⚠ Chưa có hạng mục công việc nào. Vào Danh mục → Hạng mục công việc để tạo trước.

)}
setForm({ ...form, diaDiem: e.target.value })} placeholder="Lô K, KCN Lộc An..." />
setForm({ ...form, moTa: e.target.value })} placeholder="Phương án A: ..." />
{isPaymentCustom && ( setForm({ ...form, paymentTerms: e.target.value })} placeholder="Nhập điều khoản tùy chỉnh" className="mt-2" /> )}
{/* Section 2 — Chọn NCC/TP (chỉ Ngân sách editable, còn lại sẽ unlock sau create) */}
— (sau khi thêm NCC tham gia + chốt winner)} /> {/* b. Ngân sách — editable inline (Mig 17 toggle pattern) */}
b. Ngân sách
{!form.budgetManual ? ( <>

{!form.projectId ? 'Chọn dự án trước để xem ngân sách khả dụng.' : eligibleBudgets.data && eligibleBudgets.data.length === 0 ? 'Dự án này chưa có ngân sách đã duyệt — bật "Nhập tay" để nhập số tiền trực tiếp.' : 'Chỉ list ngân sách đã duyệt cùng dự án.'}

) : (
setForm({ ...form, budgetManualAmount: parseVnd(e.target.value) })} placeholder="0" className="pr-10 font-mono text-right text-sm" /> đ
)}
— (auto-tính từ báo giá NCC sau khi chọn winner)} /> } />
{/* Section 3 — NCC tham gia (locked) */}
{/* Section 4 — Hạng mục + Báo giá (locked) */}
{/* Section 5 — Ý kiến 4 phòng ban (locked + amber hint per Q5 user) */}
Ý kiến + chữ ký nhập khi duyệt phiếu — vào menu “Duyệt” để ký.
{/* Action bar */}
{onCancel && ( )}
) } // Helper components — duplicate từ PeDetailTabs để tránh circular import. function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } function FormRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label}
{value}
) } function LockedHint({ text }: { text: string }) { return (
{text}
) }