// Header form cho phiếu Duyệt NCC — tách từ PurchaseEvaluationCreatePage để // reuse trong Workspace mode "new". Sửa header sau khi tạo vẫn redirect về // page Create cũ (`/purchase-evaluations/new?id=`) — workspace KHÔNG re-edit // header. 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 { Textarea } from '@/components/ui/Textarea' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { PurchaseEvaluationType, PurchaseEvaluationTypeLabel, type PeDetailBundle, } 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, mirror PeWorkspaceCreateView). type WorkItemOption = { id: string code: string name: string category?: string | null isActive?: boolean } export function PeHeaderForm({ editId, defaultType, onSaved, onCancel, }: { editId?: string | null defaultType?: number /** Gọi sau khi save thành công với (newId, type). Caller decide navigation. */ onSaved: (id: string, type: number) => void onCancel?: () => void }) { const qc = useQueryClient() const initialType = defaultType ?? PurchaseEvaluationType.DuyetNcc const projects = useQuery({ queryKey: ['all-projects'], queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items, }) const existing = useQuery({ queryKey: ['pe-detail', editId], queryFn: async () => (await api.get(`/purchase-evaluations/${editId}`)).data, enabled: !!editId, }) // S59 — track Địa điểm auto-fill gần nhất (mirror PeWorkspaceCreateView). const lastAutoLoc = useRef('') // S57bis — list Hạng mục công việc (active only, mirror PeWorkspaceCreateView). // S59 — sort numeric-aware client (mã PMH không pad số — mirror CreateView). 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 })), }) const [form, setForm] = useState({ type: initialType as number, tenGoiThau: '', projectId: '', workItemId: '', diaDiem: '', moTa: '', paymentTerms: '', // [S61 Mig 50] "Ngân sách - kỳ này" — thay budgetId/budgetManual* cũ (module // Budget xóa hẳn; bảng Tổng hợp ngân sách gói thầu nằm ở PeDetailTabs). budgetPeriodAmount: 0, }) useEffect(() => { if (existing.data) { setForm({ type: existing.data.type, tenGoiThau: existing.data.tenGoiThau, projectId: existing.data.projectId, workItemId: existing.data.workItemId ?? '', // S57bis — load để PUT gửi lại, tránh null-hóa diaDiem: existing.data.diaDiem ?? '', moTa: existing.data.moTa ?? '', paymentTerms: existing.data.paymentTerms ?? '', budgetPeriodAmount: existing.data.budgetPeriodAmount ?? 0, }) } }, [existing.data]) // [S61] PUT UpdateDraft null-safe: budgetPeriodAmount null = GIỮ giá trị cũ // BE-side; gửi số > 0 mới set. (Clear hẳn → dùng bảng Tổng hợp/budget-adjust.) const payloadBudgetFields = { budgetPeriodAmount: form.budgetPeriodAmount > 0 ? form.budgetPeriodAmount : null, } const mut = useMutation({ mutationFn: async () => { if (editId) { return api.put(`/purchase-evaluations/${editId}`, { id: editId, tenGoiThau: form.tenGoiThau, workItemId: form.workItemId || null, // S57bis — BE null-safe: null = giữ nguyên diaDiem: form.diaDiem || null, moTa: form.moTa || null, paymentTerms: form.paymentTerms || null, ...payloadBudgetFields, }) } return api.post<{ id: string }>('/purchase-evaluations', { type: form.type, tenGoiThau: form.tenGoiThau, projectId: form.projectId, workItemId: form.workItemId || null, // S57bis — create require (BE validator NotEmpty) diaDiem: form.diaDiem || null, moTa: form.moTa || null, paymentTerms: form.paymentTerms || null, ...payloadBudgetFields, }) }, onSuccess: res => { toast.success(editId ? 'Đã lưu.' : 'Đã tạo phiếu.') qc.invalidateQueries({ queryKey: ['pe-list'] }) const id = editId ?? (res as { data: { id: string } }).data.id onSaved(id, form.type) }, onError: e => toast.error(getErrorMessage(e)), }) return (

{editId ? 'Sửa header phiếu' : 'Tạo phiếu Duyệt NCC mới'}

{editId ? 'Chỉ sửa các field thông tin chung — NCC + báo giá + ý kiến nhập ở Panel chi tiết.' : 'Tạo header trước, sau đó nhập NCC + Báo giá + Hạng mục ở Panel chi tiết.'}

{/* [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 2 field S57bis thành 1 select: chọn hạng mục set cả workItemId + tenGoiThau (= tên hạng mục). Phiếu cũ (workItemId null, tên nhập tay): option đầu "Giữ nguyên" — không ép đổi, PUT null-safe. */} {/* S59 UAT "nên có lọc để tự đánh chữ" → SearchableSelect gõ-lọc bỏ dấu. Phiếu cũ (workItemId null): placeholder "Giữ nguyên: …", clear (×) = về giữ-nguyên. */} ({ 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 ?? '') : (editId ? form.tenGoiThau : '') }) }} placeholder={editId && form.tenGoiThau && !form.workItemId ? `Giữ nguyên: ${form.tenGoiThau}` : '-- Chọn hạng mục công việc (gõ để lọc) --'} />
{/* S59 UAT: gõ-lọc + auto-fill Địa điểm từ Project.Location (mirror CreateView; chỉ ăn khi tạo mới — edit disabled). Ghi đè khi user chưa gõ tay diaDiem. */} ({ value: p.id, label: `${p.code} — ${p.name}` }))} value={form.projectId} disabled={!!editId} 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) --" />
{/* [S61 Mig 50] Ô đơn "Ngân sách kỳ này" — thay picker Budget cũ + toggle nhập tay. Số phân bổ cho RIÊNG phiếu này (row 3 bảng Tổng hợp). */}
setForm({ ...form, budgetPeriodAmount: parseVnd(e.target.value) })} placeholder="0" className="pr-10 font-mono text-right" /> đ

Số phân bổ cho riêng phiếu này — 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ý" trong phiếu.

setForm({ ...form, diaDiem: e.target.value })} placeholder="Lô K, KCN Lộc An - Bình Sơn..." />