[CLAUDE] PurchaseEvaluation: Mig 55 ô "Ghi chú từ CCM" ngân sách gói thầu — CCM nhập số + ghi lý do giống PRO
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m54s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m54s
- Entity PeWorkItemBudget +CcmNote (mirror ProNote, nvarchar 1000) + Mig 55 additive-nullable - UpdatePeBudgetCcmCommand +CcmNote absolute-set, role-gate CostControl/Admin fail-closed - DTO PeBudgetSummaryDto +CcmNote + controller BudgetCcmBody + GET mapping - FE 2 app SHA-mirror: dòng "Ghi chú từ CCM" gate canEditCcm (sau V0/hiệu chỉnh), absolute-set đủ 3 field - Test +5 (set CCM/Admin, null-clear, non-priv Forbidden, all-3-persist) -> 339 pass Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1079,7 +1079,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
|
||||
})
|
||||
// PUT /budget/ccm — chỉ khi canEditCcm. initialAmount + adjustmentAmount.
|
||||
const ccmMut = useMutation({
|
||||
mutationFn: async (body: { initialAmount: number | null; adjustmentAmount: number | null }) =>
|
||||
mutationFn: async (body: { initialAmount: number | null; adjustmentAmount: number | null; ccmNote: string | null }) =>
|
||||
api.put(`/purchase-evaluations/${ev.id}/budget/ccm`, body),
|
||||
onSuccess: () => { toast.success('Đã lưu ngân sách ban hành'); invalidate() },
|
||||
onError: e => toast.error(getErrorMessage(e)),
|
||||
@ -1097,6 +1097,9 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
|
||||
// proNote inline-edit state (Textarea — không dùng VndInlineEdit)
|
||||
const [proNoteText, setProNoteText] = useState(bs?.proNote ?? '')
|
||||
useEffect(() => { setProNoteText(bs?.proNote ?? '') }, [bs?.proNote])
|
||||
// ccmNote inline-edit state (mirror proNoteText) — [Mig anh Kiệt FDC]
|
||||
const [ccmNoteText, setCcmNoteText] = useState(bs?.ccmNote ?? '')
|
||||
useEffect(() => { setCcmNoteText(bs?.ccmNote ?? '') }, [bs?.ccmNote])
|
||||
|
||||
// Phiếu cũ chưa gắn Hạng mục công việc → budgetSummary null.
|
||||
if (!bs) {
|
||||
@ -1172,7 +1175,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
|
||||
initial={bs.initialAmount}
|
||||
saving={ccmMut.isPending}
|
||||
label="Ngân sách ban hành lần đầu"
|
||||
onSave={v => ccmMut.mutate({ initialAmount: v, adjustmentAmount: bs.adjustmentAmount })}
|
||||
onSave={v => ccmMut.mutate({ initialAmount: v, adjustmentAmount: bs.adjustmentAmount, ccmNote: bs.ccmNote })}
|
||||
/>
|
||||
) : bs.initialAmount != null ? fmtVnd(bs.initialAmount) : <span className="text-slate-400">—</span>
|
||||
}
|
||||
@ -1188,7 +1191,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
|
||||
allowNegative
|
||||
saving={ccmMut.isPending}
|
||||
label="Ngân sách hiệu chỉnh tăng giảm"
|
||||
onSave={v => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: v })}
|
||||
onSave={v => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: v, ccmNote: bs.ccmNote })}
|
||||
/>
|
||||
) : bs.adjustmentAmount != null ? (
|
||||
<span className={cn(bs.adjustmentAmount < 0 && 'text-red-600')}>{fmtVndSigned(bs.adjustmentAmount)}</span>
|
||||
@ -1196,6 +1199,37 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Ghi chú từ CCM (CCM editable — Textarea, mirror Ghi chú từ PRO) — [Mig anh Kiệt FDC] */}
|
||||
<div className="flex items-start gap-2 border-b border-slate-100 px-3 py-1.5 text-[13px]">
|
||||
<div className="min-w-0 flex-1 text-slate-700">Ghi chú từ CCM</div>
|
||||
<div className="w-72 shrink-0">
|
||||
{bs.canEditCcm ? (
|
||||
<div className="space-y-1">
|
||||
<textarea
|
||||
value={ccmNoteText}
|
||||
onChange={e => setCcmNoteText(e.target.value)}
|
||||
placeholder="Ghi chú ngân sách CCM…"
|
||||
rows={2}
|
||||
className="w-full rounded border border-slate-300 px-2 py-1 text-[12px]"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => ccmMut.mutate({ initialAmount: bs.initialAmount, adjustmentAmount: bs.adjustmentAmount, ccmNote: ccmNoteText || null })}
|
||||
disabled={ccmNoteText === (bs.ccmNote ?? '') || ccmMut.isPending}
|
||||
className="h-6 px-2 text-[11px]"
|
||||
>
|
||||
{ccmMut.isPending ? '…' : 'Lưu ghi chú'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap text-right text-[12px] text-slate-600">
|
||||
{bs.ccmNote || <span className="text-slate-400">—</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dòng 4 — Dự trù PRO (PRO editable) */}
|
||||
<BudgetRow
|
||||
label="Ngân sách PRO"
|
||||
|
||||
@ -295,6 +295,7 @@ export type PeBudgetSummary = {
|
||||
proNote: string | null
|
||||
initialAmount: number | null
|
||||
adjustmentAmount: number | null // CCM "NS V0/hiệu chỉnh" — cho phép ÂM
|
||||
ccmNote: string | null // [Mig — anh Kiệt FDC] Ghi chú từ CCM (mirror proNote)
|
||||
fullAmount: number
|
||||
fullIsEstimate: boolean
|
||||
canEditPro: boolean
|
||||
|
||||
Reference in New Issue
Block a user