+ {!projectId
+ ? 'Chọn dự án trước để xem ngân sách khả dụng.'
+ : eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+
+
+
+ {isDraft ? (
+ <>
+
+
+ {eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+ {!form.projectId
+ ? 'Chọn dự án trước để xem ngân sách khả dụng.'
+ : eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+ {!projectId
+ ? 'Chọn dự án trước để xem ngân sách khả dụng.'
+ : eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+
+
+
+ {isDraft ? (
+ <>
+
+
+ {eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+ {!form.projectId
+ ? 'Chọn dự án trước để xem ngân sách khả dụng.'
+ : eligibleBudgets.data && eligibleBudgets.data.length === 0
+ ? 'Dự án này chưa có ngân sách đã duyệt.'
+ : 'Chỉ list ngân sách đã duyệt cùng dự án.'}
+
+
+
ActivePhases,
List NextPhases);
+// Snapshot ngân sách link cho PE / Contract DetailBundle. Compact — chỉ
+// header info, không bao gồm BudgetDetails (gọi /budgets/{id} riêng nếu cần
+// đối chiếu chi tiết theo từng GroupCode/ItemCode).
+public record BudgetSummaryDto(
+ Guid Id,
+ string? MaNganSach,
+ string TenNganSach,
+ int NamNganSach,
+ BudgetPhase Phase,
+ decimal TongNganSach);
+
public record BudgetDetailBundleDto(
Guid Id,
string? MaNganSach,
diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs
index 30d47b7..5d8274a 100644
--- a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs
+++ b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs
@@ -24,7 +24,8 @@ public record CreateContractCommand(
string? TenHopDong,
string? NoiDung,
bool BypassProcurementAndCCM,
- string? DraftData) : IRequest;
+ string? DraftData,
+ Guid? BudgetId) : IRequest;
public class CreateContractCommandValidator : AbstractValidator
{
@@ -59,6 +60,18 @@ public class CreateContractCommandHandler(
.Select(w => (Guid?)w.Id)
.FirstOrDefaultAsync(ct);
+ // Validate Budget link nếu có: cùng Project + Phase=DaDuyet.
+ if (request.BudgetId is Guid bid)
+ {
+ var bg = await db.Budgets.AsNoTracking()
+ .FirstOrDefaultAsync(b => b.Id == bid, ct)
+ ?? throw new NotFoundException("Budget", bid);
+ if (bg.ProjectId != request.ProjectId)
+ throw new ConflictException("Ngân sách phải cùng dự án với HĐ.");
+ if (bg.Phase != Domain.Budgets.BudgetPhase.DaDuyet)
+ throw new ConflictException("Chỉ link được ngân sách đã duyệt.");
+ }
+
var entity = new Contract
{
Type = request.Type,
@@ -73,6 +86,7 @@ public class CreateContractCommandHandler(
NoiDung = request.NoiDung,
BypassProcurementAndCCM = request.BypassProcurementAndCCM,
DraftData = request.DraftData,
+ BudgetId = request.BudgetId,
WorkflowDefinitionId = activeWfId,
SlaDeadline = DateTime.UtcNow.Add(workflow.GetPhaseSla(ContractPhase.DangSoanThao) ?? TimeSpan.FromDays(7)),
};
@@ -106,7 +120,8 @@ public record UpdateContractDraftCommand(
string? TenHopDong,
string? NoiDung,
Guid? TemplateId,
- string? DraftData) : IRequest;
+ string? DraftData,
+ Guid? BudgetId) : IRequest;
public class UpdateContractDraftCommandHandler(
IApplicationDbContext db,
@@ -120,6 +135,18 @@ public class UpdateContractDraftCommandHandler(
if (entity.Phase != ContractPhase.DangSoanThao)
throw new ConflictException("Chỉ được sửa HĐ khi ở phase Đang soạn thảo.");
+ // Validate Budget link nếu thay đổi.
+ if (request.BudgetId is Guid bid && bid != entity.BudgetId)
+ {
+ var bg = await db.Budgets.AsNoTracking()
+ .FirstOrDefaultAsync(b => b.Id == bid, ct)
+ ?? throw new NotFoundException("Budget", bid);
+ if (bg.ProjectId != entity.ProjectId)
+ throw new ConflictException("Ngân sách phải cùng dự án với HĐ.");
+ if (bg.Phase != Domain.Budgets.BudgetPhase.DaDuyet)
+ throw new ConflictException("Chỉ link được ngân sách đã duyệt.");
+ }
+
// Capture diff trước update để log
var changes = new List