[CLAUDE] FE-PE: NCC cell button visual + Hạng mục header gộp 1 ô Ngân sách + DetailDialog rút gọn
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m0s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m0s
User Session 20 turn 5: 2 yêu cầu UX rõ ràng chỗ nhập tiền.
FE-only mirror fe-admin + fe-user.
1. NCC grid cell "Số tiền" → button visual rõ ràng cho user biết là chỗ nhập:
- Trước: <td onClick> trông như text cell (chỉ hover bg → user không
biết click được)
- Sau: <button> trong td:
* Empty (chưa nhập): border-dashed border-slate-300 bg-slate-50
text-slate-400 + label "+ Nhập số tiền" + hover brand
* Filled: border-solid border-slate-300 bg-white font-semibold + số tiền
+ suffix " đ" + hover brand
* Winner: border-emerald-300 bg-emerald-50 text-emerald-700
- Read-only mode: hiển thị <div> số tiền (không button)
- Drop `cellHover` var không còn dùng
2. Hạng mục header gộp 3 stat (KL / ĐG ngân sách / Thành tiền NS) → 1 ô
"Số tiền ngân sách" lớn hơn (text-base font-semibold + suffix đ):
- Trước: 3 columns hiển thị KL + ĐG + TT (kỹ thuật, user không cần thấy)
- Sau: 1 column "Số tiền ngân sách: X đ" — duy nhất số quan trọng
- "NS link Δ" comparison column giữ (nếu có Budget link FYI)
3. DetailDialog rút gọn 11 input → 3 input chính:
- Trước: groupCode + groupName + itemCode + noiDung + donViTinh +
KL ngân sách + KL thi công + đơn giá + thành tiền auto + ghi chú (10
input grid 3 cols)
- Sau: Tên hạng mục (noiDung) + Số tiền ngân sách (VND format + suffix
đ + hint) + Ghi chú (3 input vertical)
- Helper setBudgetAmount: user nhập 1 số → set cả donGia + thanhTien
(KL=1 ngầm). BE giữ schema 3 field backward compat.
- Form state default đổi: groupCode="01" groupName="Hạng mục chính"
donViTinh="gói" KL=1 (consistent với Chunk A BE seed)
- Drop updateAndRecalc helper (không còn auto-calc KL × ĐG)
KHÔNG đụng schema BE.
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:
@ -1324,16 +1324,11 @@ function HangMucCard({
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-start gap-4 text-right text-xs">
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-slate-400">KL</div>
|
||||
<div className="font-mono">{detail.khoiLuongNganSach}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-slate-400">ĐG ngân sách</div>
|
||||
<div className="font-mono">{fmtMoney(detail.donGiaNganSach)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] uppercase text-slate-400">Thành tiền NS</div>
|
||||
<div className="font-mono font-semibold">{fmtMoney(detail.thanhTienNganSach)}</div>
|
||||
<div className="text-[10px] uppercase text-slate-400">Số tiền ngân sách</div>
|
||||
<div className="font-mono text-base font-semibold text-slate-900">
|
||||
{fmtMoney(detail.thanhTienNganSach)}
|
||||
<span className="ml-1 text-xs font-normal text-slate-500">đ</span>
|
||||
</div>
|
||||
</div>
|
||||
{showBudgetCol && bgValue != null && (
|
||||
<div className="border-l border-slate-200 pl-3">
|
||||
@ -1409,7 +1404,6 @@ function HangMucCard({
|
||||
const hasQuotes = ev.details.some(dd => dd.quotes.some(qq => qq.purchaseEvaluationSupplierId === s.id))
|
||||
const canDelete = !isWinner && !hasQuotes
|
||||
const openQuote = () => setQuoteEdit({ supplier: s, existing: q })
|
||||
const cellHover = !readOnly && 'cursor-pointer hover:bg-brand-50'
|
||||
return (
|
||||
<tr key={s.id} className={cn('align-top', isWinner && 'bg-emerald-50/60')}>
|
||||
<td className="border-r border-slate-200 px-2 py-1.5">
|
||||
@ -1438,16 +1432,28 @@ 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 font-semibold',
|
||||
isWinner && 'text-emerald-700',
|
||||
cellHover,
|
||||
<td className="border-r border-slate-200 px-2 py-1.5">
|
||||
{!readOnly ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openQuote}
|
||||
className={cn(
|
||||
'w-full rounded border px-2 py-1 text-right font-mono text-[11px] transition',
|
||||
q
|
||||
? isWinner
|
||||
? 'border-emerald-300 bg-emerald-50 font-semibold text-emerald-700 hover:bg-emerald-100'
|
||||
: 'border-slate-300 bg-white font-semibold hover:border-brand-300 hover:bg-brand-50'
|
||||
: 'border-dashed border-slate-300 bg-slate-50 text-[10px] text-slate-400 hover:border-brand-400 hover:bg-brand-50 hover:text-brand-600',
|
||||
)}
|
||||
title="Click để nhập / sửa số tiền"
|
||||
>
|
||||
{q ? `${fmtMoney(q.thanhTien)} đ` : '+ Nhập số tiền'}
|
||||
</button>
|
||||
) : (
|
||||
<div className={cn('text-right font-mono font-semibold', isWinner && 'text-emerald-700')}>
|
||||
{q ? `${fmtMoney(q.thanhTien)} đ` : <span className="text-slate-300">—</span>}
|
||||
</div>
|
||||
)}
|
||||
title={!readOnly ? 'Click để nhập / sửa số tiền' : undefined}
|
||||
>
|
||||
{q ? fmtMoney(q.thanhTien) : <span className="text-slate-300">—</span>}
|
||||
</td>
|
||||
{!readOnly && (
|
||||
<td className="px-2 py-1.5">
|
||||
@ -1519,14 +1525,18 @@ function HangMucCard({
|
||||
|
||||
function DetailDialog({ evaluationId, row, onClose }: { evaluationId: string; row: PeDetailRow | null; onClose: () => void }) {
|
||||
const qc = useQueryClient()
|
||||
// Session 20 turn 5: user yêu cầu rút gọn — chỉ Tên hạng mục + Số tiền
|
||||
// ngân sách (VND format) + Ghi chú. Các field schema khác (groupCode/
|
||||
// groupName/itemCode/donViTinh/khoiLuongs/donGia) giữ default cho BE
|
||||
// schema backward compat — KHÔNG expose UI cho user.
|
||||
const [form, setForm] = useState({
|
||||
groupCode: row?.groupCode ?? 'A.I',
|
||||
groupName: row?.groupName ?? '',
|
||||
groupCode: row?.groupCode ?? '01',
|
||||
groupName: row?.groupName ?? 'Hạng mục chính',
|
||||
itemCode: row?.itemCode ?? '',
|
||||
noiDung: row?.noiDung ?? '',
|
||||
donViTinh: row?.donViTinh ?? '',
|
||||
khoiLuongNganSach: row?.khoiLuongNganSach ?? 0,
|
||||
khoiLuongThiCong: row?.khoiLuongThiCong ?? 0,
|
||||
donViTinh: row?.donViTinh ?? 'gói',
|
||||
khoiLuongNganSach: row?.khoiLuongNganSach ?? 1,
|
||||
khoiLuongThiCong: row?.khoiLuongThiCong ?? 1,
|
||||
donGiaNganSach: row?.donGiaNganSach ?? 0,
|
||||
thanhTienNganSach: row?.thanhTienNganSach ?? 0,
|
||||
ghiChu: row?.ghiChu ?? '',
|
||||
@ -1540,11 +1550,10 @@ function DetailDialog({ evaluationId, row, onClose }: { evaluationId: string; ro
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
const updateAndRecalc = (patch: Partial<typeof form>) => {
|
||||
const next = { ...form, ...patch }
|
||||
// Auto-compute ThanhTien = KL ngân sách × ĐG ngân sách
|
||||
next.thanhTienNganSach = Number(next.khoiLuongNganSach) * Number(next.donGiaNganSach)
|
||||
setForm(next)
|
||||
// Sync ngân sách: user nhập "Số tiền ngân sách" → set cả donGia + thanhTien
|
||||
// (KL = 1 ngầm). BE giữ schema 3 field.
|
||||
const setBudgetAmount = (n: number) => {
|
||||
setForm({ ...form, donGiaNganSach: n, thanhTienNganSach: n })
|
||||
}
|
||||
|
||||
return (
|
||||
@ -1552,24 +1561,38 @@ function DetailDialog({ evaluationId, row, onClose }: { evaluationId: string; ro
|
||||
open
|
||||
onClose={onClose}
|
||||
title={(row ? 'Sửa' : 'Thêm') + ' hạng mục'}
|
||||
size="lg"
|
||||
footer={<>
|
||||
<Button variant="ghost" onClick={onClose}>Hủy</Button>
|
||||
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>{row ? 'Lưu' : 'Thêm'}</Button>
|
||||
</>}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div><Label>Nhóm (A.I/A.II...)</Label><Input value={form.groupCode} onChange={e => setForm({ ...form, groupCode: e.target.value })} /></div>
|
||||
<div className="col-span-2"><Label>Tên nhóm</Label><Input value={form.groupName} onChange={e => setForm({ ...form, groupName: e.target.value })} placeholder="Bê tông / Phụ gia..." /></div>
|
||||
<div><Label>Mã (tùy chọn)</Label><Input value={form.itemCode} onChange={e => setForm({ ...form, itemCode: e.target.value })} /></div>
|
||||
<div className="col-span-2"><Label>Nội dung</Label><Input value={form.noiDung} onChange={e => setForm({ ...form, noiDung: e.target.value })} /></div>
|
||||
<div><Label>ĐVT</Label><Input value={form.donViTinh} onChange={e => setForm({ ...form, donViTinh: e.target.value })} /></div>
|
||||
<div><Label>KL ngân sách</Label><Input type="number" value={form.khoiLuongNganSach} onChange={e => updateAndRecalc({ khoiLuongNganSach: Number(e.target.value) })} /></div>
|
||||
<div><Label>KL thi công</Label><Input type="number" value={form.khoiLuongThiCong} onChange={e => setForm({ ...form, khoiLuongThiCong: Number(e.target.value) })} /></div>
|
||||
<div><Label>Đơn giá ngân sách</Label><Input type="number" value={form.donGiaNganSach} onChange={e => updateAndRecalc({ donGiaNganSach: Number(e.target.value) })} /></div>
|
||||
<div className="col-span-2"><Label>Thành tiền ngân sách (auto)</Label><Input type="number" value={form.thanhTienNganSach} onChange={e => setForm({ ...form, thanhTienNganSach: Number(e.target.value) })} /></div>
|
||||
<div className="col-span-3"><Label>Ghi chú</Label><Input value={form.ghiChu} onChange={e => setForm({ ...form, ghiChu: e.target.value })} /></div>
|
||||
<div>
|
||||
<Label>Tên hạng mục</Label>
|
||||
<Input
|
||||
value={form.noiDung}
|
||||
onChange={e => setForm({ ...form, noiDung: e.target.value })}
|
||||
placeholder="vd Cung cấp bê tông M250"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Số tiền ngân sách</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={formatVndInput(form.thanhTienNganSach)}
|
||||
onChange={e => setBudgetAmount(parseVnd(e.target.value))}
|
||||
placeholder="0"
|
||||
className="pr-12 font-mono text-right"
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[12px] font-medium text-slate-500">đ</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-slate-500">VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Ghi chú</Label>
|
||||
<Input value={form.ghiChu} onChange={e => setForm({ ...form, ghiChu: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user