[CLAUDE] PurchaseEvaluation: tách khối nổi bật "4. Thông tin chọn thầu" (anh Kiệt FDC)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m7s

Tách c/d/e (Giá chào thầu + Đề xuất PRO/CCM + Bảng so sánh + Link hồ sơ) khỏi
Section 3 thành khối NỔI BẬT riêng "4. Thông tin chọn thầu" — header xanh #1F7DC1
giống bảng ngân sách "giá trị thực hiện". Đánh số lại 1->5:
- 1. Tên NCC/TP + Giá trị (chào thầu): winner "mang từ trên xuống", hiển-thị nổi bật
  (Section 3 giữ ô CHỌN NccSelectorRow; multi-winner -> tên cách dấu phẩy)
- 2-3. Giá đề xuất PRO/CCM (SuggestedPriceRows) · 4. Bảng so sánh giá · 5. Link hồ sơ
- Ý kiến cấp duyệt: Section 4 -> 5.
FE-only (PeDetailTabs ChonNccSection + new ThongTinChonThauSection), 2 app SHA-mirror,
build sạch. Mockup anh Kiệt duyệt trước khi build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-24 17:09:07 +07:00
parent 8cf4a58c8c
commit 0d3c0cf16b
2 changed files with 138 additions and 94 deletions

View File

@ -402,7 +402,12 @@ export function PeDetailTabs({
<Section title="3. Đơn vị NCC/TP được chọn">
<ChonNccSection ev={evaluation} readOnly={readOnly} />
</Section>
<Section title="4. Ý kiến cấp duyệt (sign-off theo workflow)">
{/* [anh Kiệt FDC S87+] Khối NỔI BẬT "4. Thông tin chọn thầu" — header xanh
giống bảng "giá trị thực hiện". Gom winner + c/d/e tách khỏi Section 3. */}
<section className="px-3 py-3 sm:px-5 sm:py-4">
<ThongTinChonThauSection ev={evaluation} readOnly={readOnly} />
</section>
<Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)">
{mode === 'workspace' && (
<div className="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] text-amber-800">
Ý kiến + chữ auto đng bộ khi NV duyệt phiếu vào menu &ldquo;Duyệt&rdquo; đ .
@ -1742,7 +1747,7 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo
return (
<div className="space-y-2 rounded-lg border border-slate-200 bg-slate-50/60 px-3 py-2.5">
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
Giá đ xuất (ngoài giá chào thầu)
2 3. Giá đ xuất từ PRO / CCM (ngoài giá chào thầu)
</div>
{/* PRO — Giá Min / Giá Max */}
@ -1895,17 +1900,9 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.suppliers.some(s => s.isWinner)
const [createOpen, setCreateOpen] = useState(false)
// c. Giá chào thầu = sum quotes của NCC được chọn (winner). Dùng helper
// module-level (computeGiaChaoThau) — cùng predicate với pre-check gửi duyệt.
const hasWinner = ev.suppliers.some(s => s.isWinner) // [Mig 58] multi-winner
const giaChaoThau = computeGiaChaoThau(ev)
// d. Bản so sánh — attachments supplier-row null, NHƯNG loại file "đính kèm khi
// duyệt" (S78 — purpose=ApprovalAttachment cũng supplierId=null) khỏi section này.
const banSoSanhAttachments = ev.attachments.filter(
a => a.purchaseEvaluationSupplierId === null
&& a.purpose !== PeAttachmentPurpose.ApprovalAttachment,
)
// [anh Kiệt FDC S87+] c/d/e (Giá chào thầu + Đề xuất PRO/CCM + Bảng so sánh + Link hồ
// sơ) + hiển-thị winner ĐÃ TÁCH sang khối nổi bật "4. Thông tin chọn thầu"
// (ThongTinChonThauSection). Section 3 này GIỮ: a. ô CHỌN winner + b. bảng ngân sách + tạo HĐ.
return (
<div className="space-y-3">
@ -1913,39 +1910,8 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
{/* b. TỔNG HỢP NGÂN SÁCH TRÌNH KÝ (S61 — Excel anh Kiệt). Thay BudgetFieldRow
+ BudgetAdjustSection cũ (module Budget bỏ hẳn). */}
<PeBudgetSummaryTable ev={ev} readOnly={readOnly} />
<FormRow
label="c. Giá chào thầu"
value={
!hasWinner ? (
<span className="text-slate-400"> (chọn NCC/TP (a) trước)</span>
) : giaChaoThau === 0 ? (
<span className="text-slate-400"> (chưa nhập báo giá Section 4)</span>
) : (
<span className="font-semibold text-slate-900">{giaChaoThau!.toLocaleString('vi-VN')} đ</span>
)
}
/>
{/* [Mig 54 2026-06-18 — anh Kiệt FDC] Giá đề xuất PRO (Min/Max) + CCM —
NGOÀI giá NCC. Role-gate qua canEditPro/CcmSuggestedPrice (mirror budget,
KHÔNG ràng phase). Khi DaDuyet + approvedPriceAmount → show giá chốt + nguồn. */}
<SuggestedPriceRows ev={ev} readOnly={readOnly} />
<div>
<div className="flex gap-3">
<span className="w-44 shrink-0 text-[12px] text-slate-500">d. Bảng so sánh giá</span>
<div className="min-w-0 flex-1">
<GeneralAttachmentsSection
evaluationId={ev.id}
attachments={banSoSanhAttachments}
readOnly={readOnly}
/>
</div>
</div>
</div>
{/* e. Link hồ sơ (anh Kiệt FDC) — 1 hyperlink tới thư mục hồ sơ trên NAS công ty.
Read-only: render thẻ <a> bấm-mở (target=_blank). Editable: Input dán URL +
nút Lưu (PUT /purchase-evaluations/:id echo field bắt buộc + hoSoLink). */}
<HoSoLinkRow ev={ev} readOnly={readOnly} />
{/* [anh Kiệt FDC S87+] c/d/e + hiển-thị winner ĐÃ chuyển sang khối nổi bật
"4. Thông tin chọn thầu" (ThongTinChonThauSection — render ở parent, sau Section 3). */}
{ev.paymentTerms && (
<FormRow label="Điều khoản thanh toán" value={<span className="whitespace-pre-wrap">{ev.paymentTerms}</span>} />
@ -1974,6 +1940,62 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
)
}
// ===== Section 4 — Thông tin chọn thầu (anh Kiệt FDC S87+) =====
// Tách c/d/e khỏi Section 3 thành khối NỔI BẬT riêng (header xanh #1F7DC1 giống bảng
// ngân sách "giá trị thực hiện"). Đánh số 1→5: 1.Tên NCC/TP+giá trị · 2-3.Đề xuất
// PRO/CCM · 4.Bảng so sánh giá · 5.Link hồ sơ. Tên đơn vị "mang từ trên xuống" — Section 3
// giữ ô CHỌN (NccSelectorRow), khối này HIỂN THỊ kết quả (multi-winner → tên cách dấu phẩy).
function ThongTinChonThauSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
const winners = ev.suppliers.filter(s => s.isWinner)
const hasWinner = winners.length > 0
const giaChaoThau = computeGiaChaoThau(ev)
const banSoSanhAttachments = ev.attachments.filter(
a => a.purchaseEvaluationSupplierId === null
&& a.purpose !== PeAttachmentPurpose.ApprovalAttachment,
)
return (
<div className="overflow-hidden rounded-lg border border-slate-300">
<div className="bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">
Thông tin chọn thầu
</div>
<div className="space-y-3 p-3">
{/* 1. Tên NCC/TP + Giá trị (chào thầu) — kéo từ trên xuống, nổi bật */}
<div className="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2.5">
<div className="mb-1 flex items-center gap-2">
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-600 text-[11px] font-bold text-white">1</span>
<span className="text-[11px] font-semibold uppercase tracking-wide text-emerald-700">Tên NCC / TP đưc chọn</span>
</div>
<div className="pl-7">
{hasWinner
? <div className="text-[15px] font-semibold text-emerald-900">{winners.map(s => s.supplierName).join(', ')}</div>
: <div className="text-[13px] text-slate-400"> (chọn đơn vị mục 3 trước)</div>}
<div className="mt-1 flex items-baseline gap-2 text-[12px]">
<span className="text-slate-500">Giá trị (chào thầu):</span>
{!hasWinner
? <span className="text-slate-400"></span>
: giaChaoThau === 0
? <span className="text-slate-400"> (chưa nhập báo giá)</span>
: <span className="text-[14px] font-semibold text-emerald-900">{giaChaoThau!.toLocaleString('vi-VN')} đ</span>}
</div>
</div>
</div>
{/* 2 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
<SuggestedPriceRows ev={ev} readOnly={readOnly} />
{/* 4. Bảng so sánh giá */}
<div>
<div className="mb-1 text-[12px] font-medium text-slate-600">4. Bảng so sánh giá</div>
<GeneralAttachmentsSection evaluationId={ev.id} attachments={banSoSanhAttachments} readOnly={readOnly} />
</div>
{/* 5. Link hồ sơ */}
<HoSoLinkRow ev={ev} readOnly={readOnly} />
</div>
</div>
)
}
// e. Link hồ sơ — 1 cột HoSoLink (string? nullable) trỏ thư mục hồ sơ NAS.
// Read-only: thẻ <a> bấm-mở. Editable (phiếu DangSoanThao/TraLai + !readOnly):
// Input dán URL + nút Lưu. Save = PUT /purchase-evaluations/:id echo field bắt
@ -2008,7 +2030,7 @@ function HoSoLinkRow({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?:
return (
<div className="flex gap-3">
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">e. Link hồ </span>
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">5. Link hồ </span>
<div className="min-w-0 flex-1">
{canEdit ? (
<div className="flex max-w-2xl items-center gap-2">

View File

@ -402,7 +402,12 @@ export function PeDetailTabs({
<Section title="3. Đơn vị NCC/TP được chọn">
<ChonNccSection ev={evaluation} readOnly={readOnly} />
</Section>
<Section title="4. Ý kiến cấp duyệt (sign-off theo workflow)">
{/* [anh Kiệt FDC S87+] Khối NỔI BẬT "4. Thông tin chọn thầu" — header xanh
giống bảng "giá trị thực hiện". Gom winner + c/d/e tách khỏi Section 3. */}
<section className="px-3 py-3 sm:px-5 sm:py-4">
<ThongTinChonThauSection ev={evaluation} readOnly={readOnly} />
</section>
<Section title="5. Ý kiến cấp duyệt (sign-off theo workflow)">
{mode === 'workspace' && (
<div className="mb-3 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] text-amber-800">
Ý kiến + chữ auto đng bộ khi NV duyệt phiếu vào menu &ldquo;Duyệt&rdquo; đ .
@ -1742,7 +1747,7 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo
return (
<div className="space-y-2 rounded-lg border border-slate-200 bg-slate-50/60 px-3 py-2.5">
<div className="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
Giá đ xuất (ngoài giá chào thầu)
2 3. Giá đ xuất từ PRO / CCM (ngoài giá chào thầu)
</div>
{/* PRO — Giá Min / Giá Max */}
@ -1895,17 +1900,9 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.suppliers.some(s => s.isWinner)
const [createOpen, setCreateOpen] = useState(false)
// c. Giá chào thầu = sum quotes của NCC được chọn (winner). Dùng helper
// module-level (computeGiaChaoThau) — cùng predicate với pre-check gửi duyệt.
const hasWinner = ev.suppliers.some(s => s.isWinner) // [Mig 58] multi-winner
const giaChaoThau = computeGiaChaoThau(ev)
// d. Bản so sánh — attachments supplier-row null, NHƯNG loại file "đính kèm khi
// duyệt" (S78 — purpose=ApprovalAttachment cũng supplierId=null) khỏi section này.
const banSoSanhAttachments = ev.attachments.filter(
a => a.purchaseEvaluationSupplierId === null
&& a.purpose !== PeAttachmentPurpose.ApprovalAttachment,
)
// [anh Kiệt FDC S87+] c/d/e (Giá chào thầu + Đề xuất PRO/CCM + Bảng so sánh + Link hồ
// sơ) + hiển-thị winner ĐÃ TÁCH sang khối nổi bật "4. Thông tin chọn thầu"
// (ThongTinChonThauSection). Section 3 này GIỮ: a. ô CHỌN winner + b. bảng ngân sách + tạo HĐ.
return (
<div className="space-y-3">
@ -1913,39 +1910,8 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
{/* b. TỔNG HỢP NGÂN SÁCH TRÌNH KÝ (S61 — Excel anh Kiệt). Thay BudgetFieldRow
+ BudgetAdjustSection cũ (module Budget bỏ hẳn). */}
<PeBudgetSummaryTable ev={ev} readOnly={readOnly} />
<FormRow
label="c. Giá chào thầu"
value={
!hasWinner ? (
<span className="text-slate-400"> (chọn NCC/TP (a) trước)</span>
) : giaChaoThau === 0 ? (
<span className="text-slate-400"> (chưa nhập báo giá Section 4)</span>
) : (
<span className="font-semibold text-slate-900">{giaChaoThau!.toLocaleString('vi-VN')} đ</span>
)
}
/>
{/* [Mig 54 2026-06-18 — anh Kiệt FDC] Giá đề xuất PRO (Min/Max) + CCM —
NGOÀI giá NCC. Role-gate qua canEditPro/CcmSuggestedPrice (mirror budget,
KHÔNG ràng phase). Khi DaDuyet + approvedPriceAmount → show giá chốt + nguồn. */}
<SuggestedPriceRows ev={ev} readOnly={readOnly} />
<div>
<div className="flex gap-3">
<span className="w-44 shrink-0 text-[12px] text-slate-500">d. Bảng so sánh giá</span>
<div className="min-w-0 flex-1">
<GeneralAttachmentsSection
evaluationId={ev.id}
attachments={banSoSanhAttachments}
readOnly={readOnly}
/>
</div>
</div>
</div>
{/* e. Link hồ sơ (anh Kiệt FDC) — 1 hyperlink tới thư mục hồ sơ trên NAS công ty.
Read-only: render thẻ <a> bấm-mở (target=_blank). Editable: Input dán URL +
nút Lưu (PUT /purchase-evaluations/:id echo field bắt buộc + hoSoLink). */}
<HoSoLinkRow ev={ev} readOnly={readOnly} />
{/* [anh Kiệt FDC S87+] c/d/e + hiển-thị winner ĐÃ chuyển sang khối nổi bật
"4. Thông tin chọn thầu" (ThongTinChonThauSection — render ở parent, sau Section 3). */}
{ev.paymentTerms && (
<FormRow label="Điều khoản thanh toán" value={<span className="whitespace-pre-wrap">{ev.paymentTerms}</span>} />
@ -1974,6 +1940,62 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
)
}
// ===== Section 4 — Thông tin chọn thầu (anh Kiệt FDC S87+) =====
// Tách c/d/e khỏi Section 3 thành khối NỔI BẬT riêng (header xanh #1F7DC1 giống bảng
// ngân sách "giá trị thực hiện"). Đánh số 1→5: 1.Tên NCC/TP+giá trị · 2-3.Đề xuất
// PRO/CCM · 4.Bảng so sánh giá · 5.Link hồ sơ. Tên đơn vị "mang từ trên xuống" — Section 3
// giữ ô CHỌN (NccSelectorRow), khối này HIỂN THỊ kết quả (multi-winner → tên cách dấu phẩy).
function ThongTinChonThauSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
const winners = ev.suppliers.filter(s => s.isWinner)
const hasWinner = winners.length > 0
const giaChaoThau = computeGiaChaoThau(ev)
const banSoSanhAttachments = ev.attachments.filter(
a => a.purchaseEvaluationSupplierId === null
&& a.purpose !== PeAttachmentPurpose.ApprovalAttachment,
)
return (
<div className="overflow-hidden rounded-lg border border-slate-300">
<div className="bg-[#1F7DC1] px-3 py-2 text-[12px] font-bold uppercase tracking-wide text-white">
Thông tin chọn thầu
</div>
<div className="space-y-3 p-3">
{/* 1. Tên NCC/TP + Giá trị (chào thầu) — kéo từ trên xuống, nổi bật */}
<div className="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2.5">
<div className="mb-1 flex items-center gap-2">
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-600 text-[11px] font-bold text-white">1</span>
<span className="text-[11px] font-semibold uppercase tracking-wide text-emerald-700">Tên NCC / TP đưc chọn</span>
</div>
<div className="pl-7">
{hasWinner
? <div className="text-[15px] font-semibold text-emerald-900">{winners.map(s => s.supplierName).join(', ')}</div>
: <div className="text-[13px] text-slate-400"> (chọn đơn vị mục 3 trước)</div>}
<div className="mt-1 flex items-baseline gap-2 text-[12px]">
<span className="text-slate-500">Giá trị (chào thầu):</span>
{!hasWinner
? <span className="text-slate-400"></span>
: giaChaoThau === 0
? <span className="text-slate-400"> (chưa nhập báo giá)</span>
: <span className="text-[14px] font-semibold text-emerald-900">{giaChaoThau!.toLocaleString('vi-VN')} đ</span>}
</div>
</div>
</div>
{/* 2 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
<SuggestedPriceRows ev={ev} readOnly={readOnly} />
{/* 4. Bảng so sánh giá */}
<div>
<div className="mb-1 text-[12px] font-medium text-slate-600">4. Bảng so sánh giá</div>
<GeneralAttachmentsSection evaluationId={ev.id} attachments={banSoSanhAttachments} readOnly={readOnly} />
</div>
{/* 5. Link hồ sơ */}
<HoSoLinkRow ev={ev} readOnly={readOnly} />
</div>
</div>
)
}
// e. Link hồ sơ — 1 cột HoSoLink (string? nullable) trỏ thư mục hồ sơ NAS.
// Read-only: thẻ <a> bấm-mở. Editable (phiếu DangSoanThao/TraLai + !readOnly):
// Input dán URL + nút Lưu. Save = PUT /purchase-evaluations/:id echo field bắt
@ -2008,7 +2030,7 @@ function HoSoLinkRow({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?:
return (
<div className="flex gap-3">
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">e. Link hồ </span>
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">5. Link hồ </span>
<div className="min-w-0 flex-1">
{canEdit ? (
<div className="flex max-w-2xl items-center gap-2">