[CLAUDE] PurchaseEvaluation: +mục E "Link hồ sơ" (hyperlink NAS) + rename "Dự trù PRO"->"Ngân sách PRO"
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m25s

Anh Kiệt (FDC UAT): (1) thêm mục "e. Link hồ sơ" dưới mục "d. Bản so sánh" — 1 ô dán
hyperlink tới thư mục hồ sơ trên NAS công ty, hiện dạng <a> bấm-mở (target _blank rel
noopener noreferrer), null-safe; (2) đổi nhãn "Dự trù PRO"->"Ngân sách PRO" (cả badge
+ row label; GIỮ "Ghi chú từ PRO" + field-code/biến).
- BE: PurchaseEvaluation +HoSoLink string? (Mig AddHoSoLinkToPurchaseEvaluation —
  nvarchar(1000) nullable, no new table, Down reversible) + Create/Update command
  (+trailing optional =null -> backward-compat, 0 call-site break) + Detail DTO + projection.
  Build slnx PASS.
- FE x2 app SHA256 mirror (PeDetailTabs + PeWorkspaceCreateView): mục E input/hyperlink +
  rename. types +hoSoLink.
Workflow fan-out (BE song song FE -> review); FE+reviewer return-rỗng -> em main recover
disk + build-verify x3 + self-gate (bắt badge sót rename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-16 11:13:39 +07:00
parent 318860a38e
commit 5a0aaa4e83
13 changed files with 6458 additions and 10 deletions

View File

@ -993,7 +993,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
const proMut = useMutation({
mutationFn: async (body: { proEstimateAmount: number | null; proNote: string | null }) =>
api.put(`/purchase-evaluations/${ev.id}/budget/pro`, body),
onSuccess: () => { toast.success('Đã lưu dự trù PRO'); invalidate() },
onSuccess: () => { toast.success('Đã lưu ngân sách PRO'); invalidate() },
onError: e => toast.error(getErrorMessage(e)),
})
// PUT /budget/ccm — chỉ khi canEditCcm. initialAmount + adjustmentAmount.
@ -1075,7 +1075,7 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
<span className="inline-flex items-center gap-2">
Ngân sách (full gói thầu)
{bs.fullIsEstimate && (
<span className="rounded bg-white/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase">dự trù PRO</span>
<span className="rounded bg-white/20 px-1.5 py-0.5 text-[9px] font-semibold uppercase">ngân sách PRO</span>
)}
</span>
}
@ -1117,13 +1117,13 @@ function PeBudgetSummaryTable({ ev, readOnly }: { ev: PeDetailBundle; readOnly:
{/* Dòng 4 — Dự trù PRO (PRO editable) */}
<BudgetRow
label="Dự trù PRO"
label="Ngân sách PRO"
value={
bs.canEditPro ? (
<VndInlineEdit
initial={bs.proEstimateAmount}
saving={proMut.isPending}
label="Dự trù PRO"
label="Ngân sách PRO"
onSave={v => proMut.mutate({ proEstimateAmount: v, proNote: proNoteText || null })}
/>
) : bs.proEstimateAmount != null ? fmtVnd(bs.proEstimateAmount) : <span className="text-slate-400"></span>
@ -1347,6 +1347,11 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
</div>
</div>
{/* e. Link hồ sơ (anh Kiệt FDC) — 1 hyperlink tới thư mục hồ sơ trên NAS công ty.
Read-only: render thẻ <a> bấm-mở (target=_blank). Editable: Input dán URL +
nút Lưu (PUT /purchase-evaluations/:id echo field bắt buộc + hoSoLink). */}
<HoSoLinkRow ev={ev} readOnly={readOnly} />
{ev.paymentTerms && (
<FormRow label="Điều khoản thanh toán" value={<span className="whitespace-pre-wrap">{ev.paymentTerms}</span>} />
)}
@ -1374,6 +1379,76 @@ function ChonNccSection({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly
)
}
// e. Link hồ sơ — 1 cột HoSoLink (string? nullable) trỏ thư mục hồ sơ NAS.
// Read-only: thẻ <a> bấm-mở. Editable (phiếu DangSoanThao/TraLai + !readOnly):
// Input dán URL + nút Lưu. Save = PUT /purchase-evaluations/:id echo field bắt
// buộc (tenGoiThau + 2 ô ngân sách) như InfoTab.save để không xóa nhầm data.
function HoSoLinkRow({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) {
const canEdit = !readOnly && isEditablePhase(ev.phase)
const qc = useQueryClient()
const [hoSoLink, setHoSoLink] = useState(ev.hoSoLink ?? '')
useEffect(() => { setHoSoLink(ev.hoSoLink ?? '') }, [ev.id, ev.hoSoLink])
const dirty = hoSoLink !== (ev.hoSoLink ?? '')
const save = useMutation({
mutationFn: async () => {
await api.put(`/purchase-evaluations/${ev.id}`, {
id: ev.id,
tenGoiThau: ev.tenGoiThau,
diaDiem: ev.diaDiem,
moTa: ev.moTa,
paymentTerms: ev.paymentTerms,
budgetPeriodAmount: ev.budgetPeriodAmount,
expectedRemainingAmount: ev.expectedRemainingAmount,
hoSoLink: hoSoLink || null,
})
},
onSuccess: () => {
toast.success('Đã lưu link hồ sơ')
qc.invalidateQueries({ queryKey: ['pe-detail', ev.id] })
qc.invalidateQueries({ queryKey: ['pe-list'] })
},
onError: e => toast.error(getErrorMessage(e)),
})
return (
<div className="flex gap-3">
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">e. Link hồ </span>
<div className="min-w-0 flex-1">
{canEdit ? (
<div className="flex max-w-2xl items-center gap-2">
<Input
type="url"
value={hoSoLink}
onChange={e => setHoSoLink(e.target.value)}
placeholder="Dán link thư mục hồ sơ trên NAS..."
className="text-sm"
/>
<Button
onClick={() => save.mutate()}
disabled={!dirty || save.isPending}
className="h-9 shrink-0 px-3 text-xs"
>
{save.isPending ? 'Đang lưu…' : 'Lưu'}
</Button>
</div>
) : ev.hoSoLink ? (
<a
href={ev.hoSoLink}
target="_blank"
rel="noopener noreferrer"
className="break-all text-sm text-brand-600 hover:underline"
>
{ev.hoSoLink}
</a>
) : (
<span className="text-sm text-slate-400"></span>
)}
</div>
</div>
)
}
// 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 (