[CLAUDE] Domain+App+Api: Module Ngan sach (Budget) - 4 bang + workflow simple
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
User request: 'Them cho tao 4 bang luu ve ngan sach: Header / Chi tiet
/ Quy trinh duyet / Lich su thay doi'.
Domain (5 file + 1 enum):
- Budget (header) — Aggregate root, AuditableEntity. Field: MaNganSach,
TenNganSach, Description, NamNganSach, ProjectId FK, DepartmentId?,
DrafterUserId, Phase (BudgetPhase 5-state), TongNganSach (sum auto
tu Details), SlaDeadline, SlaWarningSent.
- BudgetDetail — flat row pattern (GroupCode/GroupName + Item +
KhoiLuong/DonGia/ThanhTien). 18,4 precision KhoiLuong, 18,2 money.
- BudgetApproval — workflow history (FromPhase/ToPhase/Decision/Comment)
- BudgetChangelog — audit log unified (EntityType: Header/Detail/Workflow)
- BudgetPhase enum 5 state: DangSoanThao(1) → ChoCCM(2) → ChoCEO(3) →
DaDuyet(4) | TuChoi(99)
- BudgetPolicy hardcoded (no versioned WF, simple default per user
confirm 'tam thoi don gian'): Drafter/DeptManager → CCM → CEO/
AuthorizedSigner. Reject path back to DangSoanThao.
Migration 14 AddBudgets:
- 4 bang moi: Budgets + BudgetDetails + BudgetApprovals + BudgetChangelogs
- Index: Phase+IsDeleted, ProjectId, NamNganSach, SlaDeadline,
MaNganSach unique filtered. Cascade delete child.
- +2 cot FK ngoai bang (per user 'lien ket ca 3'):
* Contracts.BudgetId Guid? + index
* PurchaseEvaluations.BudgetId Guid? + index
Cho phep doi chieu chi phi HD/PE vs ngan sach goi thau.
Application CQRS (BudgetFeatures.cs ~340 line):
- CreateBudget + UpdateBudgetDraft + TransitionBudget + ListBudgets
(filter Phase/Project/Year + search + paging) + GetBudget bundle
(Header + Details + Approvals + Workflow summary)
- DeleteBudget (only DangSoanThao/TuChoi)
- AddBudgetDetail + UpdateBudgetDetail + DeleteBudgetDetail (auto
recompute TongNganSach = sum Details.ThanhTien)
- ListBudgetChangelogs
Api: BudgetsController 11 endpoint REST /api/budgets:
- GET / /{id} /{id}/changelogs
- POST / /{id}/transitions /{id}/details
- PUT /{id} /{id}/details/{detailId}
- DELETE /{id} /{id}/details/{detailId}
DbContext + IApplicationDbContext: 4 DbSet new (Budgets/Details/
Approvals/Changelogs).
MenuKeys + DbInitializer: 4 menu key (Budgets root + Bg_List/Create/
Pending leaves) seed dau order=27 'Ngan sach' icon Wallet. Auto-grant
admin permission via SeedAdminPermissionsAsync (MenuKeys.All).
MaNganSach format don gian 'NS-YYYYMM-NNNN' Random.Shared (chua atomic
sequence - user said 'tam thoi chua co').
Workflow chua versioned, hardcode BudgetPolicy.Default. Tuong lai admin
config qua UI: them BudgetWorkflowDefinition tables tuong tu PE.
This commit is contained in:
388
src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs
Normal file
388
src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs
Normal file
@ -0,0 +1,388 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Budgets.Dtos;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Common.Models;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts; // ApprovalDecision + ChangelogAction
|
||||
using SolutionErp.Domain.Identity;
|
||||
|
||||
namespace SolutionErp.Application.Budgets;
|
||||
|
||||
// Compact CQRS feature file cho module Ngân sách. Pattern simplified vs PE
|
||||
// (no versioned WF, hardcoded BudgetPolicy.Default). Workflow 5 phase:
|
||||
// DangSoanThao → ChoCCM → ChoCEO → DaDuyet/TuChoi.
|
||||
|
||||
// ========== CREATE ==========
|
||||
|
||||
public record CreateBudgetCommand(
|
||||
string TenNganSach,
|
||||
string? Description,
|
||||
int NamNganSach,
|
||||
Guid ProjectId,
|
||||
Guid? DepartmentId) : IRequest<Guid>;
|
||||
|
||||
public class CreateBudgetCommandValidator : AbstractValidator<CreateBudgetCommand>
|
||||
{
|
||||
public CreateBudgetCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.TenNganSach).NotEmpty().MaximumLength(500);
|
||||
RuleFor(x => x.Description).MaximumLength(2000);
|
||||
RuleFor(x => x.ProjectId).NotEmpty();
|
||||
RuleFor(x => x.NamNganSach).InclusiveBetween(2020, 2100);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateBudgetCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<CreateBudgetCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreateBudgetCommand request, CancellationToken ct)
|
||||
{
|
||||
_ = await db.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, ct)
|
||||
?? throw new NotFoundException("Project", request.ProjectId);
|
||||
|
||||
var sla = BudgetPolicies.Default.PhaseSla.GetValueOrDefault(BudgetPhase.DangSoanThao);
|
||||
var entity = new Budget
|
||||
{
|
||||
TenNganSach = request.TenNganSach,
|
||||
Description = request.Description,
|
||||
NamNganSach = request.NamNganSach,
|
||||
ProjectId = request.ProjectId,
|
||||
DepartmentId = request.DepartmentId,
|
||||
DrafterUserId = currentUser.UserId,
|
||||
Phase = BudgetPhase.DangSoanThao,
|
||||
TongNganSach = 0,
|
||||
SlaDeadline = sla is null ? null : DateTime.UtcNow.Add(sla.Value),
|
||||
// Auto-gen MaNganSach đơn giản — atomic sequence sau (Phase 8)
|
||||
MaNganSach = $"NS-{DateTime.UtcNow:yyyyMM}-{Random.Shared.Next(1000, 9999)}",
|
||||
};
|
||||
db.Budgets.Add(entity);
|
||||
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Header,
|
||||
Action = ChangelogAction.Insert,
|
||||
PhaseAtChange = entity.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = $"Tạo ngân sách {entity.MaNganSach} — {entity.TenNganSach}",
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UPDATE draft ==========
|
||||
|
||||
public record UpdateBudgetDraftCommand(
|
||||
Guid Id,
|
||||
string TenNganSach,
|
||||
string? Description,
|
||||
int NamNganSach) : IRequest;
|
||||
|
||||
public class UpdateBudgetDraftCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<UpdateBudgetDraftCommand>
|
||||
{
|
||||
public async Task Handle(UpdateBudgetDraftCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
if (entity.Phase != BudgetPhase.DangSoanThao)
|
||||
throw new ConflictException("Chỉ sửa được ngân sách khi Đang soạn thảo.");
|
||||
|
||||
entity.TenNganSach = request.TenNganSach;
|
||||
entity.Description = request.Description;
|
||||
entity.NamNganSach = request.NamNganSach;
|
||||
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Header,
|
||||
Action = ChangelogAction.Update,
|
||||
PhaseAtChange = entity.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = "Cập nhật ngân sách",
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TRANSITION ==========
|
||||
|
||||
public record TransitionBudgetCommand(
|
||||
Guid Id, BudgetPhase TargetPhase, ApprovalDecision Decision, string? Comment) : IRequest;
|
||||
|
||||
public class TransitionBudgetCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser,
|
||||
UserManager<User> userManager) : IRequestHandler<TransitionBudgetCommand>
|
||||
{
|
||||
public async Task Handle(TransitionBudgetCommand request, CancellationToken ct)
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || currentUser.UserId is null)
|
||||
throw new UnauthorizedException();
|
||||
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
|
||||
|
||||
if (!isAdmin && !policy.IsTransitionAllowed(entity.Phase, request.TargetPhase, currentUser.Roles))
|
||||
throw new ForbiddenException(
|
||||
$"Role không đủ quyền chuyển {entity.Phase} → {request.TargetPhase}.");
|
||||
|
||||
var fromPhase = entity.Phase;
|
||||
entity.SlaWarningSent = false;
|
||||
entity.Phase = request.TargetPhase;
|
||||
var sla = policy.PhaseSla.GetValueOrDefault(request.TargetPhase);
|
||||
entity.SlaDeadline = sla is null ? null : DateTime.UtcNow.Add(sla.Value);
|
||||
|
||||
db.BudgetApprovals.Add(new BudgetApproval
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
FromPhase = fromPhase,
|
||||
ToPhase = request.TargetPhase,
|
||||
ApproverUserId = currentUser.UserId,
|
||||
Decision = request.Decision,
|
||||
Comment = request.Comment,
|
||||
ApprovedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
string? actorName = null;
|
||||
if (currentUser.UserId is Guid uid)
|
||||
{
|
||||
var u = await userManager.FindByIdAsync(uid.ToString());
|
||||
actorName = u?.FullName ?? u?.Email;
|
||||
}
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Workflow,
|
||||
Action = ChangelogAction.Transition,
|
||||
PhaseAtChange = request.TargetPhase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = actorName ?? "Hệ thống",
|
||||
Summary = $"Chuyển phase {fromPhase} → {request.TargetPhase}",
|
||||
ContextNote = request.Comment,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== LIST ==========
|
||||
|
||||
public record ListBudgetsQuery(
|
||||
BudgetPhase? Phase = null,
|
||||
Guid? ProjectId = null,
|
||||
int? NamNganSach = null) : PagedRequest, IRequest<PagedResult<BudgetListItemDto>>;
|
||||
|
||||
public class ListBudgetsQueryHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<ListBudgetsQuery, PagedResult<BudgetListItemDto>>
|
||||
{
|
||||
public async Task<PagedResult<BudgetListItemDto>> Handle(ListBudgetsQuery request, CancellationToken ct)
|
||||
{
|
||||
var q = from e in db.Budgets.AsNoTracking()
|
||||
join p in db.Projects.AsNoTracking() on e.ProjectId equals p.Id
|
||||
select new { e, p };
|
||||
|
||||
if (request.Phase is not null) q = q.Where(x => x.e.Phase == request.Phase);
|
||||
if (request.ProjectId is not null) q = q.Where(x => x.e.ProjectId == request.ProjectId);
|
||||
if (request.NamNganSach is not null) q = q.Where(x => x.e.NamNganSach == request.NamNganSach);
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
var s = request.Search.Trim();
|
||||
q = q.Where(x => (x.e.MaNganSach != null && x.e.MaNganSach.Contains(s))
|
||||
|| x.e.TenNganSach.Contains(s) || x.p.Name.Contains(s));
|
||||
}
|
||||
q = request.SortDesc ? q.OrderByDescending(x => x.e.CreatedAt) : q.OrderBy(x => x.e.CreatedAt);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
|
||||
.Select(x => new BudgetListItemDto(
|
||||
x.e.Id, x.e.MaNganSach, x.e.TenNganSach, x.e.NamNganSach, x.e.Phase,
|
||||
x.e.ProjectId, x.p.Name, x.e.TongNganSach, x.e.SlaDeadline, x.e.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<BudgetListItemDto>(items, total, request.Page, request.PageSize);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GET detail bundle ==========
|
||||
|
||||
public record GetBudgetQuery(Guid Id) : IRequest<BudgetDetailBundleDto>;
|
||||
|
||||
public class GetBudgetQueryHandler(
|
||||
IApplicationDbContext db,
|
||||
UserManager<User> userManager) : IRequestHandler<GetBudgetQuery, BudgetDetailBundleDto>
|
||||
{
|
||||
public async Task<BudgetDetailBundleDto> Handle(GetBudgetQuery request, CancellationToken ct)
|
||||
{
|
||||
var e = await db.Budgets.AsNoTracking()
|
||||
.Include(x => x.Details)
|
||||
.Include(x => x.Approvals)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
|
||||
var project = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == e.ProjectId, ct);
|
||||
var department = e.DepartmentId is null ? null : await db.Departments.AsNoTracking()
|
||||
.FirstOrDefaultAsync(d => d.Id == e.DepartmentId, ct);
|
||||
|
||||
var userIds = new HashSet<Guid>();
|
||||
if (e.DrafterUserId is Guid did) userIds.Add(did);
|
||||
foreach (var a in e.Approvals) if (a.ApproverUserId is Guid aid) userIds.Add(aid);
|
||||
var users = await userManager.Users.AsNoTracking()
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.FullName, ct);
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
|
||||
return new BudgetDetailBundleDto(
|
||||
e.Id, e.MaNganSach, e.TenNganSach, e.Description, e.NamNganSach, e.Phase,
|
||||
e.ProjectId, project?.Name ?? "",
|
||||
e.DepartmentId, department?.Name,
|
||||
e.DrafterUserId, e.DrafterUserId is Guid d && users.TryGetValue(d, out var dn) ? dn : null,
|
||||
e.TongNganSach, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
|
||||
e.Details.OrderBy(d => d.Order).Select(d => new BudgetDetailDto(
|
||||
d.Id, d.GroupCode, d.GroupName, d.ItemCode, d.NoiDung, d.DonViTinh,
|
||||
d.KhoiLuong, d.DonGia, d.ThanhTien, d.Order, d.GhiChu)).ToList(),
|
||||
e.Approvals.OrderBy(a => a.ApprovedAt).Select(a => new BudgetApprovalDto(
|
||||
a.Id, a.FromPhase, a.ToPhase, a.ApproverUserId,
|
||||
a.ApproverUserId is Guid uid && users.TryGetValue(uid, out var an) ? an : null,
|
||||
a.Decision, a.Comment, a.ApprovedAt)).ToList(),
|
||||
new BudgetWorkflowSummaryDto(
|
||||
policy.Name, policy.Description,
|
||||
policy.ActivePhases.ToList(),
|
||||
policy.NextPhasesFrom(e.Phase).ToList()));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DELETE ==========
|
||||
|
||||
public record DeleteBudgetCommand(Guid Id) : IRequest;
|
||||
|
||||
public class DeleteBudgetCommandHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<DeleteBudgetCommand>
|
||||
{
|
||||
public async Task Handle(DeleteBudgetCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
if (entity.Phase != BudgetPhase.DangSoanThao && entity.Phase != BudgetPhase.TuChoi)
|
||||
throw new ConflictException("Chỉ xóa được ngân sách phase Soạn thảo / Từ chối.");
|
||||
db.Budgets.Remove(entity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Detail CRUD ==========
|
||||
|
||||
public record AddBudgetDetailCommand(
|
||||
Guid BudgetId, string GroupCode, string GroupName, string? ItemCode, string NoiDung,
|
||||
string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu) : IRequest<Guid>;
|
||||
|
||||
public class AddBudgetDetailCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<AddBudgetDetailCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(AddBudgetDetailCommand request, CancellationToken ct)
|
||||
{
|
||||
var bg = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.BudgetId, ct)
|
||||
?? throw new NotFoundException("Budget", request.BudgetId);
|
||||
var maxOrder = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id)
|
||||
.Select(d => (int?)d.Order).MaxAsync(ct);
|
||||
var entity = new BudgetDetail
|
||||
{
|
||||
BudgetId = bg.Id,
|
||||
GroupCode = request.GroupCode, GroupName = request.GroupName,
|
||||
ItemCode = request.ItemCode, NoiDung = request.NoiDung, DonViTinh = request.DonViTinh,
|
||||
KhoiLuong = request.KhoiLuong, DonGia = request.DonGia, ThanhTien = request.ThanhTien,
|
||||
GhiChu = request.GhiChu, Order = (maxOrder ?? 0) + 1,
|
||||
};
|
||||
db.BudgetDetails.Add(entity);
|
||||
|
||||
// Recompute TongNganSach
|
||||
bg.TongNganSach = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id)
|
||||
.SumAsync(d => d.ThanhTien, ct) + entity.ThanhTien;
|
||||
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = bg.Id, EntityType = BudgetEntityType.Detail, EntityId = entity.Id,
|
||||
Action = ChangelogAction.Insert, PhaseAtChange = bg.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = $"Thêm hạng mục {request.GroupCode} — {request.NoiDung}",
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateBudgetDetailCommand(
|
||||
Guid BudgetId, Guid DetailId, string GroupCode, string GroupName, string? ItemCode, string NoiDung,
|
||||
string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu) : IRequest;
|
||||
|
||||
public class UpdateBudgetDetailCommandHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<UpdateBudgetDetailCommand>
|
||||
{
|
||||
public async Task Handle(UpdateBudgetDetailCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.BudgetDetails
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DetailId && d.BudgetId == request.BudgetId, ct)
|
||||
?? throw new NotFoundException("BudgetDetail", request.DetailId);
|
||||
entity.GroupCode = request.GroupCode; entity.GroupName = request.GroupName;
|
||||
entity.ItemCode = request.ItemCode; entity.NoiDung = request.NoiDung; entity.DonViTinh = request.DonViTinh;
|
||||
entity.KhoiLuong = request.KhoiLuong; entity.DonGia = request.DonGia; entity.ThanhTien = request.ThanhTien;
|
||||
entity.GhiChu = request.GhiChu;
|
||||
|
||||
var bg = await db.Budgets.FirstOrDefaultAsync(b => b.Id == request.BudgetId, ct);
|
||||
if (bg != null)
|
||||
bg.TongNganSach = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id)
|
||||
.SumAsync(d => d.ThanhTien, ct);
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record DeleteBudgetDetailCommand(Guid BudgetId, Guid DetailId) : IRequest;
|
||||
|
||||
public class DeleteBudgetDetailCommandHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<DeleteBudgetDetailCommand>
|
||||
{
|
||||
public async Task Handle(DeleteBudgetDetailCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.BudgetDetails
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DetailId && d.BudgetId == request.BudgetId, ct)
|
||||
?? throw new NotFoundException("BudgetDetail", request.DetailId);
|
||||
db.BudgetDetails.Remove(entity);
|
||||
|
||||
var bg = await db.Budgets.FirstOrDefaultAsync(b => b.Id == request.BudgetId, ct);
|
||||
if (bg != null)
|
||||
bg.TongNganSach = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id && d.Id != entity.Id)
|
||||
.SumAsync(d => d.ThanhTien, ct);
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CHANGELOG list ==========
|
||||
|
||||
public record ListBudgetChangelogsQuery(Guid BudgetId, int Take = 200) : IRequest<List<BudgetChangelogDto>>;
|
||||
|
||||
public class ListBudgetChangelogsQueryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<ListBudgetChangelogsQuery, List<BudgetChangelogDto>>
|
||||
{
|
||||
public async Task<List<BudgetChangelogDto>> Handle(ListBudgetChangelogsQuery request, CancellationToken ct) =>
|
||||
await db.BudgetChangelogs.AsNoTracking()
|
||||
.Where(c => c.BudgetId == request.BudgetId)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Take(request.Take)
|
||||
.Select(c => new BudgetChangelogDto(
|
||||
c.Id, c.EntityType, c.EntityId, c.Action, c.PhaseAtChange,
|
||||
c.UserId, c.UserName, c.Summary, c.FieldChangesJson, c.ContextNote, c.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
|
||||
namespace SolutionErp.Application.Budgets.Dtos;
|
||||
|
||||
public record BudgetListItemDto(
|
||||
Guid Id,
|
||||
string? MaNganSach,
|
||||
string TenNganSach,
|
||||
int NamNganSach,
|
||||
BudgetPhase Phase,
|
||||
Guid ProjectId,
|
||||
string ProjectName,
|
||||
decimal TongNganSach,
|
||||
DateTime? SlaDeadline,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record BudgetDetailDto(
|
||||
Guid Id,
|
||||
string GroupCode,
|
||||
string GroupName,
|
||||
string? ItemCode,
|
||||
string NoiDung,
|
||||
string? DonViTinh,
|
||||
decimal KhoiLuong,
|
||||
decimal DonGia,
|
||||
decimal ThanhTien,
|
||||
int Order,
|
||||
string? GhiChu);
|
||||
|
||||
public record BudgetApprovalDto(
|
||||
Guid Id,
|
||||
BudgetPhase FromPhase,
|
||||
BudgetPhase ToPhase,
|
||||
Guid? ApproverUserId,
|
||||
string? ApproverName,
|
||||
ApprovalDecision Decision,
|
||||
string? Comment,
|
||||
DateTime ApprovedAt);
|
||||
|
||||
public record BudgetChangelogDto(
|
||||
Guid Id,
|
||||
BudgetEntityType EntityType,
|
||||
Guid? EntityId,
|
||||
ChangelogAction Action,
|
||||
BudgetPhase? PhaseAtChange,
|
||||
Guid? UserId,
|
||||
string? UserName,
|
||||
string? Summary,
|
||||
string? FieldChangesJson,
|
||||
string? ContextNote,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record BudgetWorkflowSummaryDto(
|
||||
string PolicyName,
|
||||
string PolicyDescription,
|
||||
List<BudgetPhase> ActivePhases,
|
||||
List<BudgetPhase> NextPhases);
|
||||
|
||||
public record BudgetDetailBundleDto(
|
||||
Guid Id,
|
||||
string? MaNganSach,
|
||||
string TenNganSach,
|
||||
string? Description,
|
||||
int NamNganSach,
|
||||
BudgetPhase Phase,
|
||||
Guid ProjectId,
|
||||
string ProjectName,
|
||||
Guid? DepartmentId,
|
||||
string? DepartmentName,
|
||||
Guid? DrafterUserId,
|
||||
string? DrafterName,
|
||||
decimal TongNganSach,
|
||||
DateTime? SlaDeadline,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt,
|
||||
List<BudgetDetailDto> Details,
|
||||
List<BudgetApprovalDto> Approvals,
|
||||
BudgetWorkflowSummaryDto Workflow);
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Contracts.Details;
|
||||
using SolutionErp.Domain.Forms;
|
||||
@ -58,5 +59,11 @@ public interface IApplicationDbContext
|
||||
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
||||
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { get; }
|
||||
|
||||
// Module Ngân sách (Phase 7)
|
||||
DbSet<Budget> Budgets { get; }
|
||||
DbSet<BudgetDetail> BudgetDetails { get; }
|
||||
DbSet<BudgetApproval> BudgetApprovals { get; }
|
||||
DbSet<BudgetChangelog> BudgetChangelogs { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user