[CLAUDE] FE-Admin+FE-User: PE QuoteDialog bỏ checkbox isSelected + winner column highlight + loading overlay/spinner
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m1s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m1s
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>`: thêm `isWinner` check → bg-emerald-50 + text-emerald-700
+ prefix "✓ " trước tên NCC khi winner
- Cell `<td>`: 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) <noreply@anthropic.com>
This commit is contained in:
@ -567,7 +567,7 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
|
<div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
|
||||||
<span className="w-44 shrink-0 text-[12px] text-slate-500">a. NCC / TP được chọn</span>
|
<span className="w-44 shrink-0 text-[12px] text-slate-500">a. NCC / TP được chọn</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="relative min-w-0 flex-1">
|
||||||
<Select
|
<Select
|
||||||
value={ev.selectedSupplierId ?? ''}
|
value={ev.selectedSupplierId ?? ''}
|
||||||
onChange={e => setWinner.mutate(e.target.value)}
|
onChange={e => setWinner.mutate(e.target.value)}
|
||||||
@ -581,6 +581,13 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
{/* Loading spinner inline khi save có delay (user 2026-05-07) */}
|
||||||
|
{setWinner.isPending && (
|
||||||
|
<div className="mt-1 flex items-center gap-1.5 text-[11px] text-brand-600">
|
||||||
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-brand-300 border-t-brand-600" />
|
||||||
|
<span>Đang chọn NCC + sync cột giá Section 4…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{ev.suppliers.length === 0 && (
|
{ev.suppliers.length === 0 && (
|
||||||
<p className="mt-1 text-[11px] text-amber-600">
|
<p className="mt-1 text-[11px] text-amber-600">
|
||||||
Thêm NCC ở Section 3 trước rồi mới chọn winner.
|
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 · Δ
|
NS link · Δ
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{ev.suppliers.map(s => (
|
{ev.suppliers.map(s => {
|
||||||
<th key={s.id} className="border-r border-slate-200 px-2 py-2 text-right" title={s.displayName ?? undefined}>
|
// User 2026-05-07: dùng tên NCC (master) thay vì displayName.
|
||||||
{/* User 2026-05-07: dùng tên NCC (master) thay vì displayName
|
// Khi NCC là winner (selected ở Section 2.a) → column highlight
|
||||||
(custom name) để column header rõ ràng. displayName fallback
|
// emerald để cell giá ăn theo màu xanh (visual trace winner).
|
||||||
sang title tooltip nếu có. */}
|
const isWinner = ev.selectedSupplierId === s.supplierId
|
||||||
{s.supplierName}
|
return (
|
||||||
</th>
|
<th
|
||||||
))}
|
key={s.id}
|
||||||
|
className={cn(
|
||||||
|
'border-r border-slate-200 px-2 py-2 text-right',
|
||||||
|
isWinner && 'bg-emerald-50 text-emerald-700',
|
||||||
|
)}
|
||||||
|
title={s.displayName ?? undefined}
|
||||||
|
>
|
||||||
|
{isWinner && '✓ '}{s.supplierName}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{!readOnly && <th className="px-2 py-2"></th>}
|
{!readOnly && <th className="px-2 py-2"></th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -1228,6 +1245,10 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo
|
|||||||
})()}
|
})()}
|
||||||
{ev.suppliers.map(s => {
|
{ev.suppliers.map(s => {
|
||||||
const q = quoteKey(d.id, s.id)
|
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 (
|
return (
|
||||||
<td
|
<td
|
||||||
key={s.id}
|
key={s.id}
|
||||||
@ -1235,7 +1256,7 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo
|
|||||||
className={cn(
|
className={cn(
|
||||||
'border-r border-slate-200 px-2 py-2 text-right font-mono transition',
|
'border-r border-slate-200 px-2 py-2 text-right font-mono transition',
|
||||||
!readOnly && 'cursor-pointer hover:bg-brand-50',
|
!readOnly && 'cursor-pointer hover:bg-brand-50',
|
||||||
q?.isSelected && 'bg-emerald-50 font-semibold text-emerald-700',
|
isWinnerColumn && 'bg-emerald-50 font-semibold text-emerald-700',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
||||||
@ -1379,11 +1400,13 @@ function QuoteDialog({
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const qc = useQueryClient()
|
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({
|
const [form, setForm] = useState({
|
||||||
bgVat: existing?.bgVat ?? 0,
|
bgVat: existing?.bgVat ?? 0,
|
||||||
chuaVat: existing?.chuaVat ?? 0,
|
chuaVat: existing?.chuaVat ?? 0,
|
||||||
thanhTien: existing?.thanhTien ?? 0,
|
thanhTien: existing?.thanhTien ?? 0,
|
||||||
isSelected: existing?.isSelected ?? false,
|
|
||||||
note: existing?.note ?? '',
|
note: existing?.note ?? '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1399,6 +1422,7 @@ function QuoteDialog({
|
|||||||
purchaseEvaluationDetailId: detailId,
|
purchaseEvaluationDetailId: detailId,
|
||||||
purchaseEvaluationSupplierId: supplierRowId,
|
purchaseEvaluationSupplierId: supplierRowId,
|
||||||
...form,
|
...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() },
|
onSuccess: () => { toast.success('Đã lưu báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
@ -1410,28 +1434,37 @@ function QuoteDialog({
|
|||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isSaving = mut.isPending || del.isPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open
|
open
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={`Báo giá — ${supplierName}`}
|
title={`Báo giá — ${supplierName}`}
|
||||||
footer={<>
|
footer={<>
|
||||||
{existing && <Button variant="danger" onClick={() => del.mutate()} disabled={del.isPending}>Xóa</Button>}
|
{existing && <Button variant="danger" onClick={() => del.mutate()} disabled={isSaving}>{del.isPending ? 'Đang xóa…' : 'Xóa'}</Button>}
|
||||||
<Button variant="ghost" onClick={onClose}>Hủy</Button>
|
<Button variant="ghost" onClick={onClose} disabled={isSaving}>Hủy</Button>
|
||||||
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Lưu</Button>
|
<Button onClick={() => mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}</Button>
|
||||||
</>}
|
</>}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
{/* Loading overlay khi save có delay (user 2026-05-07) */}
|
||||||
|
<div className="relative space-y-3">
|
||||||
|
{isSaving && (
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-white/70 backdrop-blur-sm">
|
||||||
|
<div className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 shadow-md">
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-brand-300 border-t-brand-600" />
|
||||||
|
<span className="text-sm font-medium text-slate-700">
|
||||||
|
{mut.isPending ? 'Đang lưu báo giá…' : 'Đang xóa…'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong> · KL {khoiLuong}</p>
|
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong> · KL {khoiLuong}</p>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<div><Label>Đơn giá chưa VAT</Label><Input type="number" value={form.chuaVat} onChange={e => updateAndRecalc({ chuaVat: Number(e.target.value) })} /></div>
|
<div><Label>Đơn giá chưa VAT</Label><Input type="number" value={form.chuaVat} onChange={e => updateAndRecalc({ chuaVat: Number(e.target.value) })} /></div>
|
||||||
<div><Label>Đơn giá có VAT</Label><Input type="number" value={form.bgVat} onChange={e => setForm({ ...form, bgVat: Number(e.target.value) })} /></div>
|
<div><Label>Đơn giá có VAT</Label><Input type="number" value={form.bgVat} onChange={e => setForm({ ...form, bgVat: Number(e.target.value) })} /></div>
|
||||||
<div><Label>Thành tiền (auto)</Label><Input type="number" value={form.thanhTien} onChange={e => setForm({ ...form, thanhTien: Number(e.target.value) })} /></div>
|
<div><Label>Thành tiền (auto)</Label><Input type="number" value={form.thanhTien} onChange={e => setForm({ ...form, thanhTien: Number(e.target.value) })} /></div>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input type="checkbox" checked={form.isSelected} onChange={e => setForm({ ...form, isSelected: e.target.checked })} />
|
|
||||||
Chọn NCC này cho hạng mục
|
|
||||||
</label>
|
|
||||||
<div><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
|
<div><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -567,7 +567,7 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
|
<div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
|
||||||
<span className="w-44 shrink-0 text-[12px] text-slate-500">a. NCC / TP được chọn</span>
|
<span className="w-44 shrink-0 text-[12px] text-slate-500">a. NCC / TP được chọn</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="relative min-w-0 flex-1">
|
||||||
<Select
|
<Select
|
||||||
value={ev.selectedSupplierId ?? ''}
|
value={ev.selectedSupplierId ?? ''}
|
||||||
onChange={e => setWinner.mutate(e.target.value)}
|
onChange={e => setWinner.mutate(e.target.value)}
|
||||||
@ -581,6 +581,13 @@ function NccSelectorRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolea
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
{/* Loading spinner inline khi save có delay (user 2026-05-07) */}
|
||||||
|
{setWinner.isPending && (
|
||||||
|
<div className="mt-1 flex items-center gap-1.5 text-[11px] text-brand-600">
|
||||||
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-brand-300 border-t-brand-600" />
|
||||||
|
<span>Đang chọn NCC + sync cột giá Section 4…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{ev.suppliers.length === 0 && (
|
{ev.suppliers.length === 0 && (
|
||||||
<p className="mt-1 text-[11px] text-amber-600">
|
<p className="mt-1 text-[11px] text-amber-600">
|
||||||
Thêm NCC ở Section 3 trước rồi mới chọn winner.
|
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 · Δ
|
NS link · Δ
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{ev.suppliers.map(s => (
|
{ev.suppliers.map(s => {
|
||||||
<th key={s.id} className="border-r border-slate-200 px-2 py-2 text-right" title={s.displayName ?? undefined}>
|
// User 2026-05-07: dùng tên NCC (master) thay vì displayName.
|
||||||
{/* User 2026-05-07: dùng tên NCC (master) thay vì displayName
|
// Khi NCC là winner (selected ở Section 2.a) → column highlight
|
||||||
(custom name) để column header rõ ràng. displayName fallback
|
// emerald để cell giá ăn theo màu xanh (visual trace winner).
|
||||||
sang title tooltip nếu có. */}
|
const isWinner = ev.selectedSupplierId === s.supplierId
|
||||||
{s.supplierName}
|
return (
|
||||||
</th>
|
<th
|
||||||
))}
|
key={s.id}
|
||||||
|
className={cn(
|
||||||
|
'border-r border-slate-200 px-2 py-2 text-right',
|
||||||
|
isWinner && 'bg-emerald-50 text-emerald-700',
|
||||||
|
)}
|
||||||
|
title={s.displayName ?? undefined}
|
||||||
|
>
|
||||||
|
{isWinner && '✓ '}{s.supplierName}
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{!readOnly && <th className="px-2 py-2"></th>}
|
{!readOnly && <th className="px-2 py-2"></th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -1228,6 +1245,10 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo
|
|||||||
})()}
|
})()}
|
||||||
{ev.suppliers.map(s => {
|
{ev.suppliers.map(s => {
|
||||||
const q = quoteKey(d.id, s.id)
|
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 (
|
return (
|
||||||
<td
|
<td
|
||||||
key={s.id}
|
key={s.id}
|
||||||
@ -1235,7 +1256,7 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo
|
|||||||
className={cn(
|
className={cn(
|
||||||
'border-r border-slate-200 px-2 py-2 text-right font-mono transition',
|
'border-r border-slate-200 px-2 py-2 text-right font-mono transition',
|
||||||
!readOnly && 'cursor-pointer hover:bg-brand-50',
|
!readOnly && 'cursor-pointer hover:bg-brand-50',
|
||||||
q?.isSelected && 'bg-emerald-50 font-semibold text-emerald-700',
|
isWinnerColumn && 'bg-emerald-50 font-semibold text-emerald-700',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
||||||
@ -1379,11 +1400,13 @@ function QuoteDialog({
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const qc = useQueryClient()
|
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({
|
const [form, setForm] = useState({
|
||||||
bgVat: existing?.bgVat ?? 0,
|
bgVat: existing?.bgVat ?? 0,
|
||||||
chuaVat: existing?.chuaVat ?? 0,
|
chuaVat: existing?.chuaVat ?? 0,
|
||||||
thanhTien: existing?.thanhTien ?? 0,
|
thanhTien: existing?.thanhTien ?? 0,
|
||||||
isSelected: existing?.isSelected ?? false,
|
|
||||||
note: existing?.note ?? '',
|
note: existing?.note ?? '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1399,6 +1422,7 @@ function QuoteDialog({
|
|||||||
purchaseEvaluationDetailId: detailId,
|
purchaseEvaluationDetailId: detailId,
|
||||||
purchaseEvaluationSupplierId: supplierRowId,
|
purchaseEvaluationSupplierId: supplierRowId,
|
||||||
...form,
|
...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() },
|
onSuccess: () => { toast.success('Đã lưu báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
@ -1410,28 +1434,37 @@ function QuoteDialog({
|
|||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isSaving = mut.isPending || del.isPending
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open
|
open
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={`Báo giá — ${supplierName}`}
|
title={`Báo giá — ${supplierName}`}
|
||||||
footer={<>
|
footer={<>
|
||||||
{existing && <Button variant="danger" onClick={() => del.mutate()} disabled={del.isPending}>Xóa</Button>}
|
{existing && <Button variant="danger" onClick={() => del.mutate()} disabled={isSaving}>{del.isPending ? 'Đang xóa…' : 'Xóa'}</Button>}
|
||||||
<Button variant="ghost" onClick={onClose}>Hủy</Button>
|
<Button variant="ghost" onClick={onClose} disabled={isSaving}>Hủy</Button>
|
||||||
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Lưu</Button>
|
<Button onClick={() => mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}</Button>
|
||||||
</>}
|
</>}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
{/* Loading overlay khi save có delay (user 2026-05-07) */}
|
||||||
|
<div className="relative space-y-3">
|
||||||
|
{isSaving && (
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-white/70 backdrop-blur-sm">
|
||||||
|
<div className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 shadow-md">
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-brand-300 border-t-brand-600" />
|
||||||
|
<span className="text-sm font-medium text-slate-700">
|
||||||
|
{mut.isPending ? 'Đang lưu báo giá…' : 'Đang xóa…'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong> · KL {khoiLuong}</p>
|
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong> · KL {khoiLuong}</p>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<div><Label>Đơn giá chưa VAT</Label><Input type="number" value={form.chuaVat} onChange={e => updateAndRecalc({ chuaVat: Number(e.target.value) })} /></div>
|
<div><Label>Đơn giá chưa VAT</Label><Input type="number" value={form.chuaVat} onChange={e => updateAndRecalc({ chuaVat: Number(e.target.value) })} /></div>
|
||||||
<div><Label>Đơn giá có VAT</Label><Input type="number" value={form.bgVat} onChange={e => setForm({ ...form, bgVat: Number(e.target.value) })} /></div>
|
<div><Label>Đơn giá có VAT</Label><Input type="number" value={form.bgVat} onChange={e => setForm({ ...form, bgVat: Number(e.target.value) })} /></div>
|
||||||
<div><Label>Thành tiền (auto)</Label><Input type="number" value={form.thanhTien} onChange={e => setForm({ ...form, thanhTien: Number(e.target.value) })} /></div>
|
<div><Label>Thành tiền (auto)</Label><Input type="number" value={form.thanhTien} onChange={e => setForm({ ...form, thanhTien: Number(e.target.value) })} /></div>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input type="checkbox" checked={form.isSelected} onChange={e => setForm({ ...form, isSelected: e.target.checked })} />
|
|
||||||
Chọn NCC này cho hạng mục
|
|
||||||
</label>
|
|
||||||
<div><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
|
<div><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user