// 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, 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 { 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, }) // S57bis — list Hạng mục công việc (active only, mirror PeWorkspaceCreateView). const workItems = useQuery({ queryKey: ['catalogs', 'work-items'], queryFn: async () => (await api.get('/catalogs/work-items')).data, select: rows => rows.filter(r => r.isActive !== false), }) 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) { const hasManual = 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: form.budgetManualName || null, 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. */}
{/* 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..." />