[CLAUDE] FE-PE: NCC table SĐT+Email rõ ràng + validate format + Số tiền format VND
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m0s

User Session 20 turn 4: NCC info cơ bản (SĐT + Email), ràng buộc format,
input tiền VND format dấu chấm 1.000.000.

FE-only mirror fe-admin + fe-user.

1. Helpers cấu hình ở top file (gần fmtMoney):
   - parseVnd(s): strip non-digit → number (0 nếu rỗng)
   - formatVndInput(n): n.toLocaleString('vi-VN') hoặc '' khi n=0
   - PHONE_RE /^0\d{9,10}$/ — VN bắt đầu 0, 10-11 digits sau strip space/dash/dot
   - EMAIL_RE /^[^\s@]+@[^\s@]+\.[^\s@]+$/
   - isValidPhone(s) / isValidEmail(s) — empty OK (optional)

2. NCC inline table HangMucCard — bỏ cột "Liên hệ" tổng hợp (ContactName +
   Phone + Email gộp), thay bằng 2 cột riêng:
     Trước: NCC | Liên hệ | Điều khoản TT | File báo giá | Số tiền | Action
     Sau:   NCC | SĐT | Email | Điều khoản TT | File báo giá | Số tiền | Action
   SĐT cell font-mono (đọc số rõ); Email cell truncate + title hover full.

3. AddSupplierDialog + EditSupplierDialog — validate phone + email:
   - Input type="tel" / type="email" + inputMode + placeholder example
   - border-red-300 khi invalid + dòng error text [10px] mt-0.5 red-600
   - Disable nút Lưu/Thêm khi hasError = phoneError || emailError
   - ContactName + DisplayName + Note + PaymentTermText giữ (optional fields,
     backward compat — user nói "cơ bản thôi" áp cho display, dialog giữ
     đầy đủ field tránh churn schema)

4. QuoteDialog "Số tiền" input format VND:
   - type="text" inputMode="numeric" thay vì type="number" (để format dấu
     chấm ngàn không phá bởi browser number parser)
   - value={formatVndInput(form.thanhTien)} → display "1.000.000"
   - onChange={parseVnd(e.target.value)} → strip non-digit → state raw number
   - Suffix span "đ" tuyệt đối inset-y-0 right-3 pointer-events-none
   - Hint dưới input: "VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)"
   - Class font-mono text-right pr-12 (chừa chỗ suffix)

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:48:44 +07:00
parent e03314e2e7
commit 17c5f14e20
2 changed files with 180 additions and 34 deletions

View File

@ -42,6 +42,19 @@ import type { Paged, Supplier } from '@/types/master'
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
// Session 20 turn 4 — input helpers cho NCC/Quote inline form.
// VND format dùng convention VN dấu chấm ngàn (1.000.000). Strip non-digit
// khi parse user input → number. Empty/0 → empty string để placeholder hiện.
const parseVnd = (s: string): number => Number(s.replace(/[^\d]/g, '')) || 0
const formatVndInput = (n: number): string => (n > 0 ? n.toLocaleString('vi-VN') : '')
// Validation cơ bản FE — empty OK (optional fields). BE FluentValidation
// chưa enforce, FE check để user nhập sai biết ngay.
const PHONE_RE = /^0\d{9,10}$/ // VN: bắt đầu 0, 10-11 digits sau khi strip space/dash/dot
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const isValidPhone = (s: string): boolean => !s || PHONE_RE.test(s.replace(/[\s\-.]/g, ''))
const isValidEmail = (s: string): boolean => !s || EMAIL_RE.test(s)
// Main detail content — flat render 3 section không tabs. // Main detail content — flat render 3 section không tabs.
// Tên giữ PeDetailTabs để không break callsite (rename gây churn). // Tên giữ PeDetailTabs để không break callsite (rename gây churn).
// //
@ -1062,6 +1075,10 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
paymentTermText: '', paymentTermText: '',
note: '', note: '',
}) })
const phoneError = !isValidPhone(form.contactPhone) ? 'SĐT không hợp lệ (cần 10-11 số bắt đầu 0)' : ''
const emailError = !isValidEmail(form.contactEmail) ? 'Email không hợp lệ' : ''
const hasError = !!(phoneError || emailError)
const mut = useMutation({ const mut = useMutation({
mutationFn: async () => api.post(`/purchase-evaluations/${evaluationId}/suppliers`, form), mutationFn: async () => api.post(`/purchase-evaluations/${evaluationId}/suppliers`, form),
onSuccess: () => { toast.success('Đã thêm NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() }, onSuccess: () => { toast.success('Đã thêm NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
@ -1075,7 +1092,7 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
title="Thêm NCC vào phiếu" title="Thêm NCC vào phiếu"
footer={<> footer={<>
<Button variant="ghost" onClick={onClose}>Hủy</Button> <Button variant="ghost" onClick={onClose}>Hủy</Button>
<Button onClick={() => mut.mutate()} disabled={!form.supplierId || mut.isPending}>Thêm</Button> <Button onClick={() => mut.mutate()} disabled={!form.supplierId || hasError || mut.isPending}>Thêm</Button>
</>} </>}
> >
<div className="space-y-3"> <div className="space-y-3">
@ -1092,8 +1109,29 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
<div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" /></div> <div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" /></div>
<div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" /></div> <div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" /></div>
<div><Label>Người liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div> <div><Label>Người liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div>
<div><Label>Điện thoại</Label><Input value={form.contactPhone} onChange={e => setForm({ ...form, contactPhone: e.target.value })} /></div> <div>
<div className="col-span-2"><Label>Email</Label><Input value={form.contactEmail} onChange={e => setForm({ ...form, contactEmail: e.target.value })} /></div> <Label>Điện thoại</Label>
<Input
type="tel"
inputMode="tel"
value={form.contactPhone}
onChange={e => setForm({ ...form, contactPhone: e.target.value })}
placeholder="0987654321"
className={phoneError ? 'border-red-300' : undefined}
/>
{phoneError && <p className="mt-0.5 text-[10px] text-red-600">{phoneError}</p>}
</div>
<div className="col-span-2">
<Label>Email</Label>
<Input
type="email"
value={form.contactEmail}
onChange={e => setForm({ ...form, contactEmail: e.target.value })}
placeholder="name@example.com"
className={emailError ? 'border-red-300' : undefined}
/>
{emailError && <p className="mt-0.5 text-[10px] text-red-600">{emailError}</p>}
</div>
<div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." /></div> <div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." /></div>
</div> </div>
</div> </div>
@ -1112,6 +1150,10 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri
paymentTermText: row.paymentTermText ?? '', paymentTermText: row.paymentTermText ?? '',
note: row.note ?? '', note: row.note ?? '',
}) })
const phoneError = !isValidPhone(form.contactPhone) ? 'SĐT không hợp lệ (cần 10-11 số bắt đầu 0)' : ''
const emailError = !isValidEmail(form.contactEmail) ? 'Email không hợp lệ' : ''
const hasError = !!(phoneError || emailError)
const mut = useMutation({ const mut = useMutation({
mutationFn: async () => api.put(`/purchase-evaluations/${evaluationId}/suppliers/${row.id}`, form), mutationFn: async () => api.put(`/purchase-evaluations/${evaluationId}/suppliers/${row.id}`, form),
onSuccess: () => { toast.success('Đã cập nhật.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() }, onSuccess: () => { toast.success('Đã cập nhật.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
@ -1124,15 +1166,36 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri
title={`Sửa NCC — ${row.supplierName}`} title={`Sửa NCC — ${row.supplierName}`}
footer={<> footer={<>
<Button variant="ghost" onClick={onClose}>Hủy</Button> <Button variant="ghost" onClick={onClose}>Hủy</Button>
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Lưu</Button> <Button onClick={() => mut.mutate()} disabled={hasError || mut.isPending}>Lưu</Button>
</>} </>}
> >
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} /></div> <div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} /></div>
<div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} /></div> <div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} /></div>
<div><Label>Liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div> <div><Label>Liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div>
<div><Label>Điện thoại</Label><Input value={form.contactPhone} onChange={e => setForm({ ...form, contactPhone: e.target.value })} /></div> <div>
<div className="col-span-2"><Label>Email</Label><Input value={form.contactEmail} onChange={e => setForm({ ...form, contactEmail: e.target.value })} /></div> <Label>Điện thoại</Label>
<Input
type="tel"
inputMode="tel"
value={form.contactPhone}
onChange={e => setForm({ ...form, contactPhone: e.target.value })}
placeholder="0987654321"
className={phoneError ? 'border-red-300' : undefined}
/>
{phoneError && <p className="mt-0.5 text-[10px] text-red-600">{phoneError}</p>}
</div>
<div className="col-span-2">
<Label>Email</Label>
<Input
type="email"
value={form.contactEmail}
onChange={e => setForm({ ...form, contactEmail: e.target.value })}
placeholder="name@example.com"
className={emailError ? 'border-red-300' : undefined}
/>
{emailError && <p className="mt-0.5 text-[10px] text-red-600">{emailError}</p>}
</div>
<div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div> <div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
</div> </div>
</Dialog> </Dialog>
@ -1331,7 +1394,8 @@ function HangMucCard({
<thead className="bg-slate-50 text-slate-600"> <thead className="bg-slate-50 text-slate-600">
<tr> <tr>
<th className="border-r border-slate-200 px-2 py-1.5 text-left">NCC</th> <th className="border-r border-slate-200 px-2 py-1.5 text-left">NCC</th>
<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">SĐT</th>
<th className="border-r border-slate-200 px-2 py-1.5 text-left">Email</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">Đ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-left">File báo giá</th>
<th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th> <th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th>
@ -1355,11 +1419,13 @@ function HangMucCard({
{s.displayName && <div className="text-[10px] text-slate-500">{s.displayName}</div>} {s.displayName && <div className="text-[10px] text-slate-500">{s.displayName}</div>}
{s.note && <div className="text-[10px] text-amber-600">{s.note}</div>} {s.note && <div className="text-[10px] text-amber-600">{s.note}</div>}
</td> </td>
<td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600 font-mono">
{s.contactPhone || <span className="text-slate-300"></span>}
</td>
<td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600"> <td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600">
{s.contactName && <div>{s.contactName}</div>} {s.contactEmail
{s.contactPhone && <div>{s.contactPhone}</div>} ? <span className="truncate" title={s.contactEmail}>{s.contactEmail}</span>
{s.contactEmail && <div className="truncate" title={s.contactEmail}>{s.contactEmail}</div>} : <span className="text-slate-300"></span>}
{!s.contactName && !s.contactPhone && !s.contactEmail && <span className="text-slate-300"></span>}
</td> </td>
<td className="border-r border-slate-200 px-2 py-1.5 text-[11px]"> <td className="border-r border-slate-200 px-2 py-1.5 text-[11px]">
{s.paymentTermText ?? <span className="text-slate-300"></span>} {s.paymentTermText ?? <span className="text-slate-300"></span>}
@ -1578,12 +1644,19 @@ function QuoteDialog({
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p> <p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p>
<div> <div>
<Label>Số tiền</Label> <Label>Số tiền</Label>
<Input <div className="relative">
type="number" <Input
value={form.thanhTien} type="text"
onChange={e => setForm({ thanhTien: Number(e.target.value) })} inputMode="numeric"
autoFocus value={formatVndInput(form.thanhTien)}
/> onChange={e => setForm({ thanhTien: parseVnd(e.target.value) })}
placeholder="0"
autoFocus
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>
</div> </div>
</Dialog> </Dialog>

View File

@ -42,6 +42,19 @@ import type { Paged, Supplier } from '@/types/master'
const fmtMoney = (v: number) => v.toLocaleString('vi-VN') const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
// Session 20 turn 4 — input helpers cho NCC/Quote inline form.
// VND format dùng convention VN dấu chấm ngàn (1.000.000). Strip non-digit
// khi parse user input → number. Empty/0 → empty string để placeholder hiện.
const parseVnd = (s: string): number => Number(s.replace(/[^\d]/g, '')) || 0
const formatVndInput = (n: number): string => (n > 0 ? n.toLocaleString('vi-VN') : '')
// Validation cơ bản FE — empty OK (optional fields). BE FluentValidation
// chưa enforce, FE check để user nhập sai biết ngay.
const PHONE_RE = /^0\d{9,10}$/ // VN: bắt đầu 0, 10-11 digits sau khi strip space/dash/dot
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const isValidPhone = (s: string): boolean => !s || PHONE_RE.test(s.replace(/[\s\-.]/g, ''))
const isValidEmail = (s: string): boolean => !s || EMAIL_RE.test(s)
// Main detail content — flat render 3 section không tabs. // Main detail content — flat render 3 section không tabs.
// Tên giữ PeDetailTabs để không break callsite (rename gây churn). // Tên giữ PeDetailTabs để không break callsite (rename gây churn).
// //
@ -1062,6 +1075,10 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
paymentTermText: '', paymentTermText: '',
note: '', note: '',
}) })
const phoneError = !isValidPhone(form.contactPhone) ? 'SĐT không hợp lệ (cần 10-11 số bắt đầu 0)' : ''
const emailError = !isValidEmail(form.contactEmail) ? 'Email không hợp lệ' : ''
const hasError = !!(phoneError || emailError)
const mut = useMutation({ const mut = useMutation({
mutationFn: async () => api.post(`/purchase-evaluations/${evaluationId}/suppliers`, form), mutationFn: async () => api.post(`/purchase-evaluations/${evaluationId}/suppliers`, form),
onSuccess: () => { toast.success('Đã thêm NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() }, onSuccess: () => { toast.success('Đã thêm NCC.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
@ -1075,7 +1092,7 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
title="Thêm NCC vào phiếu" title="Thêm NCC vào phiếu"
footer={<> footer={<>
<Button variant="ghost" onClick={onClose}>Hủy</Button> <Button variant="ghost" onClick={onClose}>Hủy</Button>
<Button onClick={() => mut.mutate()} disabled={!form.supplierId || mut.isPending}>Thêm</Button> <Button onClick={() => mut.mutate()} disabled={!form.supplierId || hasError || mut.isPending}>Thêm</Button>
</>} </>}
> >
<div className="space-y-3"> <div className="space-y-3">
@ -1092,8 +1109,29 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
<div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" /></div> <div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" /></div>
<div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" /></div> <div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" /></div>
<div><Label>Người liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div> <div><Label>Người liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div>
<div><Label>Điện thoại</Label><Input value={form.contactPhone} onChange={e => setForm({ ...form, contactPhone: e.target.value })} /></div> <div>
<div className="col-span-2"><Label>Email</Label><Input value={form.contactEmail} onChange={e => setForm({ ...form, contactEmail: e.target.value })} /></div> <Label>Điện thoại</Label>
<Input
type="tel"
inputMode="tel"
value={form.contactPhone}
onChange={e => setForm({ ...form, contactPhone: e.target.value })}
placeholder="0987654321"
className={phoneError ? 'border-red-300' : undefined}
/>
{phoneError && <p className="mt-0.5 text-[10px] text-red-600">{phoneError}</p>}
</div>
<div className="col-span-2">
<Label>Email</Label>
<Input
type="email"
value={form.contactEmail}
onChange={e => setForm({ ...form, contactEmail: e.target.value })}
placeholder="name@example.com"
className={emailError ? 'border-red-300' : undefined}
/>
{emailError && <p className="mt-0.5 text-[10px] text-red-600">{emailError}</p>}
</div>
<div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." /></div> <div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." /></div>
</div> </div>
</div> </div>
@ -1112,6 +1150,10 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri
paymentTermText: row.paymentTermText ?? '', paymentTermText: row.paymentTermText ?? '',
note: row.note ?? '', note: row.note ?? '',
}) })
const phoneError = !isValidPhone(form.contactPhone) ? 'SĐT không hợp lệ (cần 10-11 số bắt đầu 0)' : ''
const emailError = !isValidEmail(form.contactEmail) ? 'Email không hợp lệ' : ''
const hasError = !!(phoneError || emailError)
const mut = useMutation({ const mut = useMutation({
mutationFn: async () => api.put(`/purchase-evaluations/${evaluationId}/suppliers/${row.id}`, form), mutationFn: async () => api.put(`/purchase-evaluations/${evaluationId}/suppliers/${row.id}`, form),
onSuccess: () => { toast.success('Đã cập nhật.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() }, onSuccess: () => { toast.success('Đã cập nhật.'); qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] }); onClose() },
@ -1124,15 +1166,36 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri
title={`Sửa NCC — ${row.supplierName}`} title={`Sửa NCC — ${row.supplierName}`}
footer={<> footer={<>
<Button variant="ghost" onClick={onClose}>Hủy</Button> <Button variant="ghost" onClick={onClose}>Hủy</Button>
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Lưu</Button> <Button onClick={() => mut.mutate()} disabled={hasError || mut.isPending}>Lưu</Button>
</>} </>}
> >
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} /></div> <div><Label>Hiển thị</Label><Input value={form.displayName} onChange={e => setForm({ ...form, displayName: e.target.value })} /></div>
<div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} /></div> <div><Label>Điều khoản TT</Label><Input value={form.paymentTermText} onChange={e => setForm({ ...form, paymentTermText: e.target.value })} /></div>
<div><Label>Liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div> <div><Label>Liên hệ</Label><Input value={form.contactName} onChange={e => setForm({ ...form, contactName: e.target.value })} /></div>
<div><Label>Điện thoại</Label><Input value={form.contactPhone} onChange={e => setForm({ ...form, contactPhone: e.target.value })} /></div> <div>
<div className="col-span-2"><Label>Email</Label><Input value={form.contactEmail} onChange={e => setForm({ ...form, contactEmail: e.target.value })} /></div> <Label>Điện thoại</Label>
<Input
type="tel"
inputMode="tel"
value={form.contactPhone}
onChange={e => setForm({ ...form, contactPhone: e.target.value })}
placeholder="0987654321"
className={phoneError ? 'border-red-300' : undefined}
/>
{phoneError && <p className="mt-0.5 text-[10px] text-red-600">{phoneError}</p>}
</div>
<div className="col-span-2">
<Label>Email</Label>
<Input
type="email"
value={form.contactEmail}
onChange={e => setForm({ ...form, contactEmail: e.target.value })}
placeholder="name@example.com"
className={emailError ? 'border-red-300' : undefined}
/>
{emailError && <p className="mt-0.5 text-[10px] text-red-600">{emailError}</p>}
</div>
<div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div> <div className="col-span-2"><Label>Ghi chú</Label><Input value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /></div>
</div> </div>
</Dialog> </Dialog>
@ -1331,7 +1394,8 @@ function HangMucCard({
<thead className="bg-slate-50 text-slate-600"> <thead className="bg-slate-50 text-slate-600">
<tr> <tr>
<th className="border-r border-slate-200 px-2 py-1.5 text-left">NCC</th> <th className="border-r border-slate-200 px-2 py-1.5 text-left">NCC</th>
<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">SĐT</th>
<th className="border-r border-slate-200 px-2 py-1.5 text-left">Email</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">Đ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-left">File báo giá</th>
<th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th> <th className="border-r border-slate-200 px-2 py-1.5 text-right">Số tiền</th>
@ -1355,11 +1419,13 @@ function HangMucCard({
{s.displayName && <div className="text-[10px] text-slate-500">{s.displayName}</div>} {s.displayName && <div className="text-[10px] text-slate-500">{s.displayName}</div>}
{s.note && <div className="text-[10px] text-amber-600">{s.note}</div>} {s.note && <div className="text-[10px] text-amber-600">{s.note}</div>}
</td> </td>
<td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600 font-mono">
{s.contactPhone || <span className="text-slate-300"></span>}
</td>
<td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600"> <td className="border-r border-slate-200 px-2 py-1.5 text-[11px] text-slate-600">
{s.contactName && <div>{s.contactName}</div>} {s.contactEmail
{s.contactPhone && <div>{s.contactPhone}</div>} ? <span className="truncate" title={s.contactEmail}>{s.contactEmail}</span>
{s.contactEmail && <div className="truncate" title={s.contactEmail}>{s.contactEmail}</div>} : <span className="text-slate-300"></span>}
{!s.contactName && !s.contactPhone && !s.contactEmail && <span className="text-slate-300"></span>}
</td> </td>
<td className="border-r border-slate-200 px-2 py-1.5 text-[11px]"> <td className="border-r border-slate-200 px-2 py-1.5 text-[11px]">
{s.paymentTermText ?? <span className="text-slate-300"></span>} {s.paymentTermText ?? <span className="text-slate-300"></span>}
@ -1578,12 +1644,19 @@ function QuoteDialog({
<p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p> <p className="text-sm text-slate-500">Hạng mục: <strong>{itemName}</strong></p>
<div> <div>
<Label>Số tiền</Label> <Label>Số tiền</Label>
<Input <div className="relative">
type="number" <Input
value={form.thanhTien} type="text"
onChange={e => setForm({ thanhTien: Number(e.target.value) })} inputMode="numeric"
autoFocus value={formatVndInput(form.thanhTien)}
/> onChange={e => setForm({ thanhTien: parseVnd(e.target.value) })}
placeholder="0"
autoFocus
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>
</div> </div>
</Dialog> </Dialog>