[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
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:
@ -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ồ sơ</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 (
|
||||
|
||||
@ -58,6 +58,8 @@ export function PeWorkspaceCreateView({
|
||||
diaDiem: '',
|
||||
moTa: '',
|
||||
paymentTerms: '',
|
||||
// anh Kiệt FDC — link thư mục hồ sơ trên NAS (1 cột HoSoLink, JSON hoSoLink).
|
||||
hoSoLink: '',
|
||||
// [S61 Mig 50] "Ngân sách - kỳ này" — thay budgetId/budgetManual* (module
|
||||
// Budget cũ xóa hẳn; bảng Tổng hợp ngân sách gói thầu ở PeDetailTabs).
|
||||
budgetPeriodAmount: 0,
|
||||
@ -115,6 +117,7 @@ export function PeWorkspaceCreateView({
|
||||
diaDiem: form.diaDiem || null,
|
||||
moTa: form.moTa || null,
|
||||
paymentTerms: null, // S59 vòng 5: field gỡ khỏi form
|
||||
hoSoLink: form.hoSoLink || null,
|
||||
approvalWorkflowId: form.approvalWorkflowId || null,
|
||||
...budgetPayload,
|
||||
})
|
||||
@ -275,6 +278,21 @@ export function PeWorkspaceCreateView({
|
||||
label="d. Bản so sánh"
|
||||
value={<LockedHint text="Tải bảng so sánh sau khi tạo phiếu." />}
|
||||
/>
|
||||
|
||||
{/* e. Link hồ sơ (anh Kiệt FDC) — dán link thư mục hồ sơ trên NAS công ty
|
||||
(1 cột HoSoLink). Create = Input; khi xem phiếu render thẻ <a> bấm-mở. */}
|
||||
<div className="flex gap-3">
|
||||
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">e. Link hồ sơ</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Input
|
||||
type="url"
|
||||
value={form.hoSoLink}
|
||||
onChange={e => setForm({ ...form, hoSoLink: e.target.value })}
|
||||
placeholder="Dán link thư mục hồ sơ trên NAS..."
|
||||
className="max-w-2xl text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
|
||||
@ -422,6 +422,9 @@ export type PeDetailBundle = {
|
||||
selectedSupplierName: string | null
|
||||
contractId: string | null
|
||||
paymentTerms: string | null
|
||||
// anh Kiệt FDC — 1 hyperlink tới thư mục hồ sơ trên NAS công ty (string? nullable,
|
||||
// BE HasMaxLength 1000). JSON camelCase "hoSoLink". KHÔNG entity con, dùng 1 cột.
|
||||
hoSoLink: string | null
|
||||
slaDeadline: string | null
|
||||
createdAt: string
|
||||
updatedAt: string | null
|
||||
|
||||
@ -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ồ sơ</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 (
|
||||
|
||||
@ -58,6 +58,8 @@ export function PeWorkspaceCreateView({
|
||||
diaDiem: '',
|
||||
moTa: '',
|
||||
paymentTerms: '',
|
||||
// anh Kiệt FDC — link thư mục hồ sơ trên NAS (1 cột HoSoLink, JSON hoSoLink).
|
||||
hoSoLink: '',
|
||||
// [S61 Mig 50] "Ngân sách - kỳ này" — thay budgetId/budgetManual* (module
|
||||
// Budget cũ xóa hẳn; bảng Tổng hợp ngân sách gói thầu ở PeDetailTabs).
|
||||
budgetPeriodAmount: 0,
|
||||
@ -115,6 +117,7 @@ export function PeWorkspaceCreateView({
|
||||
diaDiem: form.diaDiem || null,
|
||||
moTa: form.moTa || null,
|
||||
paymentTerms: null, // S59 vòng 5: field gỡ khỏi form
|
||||
hoSoLink: form.hoSoLink || null,
|
||||
approvalWorkflowId: form.approvalWorkflowId || null,
|
||||
...budgetPayload,
|
||||
})
|
||||
@ -275,6 +278,21 @@ export function PeWorkspaceCreateView({
|
||||
label="d. Bản so sánh"
|
||||
value={<LockedHint text="Tải bảng so sánh sau khi tạo phiếu." />}
|
||||
/>
|
||||
|
||||
{/* e. Link hồ sơ (anh Kiệt FDC) — dán link thư mục hồ sơ trên NAS công ty
|
||||
(1 cột HoSoLink). Create = Input; khi xem phiếu render thẻ <a> bấm-mở. */}
|
||||
<div className="flex gap-3">
|
||||
<span className="w-44 shrink-0 pt-1.5 text-[12px] text-slate-500">e. Link hồ sơ</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Input
|
||||
type="url"
|
||||
value={form.hoSoLink}
|
||||
onChange={e => setForm({ ...form, hoSoLink: e.target.value })}
|
||||
placeholder="Dán link thư mục hồ sơ trên NAS..."
|
||||
className="max-w-2xl text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
|
||||
@ -424,6 +424,9 @@ export type PeDetailBundle = {
|
||||
selectedSupplierName: string | null
|
||||
contractId: string | null
|
||||
paymentTerms: string | null
|
||||
// anh Kiệt FDC — 1 hyperlink tới thư mục hồ sơ trên NAS công ty (string? nullable,
|
||||
// BE HasMaxLength 1000). JSON camelCase "hoSoLink". KHÔNG entity con, dùng 1 cột.
|
||||
hoSoLink: string | null
|
||||
slaDeadline: string | null
|
||||
createdAt: string
|
||||
updatedAt: string | null
|
||||
|
||||
@ -206,6 +206,8 @@ public record PurchaseEvaluationDetailBundleDto(
|
||||
string TenGoiThau,
|
||||
string? DiaDiem,
|
||||
string? MoTa,
|
||||
// [HoSoLink] hyperlink thư mục hồ sơ NAS — FE render hyperlink bấm-mở.
|
||||
string? HoSoLink,
|
||||
Guid ProjectId,
|
||||
string ProjectName,
|
||||
// [Mig 49 S57bis] Hạng mục công việc — loose-Guid resolve giống ProjectName.
|
||||
|
||||
@ -25,7 +25,8 @@ public record CreatePurchaseEvaluationCommand(
|
||||
string? PaymentTerms,
|
||||
decimal? BudgetPeriodAmount, // [S61 Mig 50] "Ngân sách - kỳ này" — drafter nhập, optional lúc tạo (guard lúc submit)
|
||||
Guid? ApprovalWorkflowId = null, // [Mig 23] User chọn quy trình duyệt V2 lúc tạo
|
||||
Guid? WorkItemId = null) : IRequest<Guid>; // [Mig 49 S57bis] Hạng mục công việc — flow create PHẢI chọn (validator NotEmpty)
|
||||
Guid? WorkItemId = null, // [Mig 49 S57bis] Hạng mục công việc — flow create PHẢI chọn (validator NotEmpty)
|
||||
string? HoSoLink = null) : IRequest<Guid>; // [HoSoLink] hyperlink thư mục hồ sơ NAS — optional, max 1000
|
||||
|
||||
public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<CreatePurchaseEvaluationCommand>
|
||||
{
|
||||
@ -42,6 +43,8 @@ public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<Create
|
||||
.WithMessage("Phải chọn hạng mục công việc.");
|
||||
RuleFor(x => x.DiaDiem).MaximumLength(500);
|
||||
RuleFor(x => x.MoTa).MaximumLength(2000);
|
||||
// [HoSoLink] MaxLength MATCH EF config HasMaxLength(1000) (S35 lesson).
|
||||
RuleFor(x => x.HoSoLink).MaximumLength(1000);
|
||||
// [S61] >0 khi có nhập — KHÔNG bắt buộc lúc tạo, submit guard mới chặn.
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThan(0)
|
||||
.When(x => x.BudgetPeriodAmount.HasValue)
|
||||
@ -134,6 +137,7 @@ public class CreatePurchaseEvaluationCommandHandler(
|
||||
DepartmentId = request.DepartmentId,
|
||||
DiaDiem = request.DiaDiem,
|
||||
MoTa = request.MoTa,
|
||||
HoSoLink = request.HoSoLink, // [HoSoLink] hyperlink thư mục hồ sơ NAS
|
||||
DrafterUserId = currentUser.UserId,
|
||||
WorkflowDefinitionId = activeWfId,
|
||||
ApprovalWorkflowId = request.ApprovalWorkflowId, // Mig 23 — schema mới V2
|
||||
@ -208,7 +212,8 @@ public record UpdatePurchaseEvaluationDraftCommand(
|
||||
decimal? BudgetPeriodAmount = null, // [S61] null-safe: null = GIỮ giá trị cũ (chống null-hóa bug-class S42)
|
||||
decimal? ExpectedRemainingAmount = null, // [S61] null-safe: null = GIỮ giá trị cũ
|
||||
Guid? ApprovalWorkflowId = null, // [Mig 23] cho User đổi quy trình khi sửa Nháp
|
||||
Guid? WorkItemId = null) : IRequest; // [Mig 49 S57bis] cho User đổi hạng mục công việc khi sửa Nháp/Trả lại
|
||||
Guid? WorkItemId = null, // [Mig 49 S57bis] cho User đổi hạng mục công việc khi sửa Nháp/Trả lại
|
||||
string? HoSoLink = null) : IRequest; // [HoSoLink] hyperlink thư mục hồ sơ NAS — absolute-set như MoTa/DiaDiem (null = clear link)
|
||||
|
||||
public class UpdatePurchaseEvaluationDraftCommandValidator : AbstractValidator<UpdatePurchaseEvaluationDraftCommand>
|
||||
{
|
||||
@ -217,6 +222,8 @@ public class UpdatePurchaseEvaluationDraftCommandValidator : AbstractValidator<U
|
||||
RuleFor(x => x.TenGoiThau).NotEmpty().MaximumLength(500);
|
||||
RuleFor(x => x.DiaDiem).MaximumLength(500);
|
||||
RuleFor(x => x.MoTa).MaximumLength(2000);
|
||||
// [HoSoLink] MaxLength MATCH EF config HasMaxLength(1000) (S35 lesson).
|
||||
RuleFor(x => x.HoSoLink).MaximumLength(1000);
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThan(0)
|
||||
.When(x => x.BudgetPeriodAmount.HasValue)
|
||||
.WithMessage("Ngân sách kỳ này phải lớn hơn 0.");
|
||||
@ -264,6 +271,7 @@ public class UpdatePurchaseEvaluationDraftCommandHandler(
|
||||
entity.TenGoiThau = request.TenGoiThau;
|
||||
entity.DiaDiem = request.DiaDiem;
|
||||
entity.MoTa = request.MoTa;
|
||||
entity.HoSoLink = request.HoSoLink; // [HoSoLink] absolute-set như MoTa/DiaDiem (Section 1 text field, null = clear)
|
||||
entity.PaymentTerms = request.PaymentTerms;
|
||||
entity.ApprovalWorkflowId = request.ApprovalWorkflowId; // Mig 23 — User đổi quy trình
|
||||
// [S61] null-safe 2 field ngân sách mới (mirror WorkItemId bên dưới):
|
||||
@ -1028,6 +1036,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
|
||||
return new PurchaseEvaluationDetailBundleDto(
|
||||
e.Id, e.MaPhieu, e.Type, e.Phase, e.TenGoiThau, e.DiaDiem, e.MoTa,
|
||||
e.HoSoLink, // [HoSoLink] hyperlink thư mục hồ sơ NAS
|
||||
e.ProjectId, project?.Name ?? "",
|
||||
e.WorkItemId, workItem?.Name, workItem?.Code,
|
||||
e.DepartmentId, department?.Name,
|
||||
|
||||
@ -18,6 +18,7 @@ public class PurchaseEvaluation : AuditableEntity
|
||||
public Guid? DrafterUserId { get; set; } // QS/NV.PB soạn
|
||||
public string? DiaDiem { get; set; } // Lô K, KCN Lộc An...
|
||||
public string? MoTa { get; set; }
|
||||
public string? HoSoLink { get; set; } // [HoSoLink] 1 hyperlink tới thư mục hồ sơ trên NAS công ty (anh Kiệt) — paste link thư mục, FE render hyperlink bấm-mở. Nullable, max 1000. KHÔNG entity con / bảng mới.
|
||||
|
||||
public Guid? WorkflowDefinitionId { get; set; } // [LEGACY Mig 21] Pinned at create — config y như HĐ
|
||||
public Guid? ApprovalWorkflowId { get; set; } // [Mig 23 Session 17] Pin schema mới ApprovalWorkflowsV2
|
||||
|
||||
@ -18,6 +18,8 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
|
||||
b.Property(x => x.TenGoiThau).HasMaxLength(500).IsRequired();
|
||||
b.Property(x => x.DiaDiem).HasMaxLength(500);
|
||||
b.Property(x => x.MoTa).HasMaxLength(2000);
|
||||
// [HoSoLink] hyperlink thư mục hồ sơ NAS — nvarchar(1000) nullable, KHÔNG index.
|
||||
b.Property(x => x.HoSoLink).HasMaxLength(1000);
|
||||
b.Property(x => x.PaymentTerms).HasColumnType("nvarchar(max)");
|
||||
// [S61 Mig 50] 2 cột ngân sách mới thay BudgetManual* — precision giữ (18,2).
|
||||
b.Property(x => x.BudgetPeriodAmount).HasPrecision(18, 2);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHoSoLinkToPurchaseEvaluation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "HoSoLink",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "nvarchar(1000)",
|
||||
maxLength: 1000,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HoSoLink",
|
||||
table: "PurchaseEvaluations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4617,6 +4617,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("HoSoLink")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user