From 94e0e12f777923bfcdcce2e31881b3285b3d6578 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 19 Jun 2026 14:08:45 +0700 Subject: [PATCH] [CLAUDE] PurchaseEvaluation: Mig 57 ghi chu gia de xuat PRO/CCM + so phan cach VND + sua chinh ta + guard #70 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theo Tra Sol + anh Kiet FDC (Zalo): A) o nhap tien VndInlineEdit + BudgetCell nhay phan cach vi-VN (300.000.000) on-keystroke (o dialog da co san). B) them o ghi chu PRO + CCM canh nut Luu trong khoi Gia de xuat (giai thich vi sao chon Min/Max) — Mig 57 AddPeSuggestedPriceNotes (+ProSuggestedPriceNote +CcmSuggestedPriceNote nvarchar(1000) null, additive no-backfill no-table); 2 setter command +Note absolute-set rides role-gate PRO/CCM/Admin; DTO +2 field; controller body +note. C) sua chinh ta 'd. Ban so sanh' -> 'd. Bang so sanh gia' (2 app). GUARD gotcha #70: o gia + ghi chu echo nhau absolute-set -> them peFetching khoa nut Luu toi khi pe-detail refetch land, tranh stale-echo mat du lieu (mirror bang ngan sach S76; em-main review bat impl-frontend sot guard). BE slnx 0-warn 0-err; FE build PASS x2; test 344->351 (+7); PeDetailTabs/PeWorkspaceCreateView 2 app SHA256-identical. Co-Authored-By: Claude Opus 4.8 --- fe-admin/src/components/pe/PeDetailTabs.tsx | 110 +- .../components/pe/PeWorkspaceCreateView.tsx | 2 +- fe-admin/src/types/purchaseEvaluation.ts | 4 + fe-user/src/components/pe/PeDetailTabs.tsx | 110 +- .../components/pe/PeWorkspaceCreateView.tsx | 2 +- fe-user/src/types/purchaseEvaluation.ts | 4 + .../PurchaseEvaluationsController.cs | 8 +- .../Dtos/PurchaseEvaluationDtos.cs | 4 + .../PeSuggestedPriceFeatures.cs | 19 +- .../PurchaseEvaluationFeatures.cs | 1 + .../PurchaseEvaluations/PurchaseEvaluation.cs | 8 + .../PurchaseEvaluationConfiguration.cs | 4 + ...70051_AddPeSuggestedPriceNotes.Designer.cs | 6259 +++++++++++++++++ ...20260619070051_AddPeSuggestedPriceNotes.cs | 40 + .../ApplicationDbContextModelSnapshot.cs | 8 + .../PeSuggestedPriceSetterAuthzTests.cs | 167 + 16 files changed, 6714 insertions(+), 36 deletions(-) create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260619070051_AddPeSuggestedPriceNotes.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260619070051_AddPeSuggestedPriceNotes.cs 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 ? ( +
+