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 })} />