From d2306b88d1505edf038c004577006ef1e2ca11fd Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 7 May 2026 16:49:48 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin+FE-User:=20PE=20QuoteDialog?= =?UTF-8?q?=20b=E1=BB=8F=20checkbox=20isSelected=20+=20winner=20column=20h?= =?UTF-8?q?ighlight=20+=20loading=20overlay/spinner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback 2026-05-07 (annotation): 1. Bỏ checkbox "Chọn NCC này cho hạng mục" trong QuoteDialog (consolidate winner selection chỉ ở Section 2.a NccSelectorRow — tránh 2 nơi pick winner). 2. Khi NCC là winner (selectedSupplierId === s.supplierId) → cell giá Section 4 matrix ăn theo màu xanh emerald (header + cells trong column). 3. Save có delay → hiện loading spinner / overlay để user biết đang xử lý. Implementation: ~ QuoteDialog (× 2 app): - Remove `isSelected` từ form state + UI checkbox - Vẫn gửi `isSelected: existing?.isSelected ?? false` lên API (giữ nguyên trạng thái cũ — không expose UI để tránh confusion) - Disable Xóa/Hủy/Lưu khi `isSaving = mut.isPending || del.isPending` - Button text: "Đang lưu báo giá…" / "Đang xóa…" thay "Lưu" / "Xóa" - Full overlay loading: absolute z-10 + bg-white/70 backdrop-blur-sm + spinner ring brand-600 + status text rõ ràng ~ ItemsTab matrix (× 2 app): - Column header ``: thêm `isWinner` check → bg-emerald-50 + text-emerald-700 + prefix "✓ " trước tên NCC khi winner - Cell ``: thay `q?.isSelected` highlight → `isWinnerColumn` (entire column ăn theo Section 2.a winner). Cells của winner column LUÔN xanh bất kể quote đã nhập hay chưa (visual trace winner rõ ràng). ~ NccSelectorRow (× 2 app): - Wrap Select trong `relative` div - Thêm inline spinner + text "Đang chọn NCC + sync cột giá Section 4…" khi setWinner.isPending — báo cho user biết delay đang xử lý Verify: npm run build fe-admin + fe-user pass · 0 TS error. UAT mode: skip dotnet test (FE-only), push ngay. Co-Authored-By: Claude Opus 4.7 (1M context) --- fe-admin/src/components/pe/PeDetailTabs.tsx | 71 +++++++++++++++------ fe-user/src/components/pe/PeDetailTabs.tsx | 71 +++++++++++++++------ 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 668df84..c2a6775 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -567,7 +567,7 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea return (
a. NCC / TP được chọn -
+
+ {/* Loading spinner inline khi save có delay (user 2026-05-07) */} + {setWinner.isPending && ( +
+
+ Đang chọn NCC + sync cột giá Section 4… +
+ )} {ev.suppliers.length === 0 && (

Thêm NCC ở Section 3 trước rồi mới chọn winner. @@ -1185,14 +1192,24 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo NS link · Δ )} - {ev.suppliers.map(s => ( - - {/* User 2026-05-07: dùng tên NCC (master) thay vì displayName - (custom name) để column header rõ ràng. displayName fallback - sang title tooltip nếu có. */} - {s.supplierName} - - ))} + {ev.suppliers.map(s => { + // User 2026-05-07: dùng tên NCC (master) thay vì displayName. + // Khi NCC là winner (selected ở Section 2.a) → column highlight + // emerald để cell giá ăn theo màu xanh (visual trace winner). + const isWinner = ev.selectedSupplierId === s.supplierId + return ( + + {isWinner && '✓ '}{s.supplierName} + + ) + })} {!readOnly && } @@ -1228,6 +1245,10 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo })()} {ev.suppliers.map(s => { const q = quoteKey(d.id, s.id) + // Winner NCC (selected ở Section 2.a) → cell ăn theo màu xanh + // emerald (user 2026-05-07). isSelected per-quote checkbox bỏ + // (đã consolidate winner ở Section 2.a NccSelectorRow). + const isWinnerColumn = ev.selectedSupplierId === s.supplierId return ( {q ? fmtMoney(q.thanhTien) : } @@ -1379,11 +1400,13 @@ function QuoteDialog({ onClose: () => void }) { const qc = useQueryClient() + // User 2026-05-07: Bỏ `isSelected` checkbox per-quote (consolidate winner + // selection ở Section 2.a NccSelectorRow). BE vẫn nhận isSelected nhưng FE + // luôn gửi `false` (existing.isSelected nếu có để giữ nguyên trạng thái cũ). const [form, setForm] = useState({ bgVat: existing?.bgVat ?? 0, chuaVat: existing?.chuaVat ?? 0, thanhTien: existing?.thanhTien ?? 0, - isSelected: existing?.isSelected ?? false, note: existing?.note ?? '', }) @@ -1399,6 +1422,7 @@ function QuoteDialog({ purchaseEvaluationDetailId: detailId, purchaseEvaluationSupplierId: supplierRowId, ...form, + isSelected: existing?.isSelected ?? false, // giữ nguyên trạng thái cũ, không expose UI }), onSuccess: () => { toast.success('Đã lưu báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() }, onError: e => toast.error(getErrorMessage(e)), @@ -1410,28 +1434,37 @@ function QuoteDialog({ onError: e => toast.error(getErrorMessage(e)), }) + const isSaving = mut.isPending || del.isPending + return (

- {existing && } - - + {existing && } + + } > -
+ {/* Loading overlay khi save có delay (user 2026-05-07) */} +
+ {isSaving && ( +
+
+
+ + {mut.isPending ? 'Đang lưu báo giá…' : 'Đang xóa…'} + +
+
+ )}

Hạng mục: {itemName} · KL {khoiLuong}

updateAndRecalc({ chuaVat: Number(e.target.value) })} />
setForm({ ...form, bgVat: Number(e.target.value) })} />
setForm({ ...form, thanhTien: Number(e.target.value) })} />
-
setForm({ ...form, note: e.target.value })} />
diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 668df84..c2a6775 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -567,7 +567,7 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea return (
a. NCC / TP được chọn -
+
+ {/* Loading spinner inline khi save có delay (user 2026-05-07) */} + {setWinner.isPending && ( +
+
+ Đang chọn NCC + sync cột giá Section 4… +
+ )} {ev.suppliers.length === 0 && (

Thêm NCC ở Section 3 trước rồi mới chọn winner. @@ -1185,14 +1192,24 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo NS link · Δ )} - {ev.suppliers.map(s => ( - - {/* User 2026-05-07: dùng tên NCC (master) thay vì displayName - (custom name) để column header rõ ràng. displayName fallback - sang title tooltip nếu có. */} - {s.supplierName} - - ))} + {ev.suppliers.map(s => { + // User 2026-05-07: dùng tên NCC (master) thay vì displayName. + // Khi NCC là winner (selected ở Section 2.a) → column highlight + // emerald để cell giá ăn theo màu xanh (visual trace winner). + const isWinner = ev.selectedSupplierId === s.supplierId + return ( + + {isWinner && '✓ '}{s.supplierName} + + ) + })} {!readOnly && } @@ -1228,6 +1245,10 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo })()} {ev.suppliers.map(s => { const q = quoteKey(d.id, s.id) + // Winner NCC (selected ở Section 2.a) → cell ăn theo màu xanh + // emerald (user 2026-05-07). isSelected per-quote checkbox bỏ + // (đã consolidate winner ở Section 2.a NccSelectorRow). + const isWinnerColumn = ev.selectedSupplierId === s.supplierId return ( {q ? fmtMoney(q.thanhTien) : } @@ -1379,11 +1400,13 @@ function QuoteDialog({ onClose: () => void }) { const qc = useQueryClient() + // User 2026-05-07: Bỏ `isSelected` checkbox per-quote (consolidate winner + // selection ở Section 2.a NccSelectorRow). BE vẫn nhận isSelected nhưng FE + // luôn gửi `false` (existing.isSelected nếu có để giữ nguyên trạng thái cũ). const [form, setForm] = useState({ bgVat: existing?.bgVat ?? 0, chuaVat: existing?.chuaVat ?? 0, thanhTien: existing?.thanhTien ?? 0, - isSelected: existing?.isSelected ?? false, note: existing?.note ?? '', }) @@ -1399,6 +1422,7 @@ function QuoteDialog({ purchaseEvaluationDetailId: detailId, purchaseEvaluationSupplierId: supplierRowId, ...form, + isSelected: existing?.isSelected ?? false, // giữ nguyên trạng thái cũ, không expose UI }), onSuccess: () => { toast.success('Đã lưu báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() }, onError: e => toast.error(getErrorMessage(e)), @@ -1410,28 +1434,37 @@ function QuoteDialog({ onError: e => toast.error(getErrorMessage(e)), }) + const isSaving = mut.isPending || del.isPending + return (

- {existing && } - - + {existing && } + + } > -
+ {/* Loading overlay khi save có delay (user 2026-05-07) */} +
+ {isSaving && ( +
+
+
+ + {mut.isPending ? 'Đang lưu báo giá…' : 'Đang xóa…'} + +
+
+ )}

Hạng mục: {itemName} · KL {khoiLuong}

updateAndRecalc({ chuaVat: Number(e.target.value) })} />
setForm({ ...form, bgVat: Number(e.target.value) })} />
setForm({ ...form, thanhTien: Number(e.target.value) })} />
-
setForm({ ...form, note: e.target.value })} />