From f568945069a5403180a7cb913a3bfe173457ec27 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Mon, 11 May 2026 11:12:43 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-PE:=20Manual=20budget=20"Nh?= =?UTF-8?q?=E1=BA=ADp=20tay"=20=E2=80=94=20drop=20T=C3=AAn=20field,=20form?= =?UTF-8?q?at=20VND?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User Session 20 turn 6 screenshot: chế độ "Nhập tay (không link)" Section 2 b. Ngân sách vẫn còn input "Tên (vd Tạm tính T11/2025)" cùng số tiền. User chỉ cần nhập số tiền — bỏ Tên + áp VND format consistent. 3 file × 2 app = 6 file FE update: - PeDetailTabs.tsx BudgetFieldRow (Section 2 detail editor) - PeWorkspaceCreateView.tsx (workspace mode "new") - PeHeaderForm.tsx (Create/Edit header page) Mỗi file: - Drop Input "Tên ngân sách" UI khỏi manual mode (state field giữ '' để backward compat — BE save luôn null) - Manual mode UI giờ chỉ 1 input số tiền (max-w-xs): * type="text" inputMode="numeric" + value={formatVndInput(amount)} * onChange={parseVnd} strip non-digit → number * Suffix "đ" tuyệt đối inset-y-0 right-3 * Hint "VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)" - Helpers parseVnd + formatVndInput inline mỗi file (mirror PeDetailTabs) PeDetailTabs BudgetFieldRow cleanup: - Drop state manualName + setManualName - Drop manualName từ dirty check - Save payload: budgetManualName: null luôn (không phụ thuộc state) - Hủy thay đổi: drop reset manualName line Read-only display (legacy data) giữ ev.budgetManualName nếu data cũ có tên (đoạn render khi !canEdit) — không xóa hiển thị, chỉ ẩn input UI. BE schema KHÔNG đụng — endpoint PUT /pe/:id vẫn nhận budgetManualName field, chỉ FE luôn gửi null. Verify: - npm run build × fe-admin pass - npm run build × fe-user pass Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 33 +++++++---------- fe-admin/src/components/pe/PeHeaderForm.tsx | 37 ++++++++----------- .../components/pe/PeWorkspaceCreateView.tsx | 26 ++++++------- fe-user/src/components/pe/PeDetailTabs.tsx | 33 +++++++---------- fe-user/src/components/pe/PeHeaderForm.tsx | 37 ++++++++----------- .../components/pe/PeWorkspaceCreateView.tsx | 26 ++++++------- 6 files changed, 82 insertions(+), 110 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 5096a2d..c7ad1f6 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -766,11 +766,13 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea const canEdit = !readOnly && isEditablePhase(ev.phase) const qc = useQueryClient() - // Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link + // Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link. + // Session 20 turn 6: user yêu cầu manual mode chỉ nhập số tiền — bỏ Tên field + // khỏi UI. State manualName drop, BE save luôn null cho field này. Data cũ với + // tên vẫn hiển thị OK ở read-only display (ev.budgetManualName). const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId const [manualMode, setManualMode] = useState(initialManual) const [budgetId, setBudgetId] = useState(ev.budgetId ?? '') - const [manualName, setManualName] = useState(ev.budgetManualName ?? '') const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0) // Eligible budgets — chỉ fetch khi user có khả năng edit @@ -784,13 +786,13 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea // Dirty detect — compare current state vs ev original const dirty = manualMode !== initialManual - || (manualMode && (manualName !== (ev.budgetManualName ?? '') || manualAmount !== (ev.budgetManualAmount ?? 0))) + || (manualMode && manualAmount !== (ev.budgetManualAmount ?? 0)) || (!manualMode && budgetId !== (ev.budgetId ?? '')) const save = useMutation({ mutationFn: async () => { const payload = manualMode - ? { budgetId: null, budgetManualName: manualName || null, budgetManualAmount: manualAmount > 0 ? manualAmount : null } + ? { budgetId: null, budgetManualName: null, budgetManualAmount: manualAmount > 0 ? manualAmount : null } : { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null } await api.put(`/purchase-evaluations/${ev.id}`, { id: ev.id, @@ -862,22 +864,16 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea ))} ) : ( -
+
setManualName(e.target.value)} - placeholder="Tên ngân sách (vd Tạm tính T11/2025)" - maxLength={200} - className="text-sm" - /> - setManualAmount(Number(e.target.value))} - placeholder="Số tiền (đ)" - className="text-sm" + type="text" + inputMode="numeric" + value={formatVndInput(manualAmount)} + onChange={e => setManualAmount(parseVnd(e.target.value))} + placeholder="0" + className="pr-10 font-mono text-right text-sm" /> + đ
)} {dirty && ( @@ -893,7 +889,6 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea onClick={() => { setManualMode(initialManual) setBudgetId(ev.budgetId ?? '') - setManualName(ev.budgetManualName ?? '') setManualAmount(ev.budgetManualAmount ?? 0) }} className="text-[11px] text-slate-500 hover:text-slate-700" diff --git a/fe-admin/src/components/pe/PeHeaderForm.tsx b/fe-admin/src/components/pe/PeHeaderForm.tsx index 75f84f1..bcc30dd 100644 --- a/fe-admin/src/components/pe/PeHeaderForm.tsx +++ b/fe-admin/src/components/pe/PeHeaderForm.tsx @@ -20,6 +20,10 @@ import { 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') : '') + export function PeHeaderForm({ editId, defaultType, @@ -220,31 +224,20 @@ export function PeHeaderForm({

) : ( -
-
- +
+ +
setForm({ ...form, budgetManualName: e.target.value })} - placeholder="vd Tạm tính dự toán T11/2025" - maxLength={200} + type="text" + inputMode="numeric" + value={formatVndInput(form.budgetManualAmount)} + onChange={e => setForm({ ...form, budgetManualAmount: parseVnd(e.target.value) })} + placeholder="0" + className="pr-10 font-mono text-right" /> + đ
-
- - setForm({ ...form, budgetManualAmount: Number(e.target.value) })} - placeholder="1000000000" - /> - {form.budgetManualAmount > 0 && ( -

- ≈ {form.budgetManualAmount.toLocaleString('vi-VN')} đ -

- )} -
+

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

)}
diff --git a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx index c580d75..1c742d8 100644 --- a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx +++ b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx @@ -20,6 +20,10 @@ import { PurchaseEvaluationTypeLabel } 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') : '') + // Preset điều khoản thanh toán phổ biến — user chọn 1 trong list, hoặc "Khác" // để nhập tay. Save as plain text (không JSON như cũ — code-style không phù // hợp UI cho end-user). User 2026-05-07 chỉnh. @@ -281,22 +285,16 @@ export function PeWorkspaceCreateView({

) : ( -
+
setForm({ ...form, budgetManualName: e.target.value })} - placeholder="Tên (vd Tạm tính T11/2025)" - maxLength={200} - className="text-sm" - /> - setForm({ ...form, budgetManualAmount: Number(e.target.value) })} - placeholder="Số tiền (đ)" - className="text-sm" + type="text" + inputMode="numeric" + value={formatVndInput(form.budgetManualAmount)} + onChange={e => setForm({ ...form, budgetManualAmount: parseVnd(e.target.value) })} + placeholder="0" + className="pr-10 font-mono text-right text-sm" /> + đ
)}
diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 5096a2d..c7ad1f6 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -766,11 +766,13 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea const canEdit = !readOnly && isEditablePhase(ev.phase) const qc = useQueryClient() - // Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link + // Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link. + // Session 20 turn 6: user yêu cầu manual mode chỉ nhập số tiền — bỏ Tên field + // khỏi UI. State manualName drop, BE save luôn null cho field này. Data cũ với + // tên vẫn hiển thị OK ở read-only display (ev.budgetManualName). const initialManual = (ev.budgetManualName !== null || ev.budgetManualAmount !== null) && !ev.budgetId const [manualMode, setManualMode] = useState(initialManual) const [budgetId, setBudgetId] = useState(ev.budgetId ?? '') - const [manualName, setManualName] = useState(ev.budgetManualName ?? '') const [manualAmount, setManualAmount] = useState(ev.budgetManualAmount ?? 0) // Eligible budgets — chỉ fetch khi user có khả năng edit @@ -784,13 +786,13 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea // Dirty detect — compare current state vs ev original const dirty = manualMode !== initialManual - || (manualMode && (manualName !== (ev.budgetManualName ?? '') || manualAmount !== (ev.budgetManualAmount ?? 0))) + || (manualMode && manualAmount !== (ev.budgetManualAmount ?? 0)) || (!manualMode && budgetId !== (ev.budgetId ?? '')) const save = useMutation({ mutationFn: async () => { const payload = manualMode - ? { budgetId: null, budgetManualName: manualName || null, budgetManualAmount: manualAmount > 0 ? manualAmount : null } + ? { budgetId: null, budgetManualName: null, budgetManualAmount: manualAmount > 0 ? manualAmount : null } : { budgetId: budgetId || null, budgetManualName: null, budgetManualAmount: null } await api.put(`/purchase-evaluations/${ev.id}`, { id: ev.id, @@ -862,22 +864,16 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea ))} ) : ( -
+
setManualName(e.target.value)} - placeholder="Tên ngân sách (vd Tạm tính T11/2025)" - maxLength={200} - className="text-sm" - /> - setManualAmount(Number(e.target.value))} - placeholder="Số tiền (đ)" - className="text-sm" + type="text" + inputMode="numeric" + value={formatVndInput(manualAmount)} + onChange={e => setManualAmount(parseVnd(e.target.value))} + placeholder="0" + className="pr-10 font-mono text-right text-sm" /> + đ
)} {dirty && ( @@ -893,7 +889,6 @@ function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea onClick={() => { setManualMode(initialManual) setBudgetId(ev.budgetId ?? '') - setManualName(ev.budgetManualName ?? '') setManualAmount(ev.budgetManualAmount ?? 0) }} className="text-[11px] text-slate-500 hover:text-slate-700" diff --git a/fe-user/src/components/pe/PeHeaderForm.tsx b/fe-user/src/components/pe/PeHeaderForm.tsx index 75f84f1..bcc30dd 100644 --- a/fe-user/src/components/pe/PeHeaderForm.tsx +++ b/fe-user/src/components/pe/PeHeaderForm.tsx @@ -20,6 +20,10 @@ import { 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') : '') + export function PeHeaderForm({ editId, defaultType, @@ -220,31 +224,20 @@ export function PeHeaderForm({

) : ( -
-
- +
+ +
setForm({ ...form, budgetManualName: e.target.value })} - placeholder="vd Tạm tính dự toán T11/2025" - maxLength={200} + type="text" + inputMode="numeric" + value={formatVndInput(form.budgetManualAmount)} + onChange={e => setForm({ ...form, budgetManualAmount: parseVnd(e.target.value) })} + placeholder="0" + className="pr-10 font-mono text-right" /> + đ
-
- - setForm({ ...form, budgetManualAmount: Number(e.target.value) })} - placeholder="1000000000" - /> - {form.budgetManualAmount > 0 && ( -

- ≈ {form.budgetManualAmount.toLocaleString('vi-VN')} đ -

- )} -
+

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

)}
diff --git a/fe-user/src/components/pe/PeWorkspaceCreateView.tsx b/fe-user/src/components/pe/PeWorkspaceCreateView.tsx index 1896b82..8c99897 100644 --- a/fe-user/src/components/pe/PeWorkspaceCreateView.tsx +++ b/fe-user/src/components/pe/PeWorkspaceCreateView.tsx @@ -20,6 +20,10 @@ import { PurchaseEvaluationTypeLabel } 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') : '') + // Preset điều khoản thanh toán phổ biến — user chọn 1 trong list, hoặc "Khác" // để nhập tay. Save as plain text (không JSON như cũ — code-style không phù // hợp UI cho end-user). User 2026-05-07 chỉnh. @@ -279,22 +283,16 @@ export function PeWorkspaceCreateView({

) : ( -
+
setForm({ ...form, budgetManualName: e.target.value })} - placeholder="Tên (vd Tạm tính T11/2025)" - maxLength={200} - className="text-sm" - /> - setForm({ ...form, budgetManualAmount: Number(e.target.value) })} - placeholder="Số tiền (đ)" - className="text-sm" + type="text" + inputMode="numeric" + value={formatVndInput(form.budgetManualAmount)} + onChange={e => setForm({ ...form, budgetManualAmount: parseVnd(e.target.value) })} + placeholder="0" + className="pr-10 font-mono text-right text-sm" /> + đ
)}