[CLAUDE] FE PE: restructure InfoTab theo spec PHIẾU TRÌNH KÝ CHỌN TP/NCC
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m3s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m3s
Match form chính thức 4 section đánh số: 1. Thông tin gói thầu — chỉ a. Tên gói thầu + b. Dự án (Địa điểm + Mô tả compact bên dưới nếu có). 2. Chọn NCC / TP — đúng a/b/c/d: a. NCC/TP được chọn — selectedSupplierName badge xanh b. Ngân sách — link Budget với mã + tên + tổng c. Giá chào thầu — tự compute = sum quotes của winner supplier (filter quotes.purchaseEvaluationSupplierId === winnerRowId) d. Bản so sánh — embed GeneralAttachmentsSection (attachments không gắn supplier-row, purpose=ComparisonTable) + ĐKTT + HĐ kế thừa link bonus + Banner emerald 'Tạo HĐ từ phiếu' khi DaDuyet + chưa có Contract 3. NCC/TP tham gia — section riêng giữ table 5 cột (NCC/Liên hệ/ĐKTT/File/Action — nhiều info hơn spec table 3 cột, useful cho UX web). 4. Hạng mục + Báo giá — matrix với cột 'NS link · Δ' + footer aggregate (giữ nguyên). Side change: - FormRow helper mới (label 176px + value flex) thay cho dl grid 2-col cũ — match style form giấy - Drop Field helper cũ (now unused) - InfoTab signature đổi: bỏ readOnly param (chỉ display, action move sang ChonNccSection) TS build pass cả 2 app. Mirror fe-user identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -95,22 +95,19 @@ export function PeDetailTabs({
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-200">
|
||||
<Section title="Thông tin">
|
||||
<InfoTab ev={evaluation} readOnly={readOnly} />
|
||||
{/* Section 1 — đúng spec form FO-PHIẾU TRÌNH KÝ CHỌN TP/NCC */}
|
||||
<Section title="1. Thông tin gói thầu">
|
||||
<InfoTab ev={evaluation} />
|
||||
</Section>
|
||||
<Section title={`NCC tham gia (${evaluation.suppliers.length})`}>
|
||||
<Section title="2. Chọn NCC / TP">
|
||||
<ChonNccSection ev={evaluation} readOnly={readOnly} />
|
||||
</Section>
|
||||
<Section title={`3. NCC / TP tham gia (${evaluation.suppliers.length})`}>
|
||||
<SuppliersTab ev={evaluation} readOnly={readOnly} />
|
||||
</Section>
|
||||
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||
<Section title={`4. Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
||||
</Section>
|
||||
<Section title="Bảng so sánh (file tổng)">
|
||||
<GeneralAttachmentsSection
|
||||
evaluationId={evaluation.id}
|
||||
attachments={evaluation.attachments.filter(a => a.purchaseEvaluationSupplierId === null)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -145,35 +142,90 @@ export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ===== Tab: Thông tin =====
|
||||
function InfoTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
|
||||
// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
|
||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||
return (
|
||||
<dl className="space-y-2 text-sm">
|
||||
<FormRow label="a. Tên gói thầu" value={ev.tenGoiThau} />
|
||||
<FormRow label="b. Dự án" value={ev.projectName} />
|
||||
{(ev.diaDiem || ev.moTa) && (
|
||||
<div className="mt-3 rounded bg-slate-50 px-3 py-2 text-[12px] text-slate-600">
|
||||
{ev.diaDiem && <div><span className="text-slate-400">Địa điểm:</span> {ev.diaDiem}</div>}
|
||||
{ev.moTa && <div><span className="text-slate-400">Mô tả:</span> {ev.moTa}</div>}
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== Section 2 — Chọn NCC/TP (spec: a/b/c/d) =====
|
||||
function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
|
||||
const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
|
||||
// c. Giá chào thầu = sum quotes của NCC được chọn (winner)
|
||||
const winnerSupplierRowId = ev.selectedSupplierId
|
||||
? ev.suppliers.find(s => s.supplierId === ev.selectedSupplierId)?.id ?? null
|
||||
: null
|
||||
const giaChaoThau = winnerSupplierRowId
|
||||
? ev.details
|
||||
.flatMap(d => d.quotes)
|
||||
.filter(q => q.purchaseEvaluationSupplierId === winnerSupplierRowId)
|
||||
.reduce((sum, q) => sum + q.thanhTien, 0)
|
||||
: null
|
||||
|
||||
// d. Bản so sánh — attachments với purpose=ComparisonTable hoặc supplier-row null
|
||||
const banSoSanhAttachments = ev.attachments.filter(
|
||||
a => a.purchaseEvaluationSupplierId === null,
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
||||
<Field label="Dự án" value={ev.projectName} />
|
||||
<Field label="Địa điểm" value={ev.diaDiem ?? '—'} />
|
||||
<Field label="Mô tả" value={ev.moTa ?? '—'} />
|
||||
<Field label="NCC được chọn" value={ev.selectedSupplierName ?? '—'} />
|
||||
<Field label="Điều khoản thanh toán" value={ev.paymentTerms ?? '—'} />
|
||||
<Field
|
||||
label="Ngân sách"
|
||||
value={ev.budget ? (
|
||||
<a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
|
||||
<span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
|
||||
{' · '}
|
||||
{ev.budget.tenNganSach}
|
||||
{' · '}
|
||||
<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
|
||||
</a>
|
||||
) : <span className="text-slate-400">— (chưa link)</span>}
|
||||
<div className="space-y-3">
|
||||
<FormRow
|
||||
label="a. NCC / TP được chọn"
|
||||
value={ev.selectedSupplierName ? (
|
||||
<span className="font-medium text-emerald-700">✓ {ev.selectedSupplierName}</span>
|
||||
) : <span className="text-slate-400">— (chưa chọn)</span>}
|
||||
/>
|
||||
<FormRow
|
||||
label="b. Ngân sách"
|
||||
value={ev.budget ? (
|
||||
<a href={`/budgets?id=${ev.budget.id}`} className="text-brand-600 hover:underline">
|
||||
<span className="font-mono text-[11px]">{ev.budget.maNganSach ?? '—'}</span>
|
||||
{' · '}{ev.budget.tenNganSach}
|
||||
{' · '}<span className="text-slate-500">{ev.budget.tongNganSach.toLocaleString('vi-VN')} đ</span>
|
||||
</a>
|
||||
) : <span className="text-slate-400">— (chưa link)</span>}
|
||||
/>
|
||||
<FormRow
|
||||
label="c. Giá chào thầu"
|
||||
value={giaChaoThau != null ? (
|
||||
<span className="font-semibold text-slate-900">{giaChaoThau.toLocaleString('vi-VN')} đ</span>
|
||||
) : <span className="text-slate-400">— (chưa chọn NCC / chưa nhập báo giá)</span>}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex gap-3">
|
||||
<span className="w-44 shrink-0 text-[12px] text-slate-500">d. Bản so sánh</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<GeneralAttachmentsSection
|
||||
evaluationId={ev.id}
|
||||
attachments={banSoSanhAttachments}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ev.paymentTerms && (
|
||||
<FormRow label="Điều khoản thanh toán" value={<span className="whitespace-pre-wrap">{ev.paymentTerms}</span>} />
|
||||
)}
|
||||
{ev.contractId && (
|
||||
<FormRow
|
||||
label="HĐ kế thừa"
|
||||
value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>}
|
||||
/>
|
||||
{ev.contractId && (
|
||||
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
|
||||
{canCreateContract && (
|
||||
<div className="rounded border border-emerald-200 bg-emerald-50 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@ -191,6 +243,16 @@ function InfoTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: bool
|
||||
)
|
||||
}
|
||||
|
||||
// Form row: label cố định 176px (w-44) bên trái + value bên phải (giống spec).
|
||||
function FormRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-baseline gap-3 border-b border-dotted border-slate-200 pb-1.5">
|
||||
<dt className="w-44 shrink-0 text-[12px] text-slate-500">{label}</dt>
|
||||
<dd className="min-w-0 flex-1 text-slate-800">{value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBundle; onClose: () => void }) {
|
||||
const navigate = useNavigate()
|
||||
const [form, setForm] = useState({
|
||||
@ -253,15 +315,6 @@ function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBun
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<dt className="text-[11px] uppercase tracking-wide text-slate-400">{label}</dt>
|
||||
<dd className="mt-0.5 text-slate-800">{value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== Tab: NCC =====
|
||||
function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
|
||||
const qc = useQueryClient()
|
||||
|
||||
Reference in New Issue
Block a user