[CLAUDE] FE-User · FE-Admin: PE giá đề xuất PRO/CCM editable theo role (gỡ !readOnly over-gate)
Ô "Giá đề xuất (PRO/CCM)" trong SuggestedPriceRows bị khoá thừa tầng !readOnly mà ô
ngân sách song song (editable={bs.canEditCcm}) không có. Hệ quả: CCM (role CostControl)
không nhập được giá đề xuất khi xem phiếu ở view readOnly=true (preview danh sách, hoặc
màn Duyệt chưa tới lượt mình) — đúng báo cáo anh Kiệt FDC "PRO oki nhưng CCM không nhập".
Gỡ !readOnly, gate thuần theo role-capability (ev.canEdit{Pro,Ccm}SuggestedPrice do BE
tính). BE setter vẫn fail-closed theo role (ForbiddenException) nên gỡ gate FE = an toàn,
khớp hành vi ô ngân sách + intent handler "KHÔNG ràng Phase, chỉnh như tài liệu sống".
Bỏ luôn prop readOnly không dùng khỏi signature + call-site. 2 app SHA-mirror, build xanh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1700,7 +1700,7 @@ const APPROVED_PRICE_SOURCE_LABEL: Record<string, string> = {
|
|||||||
ProMax: 'PRO Max',
|
ProMax: 'PRO Max',
|
||||||
Ccm: 'CCM',
|
Ccm: 'CCM',
|
||||||
}
|
}
|
||||||
function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
function SuggestedPriceRows({ ev }: { ev: PeDetailBundle }) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const invalidate = () => {
|
const invalidate = () => {
|
||||||
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
||||||
@ -1722,8 +1722,13 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo
|
|||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
|
|
||||||
const canEditPro = !readOnly && ev.canEditProSuggestedPrice
|
// [S89 anh Kiệt FDC bug] Giá đề xuất PRO/CCM editable theo ROLE-capability đơn thuần
|
||||||
const canEditCcm = !readOnly && ev.canEditCcmSuggestedPrice
|
// (mirror ô ngân sách Block A/B dùng bs.canEditPro/canEditCcm — KHÔNG gắn !readOnly).
|
||||||
|
// BE setter fail-closed theo role (Forbidden) + "không ràng Phase, chỉnh như tài liệu
|
||||||
|
// sống" → CCM/PRO nhập được MỌI view (kể cả preview + Duyệt chưa-tới-lượt readOnly=true),
|
||||||
|
// không còn kẹt read-only khi xem phiếu ngoài đúng lượt duyệt của mình.
|
||||||
|
const canEditPro = ev.canEditProSuggestedPrice
|
||||||
|
const canEditCcm = ev.canEditCcmSuggestedPrice
|
||||||
|
|
||||||
// Ghi chú PRO/CCM inline-edit state (Textarea). Echo cùng body absolute-set khi lưu giá.
|
// Ghi chú PRO/CCM inline-edit state (Textarea). Echo cùng body absolute-set khi lưu giá.
|
||||||
const [proNoteText, setProNoteText] = useState(ev.proSuggestedPriceNote ?? '')
|
const [proNoteText, setProNoteText] = useState(ev.proSuggestedPriceNote ?? '')
|
||||||
@ -1981,7 +1986,7 @@ function ThongTinChonThauSection({ ev, readOnly = false }: { ev: PeDetailBundle;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2 – 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
|
{/* 2 – 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
|
||||||
<SuggestedPriceRows ev={ev} readOnly={readOnly} />
|
<SuggestedPriceRows ev={ev} />
|
||||||
|
|
||||||
{/* 4. Bảng so sánh giá */}
|
{/* 4. Bảng so sánh giá */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1700,7 +1700,7 @@ const APPROVED_PRICE_SOURCE_LABEL: Record<string, string> = {
|
|||||||
ProMax: 'PRO Max',
|
ProMax: 'PRO Max',
|
||||||
Ccm: 'CCM',
|
Ccm: 'CCM',
|
||||||
}
|
}
|
||||||
function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
function SuggestedPriceRows({ ev }: { ev: PeDetailBundle }) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const invalidate = () => {
|
const invalidate = () => {
|
||||||
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
|
||||||
@ -1722,8 +1722,13 @@ function SuggestedPriceRows({ ev, readOnly }: { ev: PeDetailBundle; readOnly: bo
|
|||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
|
|
||||||
const canEditPro = !readOnly && ev.canEditProSuggestedPrice
|
// [S89 anh Kiệt FDC bug] Giá đề xuất PRO/CCM editable theo ROLE-capability đơn thuần
|
||||||
const canEditCcm = !readOnly && ev.canEditCcmSuggestedPrice
|
// (mirror ô ngân sách Block A/B dùng bs.canEditPro/canEditCcm — KHÔNG gắn !readOnly).
|
||||||
|
// BE setter fail-closed theo role (Forbidden) + "không ràng Phase, chỉnh như tài liệu
|
||||||
|
// sống" → CCM/PRO nhập được MỌI view (kể cả preview + Duyệt chưa-tới-lượt readOnly=true),
|
||||||
|
// không còn kẹt read-only khi xem phiếu ngoài đúng lượt duyệt của mình.
|
||||||
|
const canEditPro = ev.canEditProSuggestedPrice
|
||||||
|
const canEditCcm = ev.canEditCcmSuggestedPrice
|
||||||
|
|
||||||
// Ghi chú PRO/CCM inline-edit state (Textarea). Echo cùng body absolute-set khi lưu giá.
|
// Ghi chú PRO/CCM inline-edit state (Textarea). Echo cùng body absolute-set khi lưu giá.
|
||||||
const [proNoteText, setProNoteText] = useState(ev.proSuggestedPriceNote ?? '')
|
const [proNoteText, setProNoteText] = useState(ev.proSuggestedPriceNote ?? '')
|
||||||
@ -1981,7 +1986,7 @@ function ThongTinChonThauSection({ ev, readOnly = false }: { ev: PeDetailBundle;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2 – 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
|
{/* 2 – 3. Giá đề xuất PRO / CCM (ngoài giá chào thầu) */}
|
||||||
<SuggestedPriceRows ev={ev} readOnly={readOnly} />
|
<SuggestedPriceRows ev={ev} />
|
||||||
|
|
||||||
{/* 4. Bảng so sánh giá */}
|
{/* 4. Bảng so sánh giá */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user