[CLAUDE] PurchaseEvaluation: Mig 54 giá đề xuất PRO/CCM + CEO chọn giá chốt + CCM duyệt-done ô-tích
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m22s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m22s
Theo note anh Kiệt FDC (go-live so-sánh-giá thứ Hai): - (1) Giá chào thầu thêm giá đề xuất NGOÀI giá NCC: PRO nhập dải Min/Max + CCM nhập 1 giá (2 lệnh role-gate Procurement/CostControl, fail-closed). Khi duyệt cấp cuối, người duyệt CHỌN 1 giá chốt (Ncc/ProMin/ProMax/Ccm) -> luu ApprovedPriceAmount/Source (bind tai moi nhanh DaDuyet, bat buoc chon; auto-approve he thong mien). - (3) CCM duyet-done mien CEO: DOI tu AUTO-threshold (S69) sang O-TICH-TAY (finalizeByCcmDelegation) -- CCM chu dong tich, fail-closed theo nguong + role + gia goi. An toan hon (khong vo tinh bo CEO). - Mig 54 additive-nullable (5 cot PE) - FE 2 app SHA-mirror - test 306->334 (+28: opt-in 6->11, +10 gia chot, +13 setter authz). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1380,6 +1380,128 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
|
||||
)
|
||||
}
|
||||
|
||||
// [Mig 54 2026-06-18 — anh Kiệt FDC] Giá đề xuất tại khối "c. Giá chào thầu".
|
||||
// PRO nhập dải Min/Max (PUT /suggested-price/pro {minPrice,maxPrice}); CCM nhập 1
|
||||
// giá (PUT /suggested-price/ccm {ccmPrice}). Role-gate qua canEditPro/CcmSuggestedPrice
|
||||
// (BE-computed capability — mirror budget PRO/CCM, KHÔNG ràng phase). Read-only khi
|
||||
// !canEdit → hiện text giá. Khi DaDuyet + approvedPriceAmount → dòng "Giá chốt duyệt".
|
||||
const APPROVED_PRICE_SOURCE_LABEL: Record<string, string> = {
|
||||
Ncc: 'Giá NCC',
|
||||
ProMin: 'PRO Min',
|
||||
ProMax: 'PRO Max',
|
||||
Ccm: 'CCM',
|
||||
}
|
||||
function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
||||
const qc = useQueryClient()
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
||||
qc.invalidateQueries({ queryKey: ['pe-list'] })
|
||||
}
|
||||
// PRO Min/Max — ABSOLUTE SET cả cặp (mirror budget CCM dual-field: field không đổi
|
||||
// echo giá trị hiện tại để không bị clear).
|
||||
const proPriceMut = useMutation({
|
||||
mutationFn: async (body: { minPrice: number | null; maxPrice: number | null }) =>
|
||||
api.put(`/purchase-evaluations/${ev.id}/suggested-price/pro`, body),
|
||||
onSuccess: () => { toast.success('Đã lưu giá đề xuất (PRO)'); invalidate() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
const ccmPriceMut = useMutation({
|
||||
mutationFn: async (body: { ccmPrice: number | null }) =>
|
||||
api.put(`/purchase-evaluations/${ev.id}/suggested-price/ccm`, body),
|
||||
onSuccess: () => { toast.success('Đã lưu giá đề xuất (CCM)'); invalidate() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
})
|
||||
|
||||
const canEditPro = !readOnly && ev.canEditProSuggestedPrice
|
||||
const canEditCcm = !readOnly && ev.canEditCcmSuggestedPrice
|
||||
const hasAnyValue = ev.proSuggestedMinPrice != null
|
||||
|| ev.proSuggestedMaxPrice != null
|
||||
|| ev.ccmSuggestedPrice != null
|
||||
const approved = ev.phase === PurchaseEvaluationPhase.DaDuyet && ev.approvedPriceAmount != null
|
||||
|
||||
// Ẩn hoàn toàn khi không edit được + chưa có giá nào + chưa chốt → tránh khối rỗng.
|
||||
if (!canEditPro && !canEditCcm && !hasAnyValue && !approved) return null
|
||||
|
||||
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)
|
||||
</div>
|
||||
|
||||
{/* PRO — Giá Min / Giá Max */}
|
||||
<div className="flex items-start gap-3 text-[12px]">
|
||||
<span className="w-44 shrink-0 pt-1 text-slate-500">Giá đề xuất (PRO)</span>
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-12 shrink-0 text-[11px] text-slate-500">Min</span>
|
||||
{canEditPro ? (
|
||||
<VndInlineEdit
|
||||
initial={ev.proSuggestedMinPrice}
|
||||
saving={proPriceMut.isPending}
|
||||
label="Giá đề xuất PRO — Min"
|
||||
onSave={v => proPriceMut.mutate({ minPrice: v, maxPrice: ev.proSuggestedMaxPrice })}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-semibold text-slate-800">
|
||||
{ev.proSuggestedMinPrice != null ? fmtVnd(ev.proSuggestedMinPrice) : <span className="font-normal text-slate-400">—</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-12 shrink-0 text-[11px] text-slate-500">Max</span>
|
||||
{canEditPro ? (
|
||||
<VndInlineEdit
|
||||
initial={ev.proSuggestedMaxPrice}
|
||||
saving={proPriceMut.isPending}
|
||||
label="Giá đề xuất PRO — Max"
|
||||
onSave={v => proPriceMut.mutate({ minPrice: ev.proSuggestedMinPrice, maxPrice: v })}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-semibold text-slate-800">
|
||||
{ev.proSuggestedMaxPrice != null ? fmtVnd(ev.proSuggestedMaxPrice) : <span className="font-normal text-slate-400">—</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CCM — 1 giá */}
|
||||
<div className="flex items-center gap-3 text-[12px]">
|
||||
<span className="w-44 shrink-0 text-slate-500">Giá đề xuất (CCM)</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
{canEditCcm ? (
|
||||
<VndInlineEdit
|
||||
initial={ev.ccmSuggestedPrice}
|
||||
saving={ccmPriceMut.isPending}
|
||||
label="Giá đề xuất CCM"
|
||||
onSave={v => ccmPriceMut.mutate({ ccmPrice: v })}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-semibold text-slate-800">
|
||||
{ev.ccmSuggestedPrice != null ? fmtVnd(ev.ccmSuggestedPrice) : <span className="font-normal text-slate-400">—</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Giá CHỐT duyệt — chỉ khi DaDuyet + approvedPriceAmount != null. */}
|
||||
{approved && (
|
||||
<div className="flex items-center gap-3 rounded border border-emerald-200 bg-emerald-50 px-2 py-1.5 text-[12px]">
|
||||
<span className="w-44 shrink-0 font-medium text-emerald-800">Giá chốt duyệt</span>
|
||||
<span className="min-w-0 flex-1 font-semibold text-emerald-900">
|
||||
{fmtVnd(ev.approvedPriceAmount!)}
|
||||
{ev.approvedPriceSource && (
|
||||
<span className="ml-2 rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
|
||||
{APPROVED_PRICE_SOURCE_LABEL[ev.approvedPriceSource] ?? ev.approvedPriceSource}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ===== 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
|
||||
@ -1415,6 +1537,10 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
|
||||
)
|
||||
}
|
||||
/>
|
||||
{/* [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ản so sánh</span>
|
||||
|
||||
Reference in New Issue
Block a user