[CLAUDE] FE-PE: NCC table 1 cột "Số tiền" + QuoteDialog 1 input đơn giản hóa
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
User Session 20 turn 3: "Tạm thời chỉ cần nhập số tiền vào là đc, không cần
3 cột có VAT / ko VAT / tổng. 2 cột kia ẩn đi, chỉ 1 cột nhập tiền duy nhất."
FE-only mirror fe-admin + fe-user:
NCC inline table HangMucCard — bỏ 2 th + 2 td:
Trước: NCC | Liên hệ | Điều khoản TT | File báo giá | ĐG chưa VAT | ĐG có VAT | Thành tiền | Action
Sau: NCC | Liên hệ | Điều khoản TT | File báo giá | Số tiền | Action
QuoteDialog — đơn giản hóa form:
Trước: 3 input (Đơn giá chưa VAT / ĐG có VAT / Thành tiền auto-calc) + Ghi chú
+ display khoiLuong info
Sau: 1 input "Số tiền" (autoFocus) — map thẳng vào thanhTien field
Schema BE giữ nguyên (bgVat / chuaVat / note vẫn POST):
- Row mới: bgVat=0, chuaVat=0, note=''
- Existing: giữ giá trị cũ
Bỏ prop khoiLuong (không dùng — không còn auto-calc thanhTien = chuaVat × khoiLuong)
Bỏ updateAndRecalc helper
KHÔNG đụng schema BE — endpoint POST /purchase-evaluations/{id}/quotes giữ
nguyên payload shape, chỉ FE rút gọn input mặt người dùng nhập.
Verify:
- npm run build × fe-admin pass
- npm run build × fe-user pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1334,9 +1334,7 @@ function HangMucCard({
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-left">Liên hệ</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-left">Điều khoản TT</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-left">File báo giá</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">ĐG chưa VAT</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">ĐG có VAT</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">Thành tiền</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th>
|
||||
{!readOnly && <th className="px-2 py-1.5"></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
@ -1374,18 +1372,6 @@ function HangMucCard({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
onClick={readOnly ? undefined : openQuote}
|
||||
className={cn('border-r border-slate-200 px-2 py-1.5 text-right font-mono', cellHover)}
|
||||
>
|
||||
{q ? fmtMoney(q.chuaVat) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
<td
|
||||
onClick={readOnly ? undefined : openQuote}
|
||||
className={cn('border-r border-slate-200 px-2 py-1.5 text-right font-mono', cellHover)}
|
||||
>
|
||||
{q ? fmtMoney(q.bgVat) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
<td
|
||||
onClick={readOnly ? undefined : openQuote}
|
||||
className={cn(
|
||||
@ -1393,7 +1379,7 @@ function HangMucCard({
|
||||
isWinner && 'text-emerald-700',
|
||||
cellHover,
|
||||
)}
|
||||
title={!readOnly ? 'Click để nhập / sửa báo giá' : undefined}
|
||||
title={!readOnly ? 'Click để nhập / sửa số tiền' : undefined}
|
||||
>
|
||||
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
@ -1457,7 +1443,6 @@ function HangMucCard({
|
||||
supplierRowId={quoteEdit.supplier.id}
|
||||
supplierName={quoteEdit.supplier.supplierName}
|
||||
itemName={detail.noiDung}
|
||||
khoiLuong={detail.khoiLuongThiCong || detail.khoiLuongNganSach}
|
||||
existing={quoteEdit.existing}
|
||||
onClose={() => setQuoteEdit(null)}
|
||||
/>
|
||||
@ -1526,49 +1511,43 @@ function DetailDialog({ evaluationId, row, onClose }: { evaluationId: string; ro
|
||||
}
|
||||
|
||||
function QuoteDialog({
|
||||
evaluationId, detailId, supplierRowId, supplierName, itemName, khoiLuong, existing, onClose,
|
||||
evaluationId, detailId, supplierRowId, supplierName, itemName, existing, onClose,
|
||||
}: {
|
||||
evaluationId: string
|
||||
detailId: string
|
||||
supplierRowId: string
|
||||
supplierName: string
|
||||
itemName: string
|
||||
khoiLuong: number
|
||||
existing: PeQuote | null
|
||||
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ũ).
|
||||
// Session 20 turn 3: user yêu cầu "tạm thời chỉ cần nhập số tiền, không
|
||||
// cần 3 cột có VAT / không VAT / tổng". UI chỉ 1 input thanhTien; bgVat /
|
||||
// chuaVat / note vẫn gửi BE giữ schema (default 0 / empty cho row mới,
|
||||
// giữ giá trị cũ nếu existing).
|
||||
const [form, setForm] = useState({
|
||||
bgVat: existing?.bgVat ?? 0,
|
||||
chuaVat: existing?.chuaVat ?? 0,
|
||||
thanhTien: existing?.thanhTien ?? 0,
|
||||
note: existing?.note ?? '',
|
||||
})
|
||||
|
||||
const updateAndRecalc = (patch: Partial<typeof form>) => {
|
||||
const next = { ...form, ...patch }
|
||||
next.thanhTien = Number(next.chuaVat) * khoiLuong
|
||||
setForm(next)
|
||||
}
|
||||
|
||||
const mut = useMutation({
|
||||
mutationFn: async () =>
|
||||
api.post(`/purchase-evaluations/${evaluationId}/quotes`, {
|
||||
purchaseEvaluationDetailId: detailId,
|
||||
purchaseEvaluationSupplierId: supplierRowId,
|
||||
...form,
|
||||
isSelected: existing?.isSelected ?? false, // giữ nguyên trạng thái cũ, không expose UI
|
||||
bgVat: existing?.bgVat ?? 0,
|
||||
chuaVat: existing?.chuaVat ?? 0,
|
||||
thanhTien: form.thanhTien,
|
||||
note: existing?.note ?? '',
|
||||
isSelected: existing?.isSelected ?? false,
|
||||
}),
|
||||
onSuccess: () => { toast.success('Đã lưu báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onSuccess: () => { toast.success('Đã lưu số tiền.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
const del = useMutation({
|
||||
mutationFn: async () =>
|
||||
existing ? api.delete(`/purchase-evaluations/${evaluationId}/quotes/${existing.id}`) : Promise.resolve(),
|
||||
onSuccess: () => { toast.success('Đã xóa báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onSuccess: () => { toast.success('Đã xóa.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
@ -1585,25 +1564,27 @@ function QuoteDialog({
|
||||
<Button onClick={() => mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}</Button>
|
||||
</>}
|
||||
>
|
||||
{/* 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…'}
|
||||
{mut.isPending ? 'Đang lưu…' : 'Đang xóa…'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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><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>Thành tiền (auto)</Label><Input type="number" value={form.thanhTien} onChange={e => setForm({ ...form, thanhTien: Number(e.target.value) })} /></div>
|
||||
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p>
|
||||
<div>
|
||||
<Label>Số tiền</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={form.thanhTien}
|
||||
onChange={e => setForm({ thanhTien: Number(e.target.value) })}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@ -1334,9 +1334,7 @@ function HangMucCard({
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-left">Liên hệ</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-left">Điều khoản TT</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-left">File báo giá</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">ĐG chưa VAT</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">ĐG có VAT</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">Thành tiền</th>
|
||||
<th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th>
|
||||
{!readOnly && <th className="px-2 py-1.5"></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
@ -1374,18 +1372,6 @@ function HangMucCard({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
onClick={readOnly ? undefined : openQuote}
|
||||
className={cn('border-r border-slate-200 px-2 py-1.5 text-right font-mono', cellHover)}
|
||||
>
|
||||
{q ? fmtMoney(q.chuaVat) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
<td
|
||||
onClick={readOnly ? undefined : openQuote}
|
||||
className={cn('border-r border-slate-200 px-2 py-1.5 text-right font-mono', cellHover)}
|
||||
>
|
||||
{q ? fmtMoney(q.bgVat) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
<td
|
||||
onClick={readOnly ? undefined : openQuote}
|
||||
className={cn(
|
||||
@ -1393,7 +1379,7 @@ function HangMucCard({
|
||||
isWinner && 'text-emerald-700',
|
||||
cellHover,
|
||||
)}
|
||||
title={!readOnly ? 'Click để nhập / sửa báo giá' : undefined}
|
||||
title={!readOnly ? 'Click để nhập / sửa số tiền' : undefined}
|
||||
>
|
||||
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
@ -1457,7 +1443,6 @@ function HangMucCard({
|
||||
supplierRowId={quoteEdit.supplier.id}
|
||||
supplierName={quoteEdit.supplier.supplierName}
|
||||
itemName={detail.noiDung}
|
||||
khoiLuong={detail.khoiLuongThiCong || detail.khoiLuongNganSach}
|
||||
existing={quoteEdit.existing}
|
||||
onClose={() => setQuoteEdit(null)}
|
||||
/>
|
||||
@ -1526,49 +1511,43 @@ function DetailDialog({ evaluationId, row, onClose }: { evaluationId: string; ro
|
||||
}
|
||||
|
||||
function QuoteDialog({
|
||||
evaluationId, detailId, supplierRowId, supplierName, itemName, khoiLuong, existing, onClose,
|
||||
evaluationId, detailId, supplierRowId, supplierName, itemName, existing, onClose,
|
||||
}: {
|
||||
evaluationId: string
|
||||
detailId: string
|
||||
supplierRowId: string
|
||||
supplierName: string
|
||||
itemName: string
|
||||
khoiLuong: number
|
||||
existing: PeQuote | null
|
||||
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ũ).
|
||||
// Session 20 turn 3: user yêu cầu "tạm thời chỉ cần nhập số tiền, không
|
||||
// cần 3 cột có VAT / không VAT / tổng". UI chỉ 1 input thanhTien; bgVat /
|
||||
// chuaVat / note vẫn gửi BE giữ schema (default 0 / empty cho row mới,
|
||||
// giữ giá trị cũ nếu existing).
|
||||
const [form, setForm] = useState({
|
||||
bgVat: existing?.bgVat ?? 0,
|
||||
chuaVat: existing?.chuaVat ?? 0,
|
||||
thanhTien: existing?.thanhTien ?? 0,
|
||||
note: existing?.note ?? '',
|
||||
})
|
||||
|
||||
const updateAndRecalc = (patch: Partial<typeof form>) => {
|
||||
const next = { ...form, ...patch }
|
||||
next.thanhTien = Number(next.chuaVat) * khoiLuong
|
||||
setForm(next)
|
||||
}
|
||||
|
||||
const mut = useMutation({
|
||||
mutationFn: async () =>
|
||||
api.post(`/purchase-evaluations/${evaluationId}/quotes`, {
|
||||
purchaseEvaluationDetailId: detailId,
|
||||
purchaseEvaluationSupplierId: supplierRowId,
|
||||
...form,
|
||||
isSelected: existing?.isSelected ?? false, // giữ nguyên trạng thái cũ, không expose UI
|
||||
bgVat: existing?.bgVat ?? 0,
|
||||
chuaVat: existing?.chuaVat ?? 0,
|
||||
thanhTien: form.thanhTien,
|
||||
note: existing?.note ?? '',
|
||||
isSelected: existing?.isSelected ?? false,
|
||||
}),
|
||||
onSuccess: () => { toast.success('Đã lưu báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onSuccess: () => { toast.success('Đã lưu số tiền.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
const del = useMutation({
|
||||
mutationFn: async () =>
|
||||
existing ? api.delete(`/purchase-evaluations/${evaluationId}/quotes/${existing.id}`) : Promise.resolve(),
|
||||
onSuccess: () => { toast.success('Đã xóa báo giá.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onSuccess: () => { toast.success('Đã xóa.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
@ -1585,25 +1564,27 @@ function QuoteDialog({
|
||||
<Button onClick={() => mut.mutate()} disabled={isSaving}>{mut.isPending ? 'Đang lưu…' : 'Lưu'}</Button>
|
||||
</>}
|
||||
>
|
||||
{/* 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…'}
|
||||
{mut.isPending ? 'Đang lưu…' : 'Đang xóa…'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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><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>Thành tiền (auto)</Label><Input type="number" value={form.thanhTien} onChange={e => setForm({ ...form, thanhTien: Number(e.target.value) })} /></div>
|
||||
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p>
|
||||
<div>
|
||||
<Label>Số tiền</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={form.thanhTien}
|
||||
onChange={e => setForm({ thanhTien: Number(e.target.value) })}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user