[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
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:
@ -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>
|
||||||
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
value={form.thanhTien}
|
inputMode="numeric"
|
||||||
onChange={e => setForm({ thanhTien: Number(e.target.value) })}
|
value={formatVndInput(form.thanhTien)}
|
||||||
|
onChange={e => setForm({ thanhTien: parseVnd(e.target.value) })}
|
||||||
|
placeholder="0"
|
||||||
autoFocus
|
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
value={form.thanhTien}
|
inputMode="numeric"
|
||||||
onChange={e => setForm({ thanhTien: Number(e.target.value) })}
|
value={formatVndInput(form.thanhTien)}
|
||||||
|
onChange={e => setForm({ thanhTien: parseVnd(e.target.value) })}
|
||||||
|
placeholder="0"
|
||||||
autoFocus
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user