diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 1384dc5..bb204ad 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -989,7 +989,7 @@ function VndInlineEdit({ type="text" inputMode="numeric" value={text} - onChange={e => setText(e.target.value.replace(/[^\d.]/g, ''))} + onChange={e => setText(formatVndInput(parseVnd(e.target.value)))} placeholder="0" aria-label={label} className="h-7 pr-6 font-mono text-right text-[13px]" @@ -1101,7 +1101,7 @@ function BudgetCell({ value, editable, allowNegative = false, saving, onSave }: type="text" inputMode="numeric" value={text} - onChange={e => setText(e.target.value.replace(/[^\d.]/g, ''))} + onChange={e => setText(formatVndInput(parseVnd(e.target.value)))} placeholder="0" className="h-7 min-w-0 flex-1 px-1.5 text-right font-mono text-[12px]" /> @@ -1512,16 +1512,17 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] }) qc.invalidateQueries({ queryKey: ['pe-list'] }) } - // PRO Min/Max — ABSOLUTE SET cả cặp (mirror budget CCM dual-field: field không đổi - // echo giá trị hiện tại để không bị clear). + // PRO Min/Max + note — ABSOLUTE SET cả 3 field (mirror S74 CcmNote: field không đổi + // echo giá trị hiện tại để không bị clear). Lưu giá → echo note hiện tại; lưu note → + // echo min/max hiện tại. const proPriceMut = useMutation({ - mutationFn: async (body: { minPrice: number | null; maxPrice: number | null }) => + mutationFn: async (body: { minPrice: number | null; maxPrice: number | null; note: string | null }) => api.put(`/purchase-evaluations/${ev.id}/suggested-price/pro`, body), onSuccess: () => { toast.success('Đã lưu giá đề xuất (PRO)'); invalidate() }, onError: e => toast.error(getErrorMessage(e)), }) const ccmPriceMut = useMutation({ - mutationFn: async (body: { ccmPrice: number | null }) => + mutationFn: async (body: { ccmPrice: number | null; note: string | null }) => api.put(`/purchase-evaluations/${ev.id}/suggested-price/ccm`, body), onSuccess: () => { toast.success('Đã lưu giá đề xuất (CCM)'); invalidate() }, onError: e => toast.error(getErrorMessage(e)), @@ -1529,9 +1530,21 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo const canEditPro = !readOnly && ev.canEditProSuggestedPrice const canEditCcm = !readOnly && ev.canEditCcmSuggestedPrice + + // Ghi chú PRO/CCM inline-edit state (Textarea). Echo cùng body absolute-set khi lưu giá. + const [proNoteText, setProNoteText] = useState(ev.proSuggestedPriceNote ?? '') + useEffect(() => { setProNoteText(ev.proSuggestedPriceNote ?? '') }, [ev.proSuggestedPriceNote]) + const [ccmNoteText, setCcmNoteText] = useState(ev.ccmSuggestedPriceNote ?? '') + useEffect(() => { setCcmNoteText(ev.ccmSuggestedPriceNote ?? '') }, [ev.ccmSuggestedPriceNote]) + // [gotcha #70] khoá nút Lưu tới khi pe-detail refetch land — lưu giá xong lưu + // ghi chú ngay (hoặc ngược lại) sẽ echo ev.* CŨ từ snapshot → mất dữ liệu. + // Mirror peFetching của bảng ngân sách (S76). + const peFetching = useIsFetching({ queryKey: ['pe-detail', ev.id] }) > 0 const hasAnyValue = ev.proSuggestedMinPrice != null || ev.proSuggestedMaxPrice != null || ev.ccmSuggestedPrice != null + || !!ev.proSuggestedPriceNote + || !!ev.ccmSuggestedPriceNote const approved = ev.phase === PurchaseEvaluationPhase.DaDuyet && ev.approvedPriceAmount != null // Ẩn hoàn toàn khi không edit được + chưa có giá nào + chưa chốt → tránh khối rỗng. @@ -1552,9 +1565,9 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo {canEditPro ? ( proPriceMut.mutate({ minPrice: v, maxPrice: ev.proSuggestedMaxPrice })} + onSave={v => proPriceMut.mutate({ minPrice: v, maxPrice: ev.proSuggestedMaxPrice, note: ev.proSuggestedPriceNote })} /> ) : ( @@ -1567,9 +1580,9 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo {canEditPro ? ( proPriceMut.mutate({ minPrice: ev.proSuggestedMinPrice, maxPrice: v })} + onSave={v => proPriceMut.mutate({ minPrice: ev.proSuggestedMinPrice, maxPrice: v, note: ev.proSuggestedPriceNote })} /> ) : ( @@ -1580,6 +1593,42 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo + {/* Ghi chú PRO — vì sao chọn Min/Max. Editable khi canEditPro (Textarea + nút Lưu), + read-only hiện text khi có note. Lưu qua proPriceMut (echo min/max hiện tại). */} + {(canEditPro || ev.proSuggestedPriceNote) && ( +
+ Ghi chú (PRO) +
+ {canEditPro ? ( +
+