diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index 4af4413..c853b99 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -42,6 +42,19 @@ import type { Paged, Supplier } from '@/types/master' 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. // Tên giữ PeDetailTabs để không break callsite (rename gây churn). // @@ -1062,6 +1075,10 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on paymentTermText: '', 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({ mutationFn: async () => api.post(`/purchase-evaluations/${evaluationId}/suppliers`, form), 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" footer={<> - + } >
@@ -1092,8 +1109,29 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" />
setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" />
setForm({ ...form, contactName: e.target.value })} />
-
setForm({ ...form, contactPhone: e.target.value })} />
-
setForm({ ...form, contactEmail: e.target.value })} />
+
+ + setForm({ ...form, contactPhone: e.target.value })} + placeholder="0987654321" + className={phoneError ? 'border-red-300' : undefined} + /> + {phoneError &&

{phoneError}

} +
+
+ + setForm({ ...form, contactEmail: e.target.value })} + placeholder="name@example.com" + className={emailError ? 'border-red-300' : undefined} + /> + {emailError &&

{emailError}

} +
setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." />
@@ -1112,6 +1150,10 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri paymentTermText: row.paymentTermText ?? '', 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({ 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() }, @@ -1124,15 +1166,36 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri title={`Sửa NCC — ${row.supplierName}`} footer={<> - + } >
setForm({ ...form, displayName: e.target.value })} />
setForm({ ...form, paymentTermText: e.target.value })} />
setForm({ ...form, contactName: e.target.value })} />
-
setForm({ ...form, contactPhone: e.target.value })} />
-
setForm({ ...form, contactEmail: e.target.value })} />
+
+ + setForm({ ...form, contactPhone: e.target.value })} + placeholder="0987654321" + className={phoneError ? 'border-red-300' : undefined} + /> + {phoneError &&

{phoneError}

} +
+
+ + setForm({ ...form, contactEmail: e.target.value })} + placeholder="name@example.com" + className={emailError ? 'border-red-300' : undefined} + /> + {emailError &&

{emailError}

} +
setForm({ ...form, note: e.target.value })} />
@@ -1331,7 +1394,8 @@ function HangMucCard({ NCC - Liên hệ + SĐT + Email Điều khoản TT File báo giá Số tiền @@ -1355,11 +1419,13 @@ function HangMucCard({ {s.displayName &&
{s.displayName}
} {s.note &&
{s.note}
} + + {s.contactPhone || } + - {s.contactName &&
{s.contactName}
} - {s.contactPhone &&
{s.contactPhone}
} - {s.contactEmail &&
{s.contactEmail}
} - {!s.contactName && !s.contactPhone && !s.contactEmail && } + {s.contactEmail + ? {s.contactEmail} + : } {s.paymentTermText ?? } @@ -1578,12 +1644,19 @@ function QuoteDialog({

Hạng mục: {itemName}

- setForm({ thanhTien: Number(e.target.value) })} - autoFocus - /> +
+ setForm({ thanhTien: parseVnd(e.target.value) })} + placeholder="0" + autoFocus + className="pr-12 font-mono text-right" + /> + đ +
+

VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)

diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index 4af4413..c853b99 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -42,6 +42,19 @@ import type { Paged, Supplier } from '@/types/master' 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. // Tên giữ PeDetailTabs để không break callsite (rename gây churn). // @@ -1062,6 +1075,10 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on paymentTermText: '', 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({ mutationFn: async () => api.post(`/purchase-evaluations/${evaluationId}/suppliers`, form), 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" footer={<> - + } >
@@ -1092,8 +1109,29 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
setForm({ ...form, displayName: e.target.value })} placeholder="vd TGN-30 ngày" />
setForm({ ...form, paymentTermText: e.target.value })} placeholder="vd 30 ngày, 300tr" />
setForm({ ...form, contactName: e.target.value })} />
-
setForm({ ...form, contactPhone: e.target.value })} />
-
setForm({ ...form, contactEmail: e.target.value })} />
+
+ + setForm({ ...form, contactPhone: e.target.value })} + placeholder="0987654321" + className={phoneError ? 'border-red-300' : undefined} + /> + {phoneError &&

{phoneError}

} +
+
+ + setForm({ ...form, contactEmail: e.target.value })} + placeholder="name@example.com" + className={emailError ? 'border-red-300' : undefined} + /> + {emailError &&

{emailError}

} +
setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." />
@@ -1112,6 +1150,10 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri paymentTermText: row.paymentTermText ?? '', 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({ 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() }, @@ -1124,15 +1166,36 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri title={`Sửa NCC — ${row.supplierName}`} footer={<> - + } >
setForm({ ...form, displayName: e.target.value })} />
setForm({ ...form, paymentTermText: e.target.value })} />
setForm({ ...form, contactName: e.target.value })} />
-
setForm({ ...form, contactPhone: e.target.value })} />
-
setForm({ ...form, contactEmail: e.target.value })} />
+
+ + setForm({ ...form, contactPhone: e.target.value })} + placeholder="0987654321" + className={phoneError ? 'border-red-300' : undefined} + /> + {phoneError &&

{phoneError}

} +
+
+ + setForm({ ...form, contactEmail: e.target.value })} + placeholder="name@example.com" + className={emailError ? 'border-red-300' : undefined} + /> + {emailError &&

{emailError}

} +
setForm({ ...form, note: e.target.value })} />
@@ -1331,7 +1394,8 @@ function HangMucCard({ NCC - Liên hệ + SĐT + Email Điều khoản TT File báo giá Số tiền @@ -1355,11 +1419,13 @@ function HangMucCard({ {s.displayName &&
{s.displayName}
} {s.note &&
{s.note}
} + + {s.contactPhone || } + - {s.contactName &&
{s.contactName}
} - {s.contactPhone &&
{s.contactPhone}
} - {s.contactEmail &&
{s.contactEmail}
} - {!s.contactName && !s.contactPhone && !s.contactEmail && } + {s.contactEmail + ? {s.contactEmail} + : } {s.paymentTermText ?? } @@ -1578,12 +1644,19 @@ function QuoteDialog({

Hạng mục: {itemName}

- setForm({ thanhTien: Number(e.target.value) })} - autoFocus - /> +
+ setForm({ thanhTien: parseVnd(e.target.value) })} + placeholder="0" + autoFocus + className="pr-12 font-mono text-right" + /> + đ +
+

VND — nhập số, tự format dấu chấm ngàn (vd 1.000.000)