// 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 { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' import { SearchableSelect } from '@/components/ui/SearchableSelect' import { Select } from '@/components/ui/Select' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { PurchaseEvaluationTypeLabel } from '@/types/purchaseEvaluation' import type { 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 } // S59 UAT vòng 5 (anh chốt "điều khoản thanh toán bỏ nốt ra luôn tất cả các form"): // field Điều khoản TT phiếu-level GỠ khỏi mọi form (Create/HeaderForm/inline-edit). // PAYMENT_PRESETS + PAYMENT_CUSTOM drop. Cột "Điều khoản TT" per-NCC trong bảng // so sánh GIỮ (data của từng NCC, khác field). Phiếu cũ đã nhập → display read-only. 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: '', // anh Kiệt FDC — link thư mục hồ sơ trên NAS (1 cột HoSoLink, JSON hoSoLink). hoSoLink: '', // [S61 Mig 50] "Ngân sách - kỳ này" — thay budgetId/budgetManual* (module // Budget cũ xóa hẳn; bảng Tổng hợp ngân sách gói thầu ở PeDetailTabs). budgetPeriodAmount: 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 projects = useQuery({ queryKey: ['all-projects'], queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items, }) // S59 — track giá trị Địa điểm auto-fill gần nhất (từ Project.Location) để biết // user đã gõ tay chưa: diaDiem === lastAutoLoc ⟹ chưa đụng → đổi dự án ghi đè được. const lastAutoLoc = useRef('') // S57bis — list Hạng mục công việc (active only — filter client nếu BE trả isActive). // S59 — sort numeric-aware client: mã PMH không pad số (MAT-1..16, MEP-SUB-1…) // → BE OrderBy(Code) string xếp "MAT-10" trước "MAT-2"; re-sort {numeric:true}. const workItems = useQuery({ queryKey: ['catalogs', 'work-items'], queryFn: async () => (await api.get('/catalogs/work-items')).data, select: rows => rows .filter(r => r.isActive !== false) .sort((a, b) => (a.category ?? '').localeCompare(b.category ?? '', 'vi') || a.code.localeCompare(b.code, 'vi', { numeric: true })), }) // Mig 23 — fetch list quy trình duyệt V2 (filter ApplicableType khớp defaultType). // 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) }, }) // [D5 anh Kiệt FDC] Auto-chọn quy trình khi danh sách chỉ có 1 lựa chọn → hết nhầm // "chưa chọn quy trình" (chỉ set khi user chưa tự chọn, giữ lựa chọn thủ công nếu có). useEffect(() => { const list = approvalWorkflows.data if (list && list.length === 1 && !form.approvalWorkflowId) { setForm(f => ({ ...f, approvalWorkflowId: list[0].id })) } }, [approvalWorkflows.data, form.approvalWorkflowId]) const budgetPayload = { budgetPeriodAmount: form.budgetPeriodAmount > 0 ? form.budgetPeriodAmount : 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: null, // S59 vòng 5: field gỡ khỏi form hoSoLink: form.hoSoLink || 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)), }) // [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 (
{/* 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]}. Liên hệ admin tạo trước.

)}
{/* [S58] anh Kiệt (FDC) chốt 06-11: "Hạng mục công việc CHÍNH LÀ tên gói thầu" → gộp field c (S57bis) vào a: chọn từ danh mục Hạng mục thay nhập tay; chọn 1 phát set cả workItemId + tenGoiThau (= tên hạng mục). Phiếu vẫn lưu cả 2 field BE — không đổi contract. */} {/* S59 UAT "nên có lọc để tự đánh chữ" → SearchableSelect gõ-lọc bỏ dấu. */} ({ value: w.id, label: `${w.category ? `[${w.category}] ` : ''}${w.code} — ${w.name}`, }))} value={form.workItemId} onChange={id => { const w = workItems.data?.find(x => x.id === id) setForm({ ...form, workItemId: id, tenGoiThau: id ? (w?.name ?? '') : '' }) }} placeholder="— Chọn hạng mục công việc (gõ để lọc) —" /> {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.

)}
{/* S59 UAT "nên có tự gõ chữ" + "địa chỉ nên tự auto": chọn dự án tự điền Địa điểm từ Project.Location (S55) — chỉ ghi đè khi user CHƯA gõ tay (rỗng hoặc vẫn là giá trị auto của dự án trước, track qua lastAutoLoc). */} ({ value: p.id, label: `${p.code} — ${p.name}` }))} value={form.projectId} onChange={id => { const p = projects.data?.find(x => x.id === id) const loc = p?.location ?? '' setForm(f => { const untouched = !f.diaDiem || f.diaDiem === lastAutoLoc.current return { ...f, projectId: id, diaDiem: untouched ? loc : f.diaDiem } }) lastAutoLoc.current = loc }} placeholder="— Chọn dự án (gõ để lọc) —" />
setForm({ ...form, diaDiem: e.target.value })} placeholder="Lô K, KCN Lộc An..." /> {/* [S157 UAT anh Kiệt 07-28 "lấy địa chỉ lên luôn"] auto-fill S59 vẫn chạy — ô trống là do dự án CHƯA có Location trong Danh mục (đo prod: 57/62 thiếu). Nói thẳng lý do + chỗ bổ sung, thay vì im lặng như hệ thống hỏng. */} {!!form.projectId && !form.diaDiem && !projects.data?.find(p => p.id === form.projectId)?.location && (

⚠ Dự án này chưa có Địa điểm trong Danh mục Dự án — nhập tay ở đây, hoặc bổ sung vào Danh mục (Dự án → Địa điểm) để các phiếu sau tự điền.

)}
setForm({ ...form, moTa: e.target.value })} placeholder="Phương án A: ..." />
{/* Section 2 — Chọn NCC/TP (chỉ Ngân sách editable, còn lại sẽ unlock sau create) */}
{/* [S61 Mig 50] b. Ngân sách kỳ này — ô đơn thay picker Budget cũ + toggle nhập tay (module Budget xóa hẳn). Số phân bổ cho RIÊNG phiếu này (row 3 bảng "Tổng hợp ngân sách trình ký"). */}
b. Ngân sách kỳ này
setForm({ ...form, budgetPeriodAmount: parseVnd(e.target.value) })} placeholder="0" className="pr-10 font-mono text-right text-sm" /> đ

Bắt buộc trước khi gửi duyệt. Ngân sách full gói thầu xem ở bảng “Tổng hợp ngân sách trình ký” sau khi tạo phiếu.

{/* [C1 anh Kiệt FDC] "c. Giá chào thầu" + "d. Bảng so sánh giá" ẨN khỏi form TẠO (chưa dùng được lúc tạo) — vẫn hiện đầy đủ ở Detail tabs sau khi tạo phiếu. */} {/* e. Link hồ sơ (anh Kiệt FDC) — dán link thư mục hồ sơ trên NAS công ty (1 cột HoSoLink). Create = Input; khi xem phiếu render thẻ bấm-mở. */}
e. Link hồ sơ
setForm({ ...form, hoSoLink: e.target.value })} placeholder="Dán link thư mục hồ sơ trên NAS..." className="max-w-2xl text-sm" />
{/* [D1 anh Kiệt FDC] Mục 3 (NCC tham gia) / 4 (Hạng mục + Báo giá) / 5 (Ý kiến) ẨN khỏi form TẠO — chỉ là placeholder "Lưu phiếu trước", gây rối. Mở khóa đầy đủ ở Detail tabs sau khi tạo phiếu. */}
{/* 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 && ( )}
) } // Helper components — duplicate từ PeDetailTabs để tránh circular import. function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) }