[CLAUDE] PurchaseEvaluation: endsBeforeCeo pointer-aware + duyệt theo khoảng Min–Max (Mig 66)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m26s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m26s
Hai chỉnh từ buổi UAT anh Kiệt FDC (S117): 1) Fix hiển thị lũy kế "duyệt rồi không bắt" — root cause là DISPLAY-LIE, không phải lũy kế bug. endsBeforeCeo cho phiếu CHƯA DaDuyet nay POINTER-AWARE: chỉ báo "kết thúc trước CEO" khi con-trỏ còn ở/trước cấp finalize. Phiếu đã đi QUA cấp finalize (approver bỏ tick "Cấp này KẾT THÚC" → trình tiếp CEO) → false: sơ đồ hết làm mờ Bước CEO + hết nói dối "Kết thúc tại Cấp X" khi phiếu đang chờ CEO. 3 site (detail in-memory + list/inbox EF-subquery). Filter lũy kế GIỮ nguyên (đúng nguyên tắc "duyệt rồi mới bắc"). BE-only — FE là consumer thuần nên tự render đúng. 2) Feature "duyệt theo KHOẢNG Min–Max": khi PRO có cả Min & Max, người duyệt cấp cuối chọn CẢ HAI (radio "PRO — cả Min và Max", source=ProMinMax) → Min ở ApprovedPriceAmount + Max ở ApprovedPriceMaxAmount (Mig 66 AddColumn nullable, no backfill, phiếu cũ nguyên vẹn). Bất-biến Max⟺ProMinMax, Min≤Max (guard ở service + validator). HĐ.GiaTri KHÔNG đọc giá chốt (= SUM báo giá NCC được chọn) → range thuần record-of-decision hiển thị, 0 tác động HĐ. FE 2-app SHA-mirror (Panel + DetailTabs). Test 495 → 501 (+1 pointer-aware repro, +5 ProMinMax). Cả 2 điều tra qua /fable-real engine-đắt (investigator-codebase deep-pass) theo yêu cầu anh Kiệt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1662,6 +1662,7 @@ const APPROVED_PRICE_SOURCE_LABEL: Record<string, string> = {
|
||||
Ncc: 'Giá NCC',
|
||||
ProMin: 'PRO Min',
|
||||
ProMax: 'PRO Max',
|
||||
ProMinMax: 'PRO Min–Max',
|
||||
Ccm: 'CCM',
|
||||
}
|
||||
function SuggestedPriceRows({ ev }: { ev: PeDetailBundle }) {
|
||||
@ -1852,7 +1853,9 @@ function SuggestedPriceRows({ ev }: { ev: PeDetailBundle }) {
|
||||
<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.approvedPriceMaxAmount != null
|
||||
? `${fmtVnd(ev.approvedPriceAmount!)} – ${fmtVnd(ev.approvedPriceMaxAmount)}`
|
||||
: 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}
|
||||
|
||||
@ -131,14 +131,22 @@ export function PeWorkflowPanel({
|
||||
|| (finalizeFlowStep != null && stepOrder === finalizeFlowStep.order
|
||||
&& finalizeFlowLevelOrder != null && levelOrder > finalizeFlowLevelOrder)
|
||||
// Ứng viên giá chốt — chỉ giá nào đã có giá trị (≠ null).
|
||||
// [S117 anh Kiệt FDC] Khi PRO có CẢ Min & Max → thêm ứng viên "duyệt theo khoảng"
|
||||
// (source=ProMinMax, amount=Min, amountMax=Max). Chọn nó = chốt dải [Min, Max].
|
||||
const hasBothProPrices = evaluation.proSuggestedMinPrice != null && evaluation.proSuggestedMaxPrice != null
|
||||
const priceCandidates = ([
|
||||
{ source: 'Ncc', label: 'Giá NCC (giá chào thầu)', amount: evaluation.winnerQuoteTotal },
|
||||
{ source: 'ProMin', label: 'PRO — Giá Min', amount: evaluation.proSuggestedMinPrice },
|
||||
{ source: 'ProMax', label: 'PRO — Giá Max', amount: evaluation.proSuggestedMaxPrice },
|
||||
...(hasBothProPrices
|
||||
? [{ source: 'ProMinMax', label: 'PRO — cả Min và Max (duyệt theo khoảng)', amount: evaluation.proSuggestedMinPrice, amountMax: evaluation.proSuggestedMaxPrice }]
|
||||
: []),
|
||||
{ source: 'Ccm', label: 'CCM đề xuất', amount: evaluation.ccmSuggestedPrice },
|
||||
] as { source: string; label: string; amount: number | null | undefined }[])
|
||||
.filter((c): c is { source: string; label: string; amount: number } => c.amount != null)
|
||||
const selectedPriceAmount = priceCandidates.find(c => c.source === approvedPriceSource)?.amount ?? null
|
||||
] as { source: string; label: string; amount: number | null | undefined; amountMax?: number | null }[])
|
||||
.filter((c): c is { source: string; label: string; amount: number; amountMax?: number } => c.amount != null)
|
||||
const selectedCandidate = priceCandidates.find(c => c.source === approvedPriceSource)
|
||||
const selectedPriceAmount = selectedCandidate?.amount ?? null
|
||||
const selectedPriceAmountMax = selectedCandidate?.amountMax ?? null
|
||||
|
||||
// 2-stage dept approvals (Migration 16) — fetch riêng để FE Workflow Panel
|
||||
// hiển thị progress per phase × dept (Stage Review NV / Confirm TPB).
|
||||
@ -203,6 +211,9 @@ export function PeWorkflowPanel({
|
||||
applyLevelFinalize: !isReject && (approverFinalizeEligible ? applyLevelFinalize : true),
|
||||
approvedPriceAmount: sendPrice ? selectedPriceAmount : null,
|
||||
approvedPriceSource: sendPrice ? approvedPriceSource : null,
|
||||
// [S117] Duyệt theo khoảng: chỉ gửi Max khi nguồn = ProMinMax (selectedPriceAmountMax != null).
|
||||
// Nguồn đơn → undefined → axios bỏ key (BE reject nếu source đơn mà kèm Max).
|
||||
approvedPriceMaxAmount: sendPrice ? (selectedPriceAmountMax ?? undefined) : undefined,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
@ -693,7 +704,9 @@ export function PeWorkflowPanel({
|
||||
onChange={() => setApprovedPriceSource(c.source)}
|
||||
/>
|
||||
<span className="flex-1 text-slate-700">{c.label}</span>
|
||||
<span className="font-mono font-semibold text-slate-900">{fmtMoney(c.amount)}đ</span>
|
||||
<span className="font-mono font-semibold text-slate-900">
|
||||
{c.amountMax != null ? `${fmtMoney(c.amount)}đ – ${fmtMoney(c.amountMax)}đ` : `${fmtMoney(c.amount)}đ`}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{priceCandidates.length === 0 && (
|
||||
|
||||
@ -488,6 +488,7 @@ export type PeDetailBundle = {
|
||||
ccmSuggestedPriceNote: string | null
|
||||
approvedPriceAmount: number | null
|
||||
approvedPriceSource: string | null
|
||||
approvedPriceMaxAmount: number | null
|
||||
canEditProSuggestedPrice: boolean
|
||||
canEditCcmSuggestedPrice: boolean
|
||||
// Mig 23 — Pin schema mới ApprovalWorkflowsV2 (User chọn lúc create).
|
||||
|
||||
Reference in New Issue
Block a user