diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx
index c7ad1f6..73411da 100644
--- a/fe-admin/src/components/pe/PeDetailTabs.tsx
+++ b/fe-admin/src/components/pe/PeDetailTabs.tsx
@@ -55,6 +55,16 @@ 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)
+// Session 20 turn 8: trang trí 5 NCC khác màu (cycle theo index). Winner override
+// thành emerald nổi bật. Literal Tailwind class để JIT scan compile được.
+const NCC_PALETTES = [
+ 'border-l-blue-400 bg-blue-50/40',
+ 'border-l-purple-400 bg-purple-50/40',
+ 'border-l-sky-400 bg-sky-50/40',
+ 'border-l-teal-400 bg-teal-50/40',
+ 'border-l-pink-400 bg-pink-50/40',
+] as const
+
// Main detail content — flat render 3 section không tabs.
// Tên giữ PeDetailTabs để không break callsite (rename gây churn).
//
@@ -1055,7 +1065,14 @@ function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBun
// trong HangMucCard (expand panel mỗi hạng mục). 2 dialog Add/Edit Supplier
// vẫn giữ vì HangMucCard call lại.
-function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; onClose: () => void }) {
+// Session 20 turn 8: Dialog thêm NCC mới — khi gọi từ HangMucCard (có detailId)
+// thì input "Số tiền" hiển thị + sequential POST: tạo supplier → tạo quote
+// cho hạng mục đó. detailId optional cho call site khác trong tương lai.
+function AddSupplierDialog({ evaluationId, detailId, onClose }: {
+ evaluationId: string
+ detailId?: string
+ onClose: () => void
+}) {
const qc = useQueryClient()
const suppliers = useQuery({
queryKey: ['all-suppliers'],
@@ -1069,14 +1086,44 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
contactPhone: '',
paymentTermText: '',
note: '',
+ thanhTien: 0,
})
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 showQuote = !!detailId
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() },
+ mutationFn: async () => {
+ // Step 1: tạo NCC tham gia (PE.Suppliers row)
+ const res = await api.post<{ id: string }>(`/purchase-evaluations/${evaluationId}/suppliers`, {
+ supplierId: form.supplierId,
+ displayName: form.displayName,
+ contactName: form.contactName,
+ contactEmail: form.contactEmail,
+ contactPhone: form.contactPhone,
+ paymentTermText: form.paymentTermText,
+ note: form.note,
+ })
+ const newSupplierRowId = res.data.id
+ // Step 2: tạo quote cho hạng mục (chỉ khi có detailId + thanhTien > 0)
+ if (detailId && form.thanhTien > 0) {
+ await api.post(`/purchase-evaluations/${evaluationId}/quotes`, {
+ purchaseEvaluationDetailId: detailId,
+ purchaseEvaluationSupplierId: newSupplierRowId,
+ bgVat: 0,
+ chuaVat: 0,
+ thanhTien: form.thanhTien,
+ note: '',
+ isSelected: false,
+ })
+ }
+ },
+ onSuccess: () => {
+ toast.success(showQuote && form.thanhTien > 0 ? 'Đã thêm NCC + báo giá.' : 'Đã thêm NCC.')
+ qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
+ onClose()
+ },
onError: e => toast.error(getErrorMessage(e)),
})
@@ -1128,6 +1175,25 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
{emailError &&
{emailError}
}
setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." />
+ {showQuote && (
+
+
+
+ setForm({ ...form, thanhTien: parseVnd(e.target.value) })}
+ placeholder="0"
+ className="pr-10 font-mono text-right"
+ />
+ đ
+
+
+ Để trống / 0 → chỉ tạo NCC, chưa báo giá. Sửa lại sau bằng cách click số tiền trong bảng.
+
+
+ )}
@@ -1393,20 +1459,34 @@ function HangMucCard({
- {ev.suppliers.map(s => {
+ {ev.suppliers.map((s, idx) => {
const q = detail.quotes.find(x => x.purchaseEvaluationSupplierId === s.id) ?? null
const isWinner = ev.selectedSupplierId === s.supplierId
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 palette = NCC_PALETTES[idx % NCC_PALETTES.length]
return (
-
+
-
- {isWinner && ✓ }{s.supplierName}
+
+ {s.supplierName}
+ {isWinner && (
+
+ 🏆 Trúng thầu
+
+ )}
{s.displayName && {s.displayName} }
- {s.note && {s.note} }
+ {s.note && {s.note} }
|
{s.contactPhone || —}
@@ -1501,7 +1581,7 @@ function HangMucCard({
)}
- {addNccOpen && setAddNccOpen(false)} />}
+ {addNccOpen && setAddNccOpen(false)} />}
{editNccRow && setEditNccRow(null)} />}
{quoteEdit && (
!s || PHONE_RE.test(s.replace(/[\s\-.]/g, ''))
const isValidEmail = (s: string): boolean => !s || EMAIL_RE.test(s)
+// Session 20 turn 8: trang trí 5 NCC khác màu (cycle theo index). Winner override
+// thành emerald nổi bật. Literal Tailwind class để JIT scan compile được.
+const NCC_PALETTES = [
+ 'border-l-blue-400 bg-blue-50/40',
+ 'border-l-purple-400 bg-purple-50/40',
+ 'border-l-sky-400 bg-sky-50/40',
+ 'border-l-teal-400 bg-teal-50/40',
+ 'border-l-pink-400 bg-pink-50/40',
+] as const
+
// Main detail content — flat render 3 section không tabs.
// Tên giữ PeDetailTabs để không break callsite (rename gây churn).
//
@@ -1055,7 +1065,14 @@ function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBun
// trong HangMucCard (expand panel mỗi hạng mục). 2 dialog Add/Edit Supplier
// vẫn giữ vì HangMucCard call lại.
-function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; onClose: () => void }) {
+// Session 20 turn 8: Dialog thêm NCC mới — khi gọi từ HangMucCard (có detailId)
+// thì input "Số tiền" hiển thị + sequential POST: tạo supplier → tạo quote
+// cho hạng mục đó. detailId optional cho call site khác trong tương lai.
+function AddSupplierDialog({ evaluationId, detailId, onClose }: {
+ evaluationId: string
+ detailId?: string
+ onClose: () => void
+}) {
const qc = useQueryClient()
const suppliers = useQuery({
queryKey: ['all-suppliers'],
@@ -1069,14 +1086,42 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
contactPhone: '',
paymentTermText: '',
note: '',
+ thanhTien: 0,
})
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 showQuote = !!detailId
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() },
+ mutationFn: async () => {
+ const res = await api.post<{ id: string }>(`/purchase-evaluations/${evaluationId}/suppliers`, {
+ supplierId: form.supplierId,
+ displayName: form.displayName,
+ contactName: form.contactName,
+ contactEmail: form.contactEmail,
+ contactPhone: form.contactPhone,
+ paymentTermText: form.paymentTermText,
+ note: form.note,
+ })
+ const newSupplierRowId = res.data.id
+ if (detailId && form.thanhTien > 0) {
+ await api.post(`/purchase-evaluations/${evaluationId}/quotes`, {
+ purchaseEvaluationDetailId: detailId,
+ purchaseEvaluationSupplierId: newSupplierRowId,
+ bgVat: 0,
+ chuaVat: 0,
+ thanhTien: form.thanhTien,
+ note: '',
+ isSelected: false,
+ })
+ }
+ },
+ onSuccess: () => {
+ toast.success(showQuote && form.thanhTien > 0 ? 'Đã thêm NCC + báo giá.' : 'Đã thêm NCC.')
+ qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
+ onClose()
+ },
onError: e => toast.error(getErrorMessage(e)),
})
@@ -1128,6 +1173,25 @@ function AddSupplierDialog({ evaluationId, onClose }: { evaluationId: string; on
{emailError && {emailError} }
setForm({ ...form, note: e.target.value })} placeholder="ĐÃ CHỐT SO SÁNH LẦN 1 / ĐÀM PHÁN THÊM..." />
+ {showQuote && (
+
+
+
+ setForm({ ...form, thanhTien: parseVnd(e.target.value) })}
+ placeholder="0"
+ className="pr-10 font-mono text-right"
+ />
+ đ
+
+
+ Để trống / 0 → chỉ tạo NCC, chưa báo giá. Sửa lại sau bằng cách click số tiền trong bảng.
+
+
+ )}
@@ -1393,20 +1457,34 @@ function HangMucCard({
|
- {ev.suppliers.map(s => {
+ {ev.suppliers.map((s, idx) => {
const q = detail.quotes.find(x => x.purchaseEvaluationSupplierId === s.id) ?? null
const isWinner = ev.selectedSupplierId === s.supplierId
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 palette = NCC_PALETTES[idx % NCC_PALETTES.length]
return (
-
+
-
- {isWinner && ✓ }{s.supplierName}
+
+ {s.supplierName}
+ {isWinner && (
+
+ 🏆 Trúng thầu
+
+ )}
{s.displayName && {s.displayName} }
- {s.note && {s.note} }
+ {s.note && {s.note} }
|
{s.contactPhone || —}
@@ -1501,7 +1579,7 @@ function HangMucCard({
)}
- {addNccOpen && setAddNccOpen(false)} />}
+ {addNccOpen && setAddNccOpen(false)} />}
{editNccRow && setEditNccRow(null)} />}
{quoteEdit && (
|