[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

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:
pqhuy1987
2026-05-11 10:41:48 +07:00
parent c4ece8071f
commit e03314e2e7
2 changed files with 48 additions and 86 deletions

View File

@ -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 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á 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>
)