setWinner.mutate(e.target.value)}
@@ -581,6 +581,13 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea
))}
+ {/* 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 && del.mutate()} disabled={del.isPending}>Xóa }
- Hủy
- mut.mutate()} disabled={mut.isPending}>Lưu
+ {existing && del.mutate()} disabled={isSaving}>{del.isPending ? 'Đang xóa…' : 'Xóa'} }
+ Hủy
+ mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}
>}
>
-
+ {/* 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}
-
- setForm({ ...form, isSelected: e.target.checked })} />
- Chọn NCC này cho hạng mục
-
Ghi chú 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
-
+
setWinner.mutate(e.target.value)}
@@ -581,6 +581,13 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea
))}
+ {/* 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 && del.mutate()} disabled={del.isPending}>Xóa }
- Hủy
- mut.mutate()} disabled={mut.isPending}>Lưu
+ {existing && del.mutate()} disabled={isSaving}>{del.isPending ? 'Đang xóa…' : 'Xóa'} }
+ Hủy
+ mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}
>}
>
-
+ {/* 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}
-
- setForm({ ...form, isSelected: e.target.checked })} />
- Chọn NCC này cho hạng mục
-
Ghi chú setForm({ ...form, note: e.target.value })} />