// 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 { 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, 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: '', budgetId: '' as string, // Mig 17 — manual budget fallback (toggle "Nhập tay" thay vì link) budgetManual: false, budgetManualName: '', budgetManualAmount: 0, }) 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, }) useEffect(() => { if (existing.data) { // S59: manual-mode detect theo CẢ amount (phiếu mới name=null sau khi bỏ ô Tên). const hasManual = existing.data.budgetManualAmount !== null || existing.data.budgetManualName !== null || existing.data.budgetManualAmount !== null 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 ?? '', budgetId: existing.data.budgetId ?? '', // Auto-toggle manual mode khi load existing có manual data hoặc không có link budgetManual: hasManual && !existing.data.budgetId, budgetManualName: existing.data.budgetManualName ?? '', budgetManualAmount: existing.data.budgetManualAmount ?? 0, }) } }, [existing.data]) // Manual mode: clear budgetId, gửi manualName/Amount. Link mode: clear manual. const payloadBudgetFields = form.budgetManual ? { budgetId: null, budgetManualName: null, // S59 anh chốt bỏ "Tên ngân sách" — manual chỉ còn Số tiền budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null, } : { budgetId: form.budgetId || null, budgetManualName: null, budgetManualAmount: 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) --" />
{/* Toggle "Nhập tay" — Mig 17 fallback khi không link Budget entity */}
{!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" /> đ

VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)

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