From a05c57b0813e317a905c3b76653d30fead9aaebf Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 28 Apr 2026 11:37:45 +0700 Subject: [PATCH] [CLAUDE] Domain+App+Api: Module Ngan sach (Budget) - 4 bang + workflow simple MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Controllers/BudgetsController.cs | 94 + .../Budgets/BudgetFeatures.cs | 388 ++ .../Budgets/Dtos/BudgetDtos.cs | 79 + .../Interfaces/IApplicationDbContext.cs | 7 + .../SolutionErp.Domain/Budgets/Budget.cs | 29 + .../Budgets/BudgetApproval.cs | 17 + .../Budgets/BudgetChangelog.cs | 28 + .../Budgets/BudgetDetail.cs | 22 + .../SolutionErp.Domain/Budgets/BudgetPhase.cs | 14 + .../Budgets/BudgetPolicy.cs | 63 + .../SolutionErp.Domain/Contracts/Contract.cs | 1 + .../SolutionErp.Domain/Identity/MenuKeys.cs | 10 + .../PurchaseEvaluations/PurchaseEvaluation.cs | 1 + .../Persistence/ApplicationDbContext.cs | 7 + .../Configurations/BudgetConfiguration.cs | 89 + .../Configurations/ContractConfiguration.cs | 1 + .../PurchaseEvaluationConfiguration.cs | 1 + .../Persistence/DbInitializer.cs | 5 + .../20260428043508_AddBudgets.Designer.cs | 3229 +++++++++++++++++ .../Migrations/20260428043508_AddBudgets.cs | 236 ++ .../ApplicationDbContextModelSnapshot.cs | 311 ++ 21 files changed, 4632 insertions(+) create mode 100644 src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs create mode 100644 src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs create mode 100644 src/Backend/SolutionErp.Application/Budgets/Dtos/BudgetDtos.cs create mode 100644 src/Backend/SolutionErp.Domain/Budgets/Budget.cs create mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs create mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs create mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs create mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs create mode 100644 src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/BudgetConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.cs diff --git a/src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs b/src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs new file mode 100644 index 0000000..5f5508d --- /dev/null +++ b/src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs @@ -0,0 +1,94 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SolutionErp.Application.Budgets; +using SolutionErp.Application.Budgets.Dtos; +using SolutionErp.Application.Common.Models; +using SolutionErp.Domain.Budgets; +using SolutionErp.Domain.Contracts; + +namespace SolutionErp.Api.Controllers; + +[ApiController] +[Route("api/budgets")] +[Authorize] +public class BudgetsController(IMediator mediator) : ControllerBase +{ + [HttpGet] + public async Task>> List( + [FromQuery] int page = 1, [FromQuery] int pageSize = 20, + [FromQuery] string? search = null, [FromQuery] bool sortDesc = true, + [FromQuery] BudgetPhase? phase = null, + [FromQuery] Guid? projectId = null, + [FromQuery] int? namNganSach = null, + CancellationToken ct = default) + => Ok(await mediator.Send(new ListBudgetsQuery(phase, projectId, namNganSach) + { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct)); + + [HttpGet("{id:guid}")] + public async Task> Get(Guid id, CancellationToken ct) + => Ok(await mediator.Send(new GetBudgetQuery(id), ct)); + + [HttpPost] + public async Task> Create([FromBody] CreateBudgetCommand cmd, CancellationToken ct) + { + var id = await mediator.Send(cmd, ct); + return CreatedAtAction(nameof(Get), new { id }, new { id }); + } + + [HttpPut("{id:guid}")] + public async Task Update(Guid id, [FromBody] UpdateBudgetDraftCommand cmd, CancellationToken ct) + { + if (id != cmd.Id) return BadRequest(new { detail = "ID không khớp" }); + await mediator.Send(cmd, ct); + return NoContent(); + } + + [HttpPost("{id:guid}/transitions")] + public async Task Transition(Guid id, [FromBody] TransitionBudgetBody body, CancellationToken ct) + { + await mediator.Send(new TransitionBudgetCommand(id, body.TargetPhase, body.Decision, body.Comment), ct); + return NoContent(); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id, CancellationToken ct) + { + await mediator.Send(new DeleteBudgetCommand(id), ct); + return NoContent(); + } + + [HttpPost("{id:guid}/details")] + public async Task> AddDetail(Guid id, [FromBody] BudgetDetailBody body, CancellationToken ct) + { + var newId = await mediator.Send(new AddBudgetDetailCommand( + id, body.GroupCode, body.GroupName, body.ItemCode, body.NoiDung, body.DonViTinh, + body.KhoiLuong, body.DonGia, body.ThanhTien, body.GhiChu), ct); + return Ok(new { id = newId }); + } + + [HttpPut("{id:guid}/details/{detailId:guid}")] + public async Task UpdateDetail(Guid id, Guid detailId, [FromBody] BudgetDetailBody body, CancellationToken ct) + { + await mediator.Send(new UpdateBudgetDetailCommand( + id, detailId, body.GroupCode, body.GroupName, body.ItemCode, body.NoiDung, body.DonViTinh, + body.KhoiLuong, body.DonGia, body.ThanhTien, body.GhiChu), ct); + return NoContent(); + } + + [HttpDelete("{id:guid}/details/{detailId:guid}")] + public async Task DeleteDetail(Guid id, Guid detailId, CancellationToken ct) + { + await mediator.Send(new DeleteBudgetDetailCommand(id, detailId), ct); + return NoContent(); + } + + [HttpGet("{id:guid}/changelogs")] + public async Task> GetChangelogs(Guid id, CancellationToken ct) + => await mediator.Send(new ListBudgetChangelogsQuery(id), ct); +} + +public record TransitionBudgetBody(BudgetPhase TargetPhase, ApprovalDecision Decision, string? Comment); +public record BudgetDetailBody( + string GroupCode, string GroupName, string? ItemCode, string NoiDung, + string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu); diff --git a/src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs b/src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs new file mode 100644 index 0000000..f8b11df --- /dev/null +++ b/src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs @@ -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; + +public class CreateBudgetCommandValidator : AbstractValidator +{ + 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 +{ + public async Task 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 +{ + 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 userManager) : IRequestHandler +{ + 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>; + +public class ListBudgetsQueryHandler( + IApplicationDbContext db) : IRequestHandler> +{ + public async Task> 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(items, total, request.Page, request.PageSize); + } +} + +// ========== GET detail bundle ========== + +public record GetBudgetQuery(Guid Id) : IRequest; + +public class GetBudgetQueryHandler( + IApplicationDbContext db, + UserManager userManager) : IRequestHandler +{ + public async Task 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(); + 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 +{ + 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; + +public class AddBudgetDetailCommandHandler( + IApplicationDbContext db, + ICurrentUser currentUser) : IRequestHandler +{ + public async Task 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 +{ + 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 +{ + 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>; + +public class ListBudgetChangelogsQueryHandler(IApplicationDbContext db) + : IRequestHandler> +{ + public async Task> 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); +} diff --git a/src/Backend/SolutionErp.Application/Budgets/Dtos/BudgetDtos.cs b/src/Backend/SolutionErp.Application/Budgets/Dtos/BudgetDtos.cs new file mode 100644 index 0000000..5fb984f --- /dev/null +++ b/src/Backend/SolutionErp.Application/Budgets/Dtos/BudgetDtos.cs @@ -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 ActivePhases, + List 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 Details, + List Approvals, + BudgetWorkflowSummaryDto Workflow); diff --git a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs index 22d08b3..2dd1a79 100644 --- a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs @@ -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 PurchaseEvaluationWorkflowStepApprovers { get; } DbSet PurchaseEvaluationCodeSequences { get; } + // Module Ngân sách (Phase 7) + DbSet Budgets { get; } + DbSet BudgetDetails { get; } + DbSet BudgetApprovals { get; } + DbSet BudgetChangelogs { get; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/src/Backend/SolutionErp.Domain/Budgets/Budget.cs b/src/Backend/SolutionErp.Domain/Budgets/Budget.cs new file mode 100644 index 0000000..c07207e --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Budgets/Budget.cs @@ -0,0 +1,29 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Budgets; + +// Aggregate root quản lý ngân sách. Gắn với Project (required), reference +// từ PurchaseEvaluation + Contract (cả 2 nullable FK Budget.Id). +// +// Workflow đơn giản 3-step: Drafter → CCM → CEO. Pattern hardcoded trong +// BudgetPolicy (chưa versioned, tương lai có thể thêm BudgetWorkflowDefinition +// nếu cần admin config). +public class Budget : AuditableEntity +{ + public string? MaNganSach { get; set; } // Auto-gen NS-YYYYMM-XXXX + public string TenNganSach { get; set; } = string.Empty; + public string? Description { get; set; } + public int NamNganSach { get; set; } // Năm áp dụng (vd 2026) + public Guid ProjectId { get; set; } // FK Projects (required) + public Guid? DepartmentId { get; set; } + public Guid? DrafterUserId { get; set; } + + public BudgetPhase Phase { get; set; } = BudgetPhase.DangSoanThao; + public decimal TongNganSach { get; set; } // Tổng = sum BudgetDetails.ThanhTien (computed) + public DateTime? SlaDeadline { get; set; } + public bool SlaWarningSent { get; set; } + + public List Details { get; set; } = new(); + public List Approvals { get; set; } = new(); + public List Changelogs { get; set; } = new(); +} diff --git a/src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs b/src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs new file mode 100644 index 0000000..2a7a9cb --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs @@ -0,0 +1,17 @@ +using SolutionErp.Domain.Common; +using SolutionErp.Domain.Contracts; // reuse ApprovalDecision + +namespace SolutionErp.Domain.Budgets; + +public class BudgetApproval : BaseEntity +{ + public Guid BudgetId { get; set; } + public BudgetPhase FromPhase { get; set; } + public BudgetPhase ToPhase { get; set; } + public Guid? ApproverUserId { get; set; } // null = system SLA auto + public ApprovalDecision Decision { get; set; } + public string? Comment { get; set; } + public DateTime ApprovedAt { get; set; } + + public Budget? Budget { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs b/src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs new file mode 100644 index 0000000..0f77d23 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs @@ -0,0 +1,28 @@ +using SolutionErp.Domain.Common; +using SolutionErp.Domain.Contracts; // reuse ChangelogAction + +namespace SolutionErp.Domain.Budgets; + +// Audit log unified cho mọi thay đổi trên ngân sách. +public class BudgetChangelog : BaseEntity +{ + public Guid BudgetId { get; set; } + public Budget? Budget { get; set; } + + public BudgetEntityType EntityType { get; set; } + public Guid? EntityId { get; set; } + public ChangelogAction Action { get; set; } + public BudgetPhase? PhaseAtChange { get; set; } + public Guid? UserId { get; set; } + public string? UserName { get; set; } + public string? Summary { get; set; } + public string? FieldChangesJson { get; set; } + public string? ContextNote { get; set; } +} + +public enum BudgetEntityType +{ + Header = 1, + Detail = 2, + Workflow = 3, +} diff --git a/src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs b/src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs new file mode 100644 index 0000000..3c25125 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs @@ -0,0 +1,22 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Budgets; + +// Chi tiết ngân sách — pattern flat row giống PurchaseEvaluationDetail. +// Group A.I/A.II/... cho hạng mục cha, NoiDung cho item con. +public class BudgetDetail : BaseEntity +{ + public Guid BudgetId { get; set; } + public string GroupCode { get; set; } = string.Empty; // "A.I", "A.II", ... + public string GroupName { get; set; } = string.Empty; // "Bê tông", "Phụ gia", ... + public string? ItemCode { get; set; } + public string NoiDung { get; set; } = string.Empty; + public string? DonViTinh { get; set; } + public decimal KhoiLuong { get; set; } + public decimal DonGia { get; set; } + public decimal ThanhTien { get; set; } // = KhoiLuong × DonGia (hoặc nhập tay) + public int Order { get; set; } + public string? GhiChu { get; set; } + + public Budget? Budget { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs b/src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs new file mode 100644 index 0000000..d3d727e --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs @@ -0,0 +1,14 @@ +namespace SolutionErp.Domain.Budgets; + +// State machine ngân sách — đơn giản 3 bước duyệt + 2 terminal. +// DangSoanThao → ChoCCM → ChoCEO → DaDuyet +// Bất kỳ phase duyệt → DangSoanThao (reject) +// DangSoanThao → TuChoi +public enum BudgetPhase +{ + DangSoanThao = 1, + ChoCCM = 2, + ChoCEO = 3, + DaDuyet = 4, + TuChoi = 99, +} diff --git a/src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs b/src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs new file mode 100644 index 0000000..4b947c3 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs @@ -0,0 +1,63 @@ +using SolutionErp.Domain.Identity; + +namespace SolutionErp.Domain.Budgets; + +// Policy hardcoded đơn giản — chưa versioned (theo user "tạm thời simple +// default"). Tương lai nếu admin cần config qua UI: thêm BudgetWorkflow +// Definition tables tương tự PE workflow. +public sealed record BudgetPolicy( + string Name, + string Description, + IReadOnlyDictionary<(BudgetPhase From, BudgetPhase To), string[]> Transitions, + IReadOnlyDictionary PhaseSla, + IReadOnlyList ActivePhases) +{ + public bool HasPhase(BudgetPhase phase) => ActivePhases.Contains(phase); + + public bool IsTransitionAllowed( + BudgetPhase from, BudgetPhase to, + IReadOnlyList actorRoles) + { + if (!Transitions.TryGetValue((from, to), out var roles)) return false; + return actorRoles.Any(r => roles.Contains(r)); + } + + public IReadOnlyList NextPhasesFrom(BudgetPhase from) => + Transitions.Keys.Where(k => k.From == from).Select(k => k.To).Distinct().ToList(); +} + +public static class BudgetPolicies +{ + private static readonly Dictionary DefaultSla = new() + { + [BudgetPhase.DangSoanThao] = TimeSpan.FromDays(5), + [BudgetPhase.ChoCCM] = TimeSpan.FromDays(3), + [BudgetPhase.ChoCEO] = TimeSpan.FromDays(2), + [BudgetPhase.DaDuyet] = null, + [BudgetPhase.TuChoi] = null, + }; + + public static readonly BudgetPolicy Default = new( + Name: "Default", + Description: "Quy trình ngân sách 3-step (Drafter → CCM → CEO).", + Transitions: new Dictionary<(BudgetPhase, BudgetPhase), string[]> + { + [(BudgetPhase.DangSoanThao, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager], + [(BudgetPhase.DangSoanThao, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager], + + [(BudgetPhase.ChoCCM, BudgetPhase.ChoCEO)] = [AppRoles.CostControl], + [(BudgetPhase.ChoCCM, BudgetPhase.DangSoanThao)] = [AppRoles.CostControl], + + [(BudgetPhase.ChoCEO, BudgetPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner], + [(BudgetPhase.ChoCEO, BudgetPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner], + }, + PhaseSla: DefaultSla, + ActivePhases: + [ + BudgetPhase.DangSoanThao, + BudgetPhase.ChoCCM, + BudgetPhase.ChoCEO, + BudgetPhase.DaDuyet, + BudgetPhase.TuChoi, + ]); +} diff --git a/src/Backend/SolutionErp.Domain/Contracts/Contract.cs b/src/Backend/SolutionErp.Domain/Contracts/Contract.cs index af2fecb..2f805ab 100644 --- a/src/Backend/SolutionErp.Domain/Contracts/Contract.cs +++ b/src/Backend/SolutionErp.Domain/Contracts/Contract.cs @@ -23,6 +23,7 @@ public class Contract : AuditableEntity public DateTime? SlaDeadline { get; set; } // Hết hạn phase hiện tại public string? DraftData { get; set; } // JSON field values (render template) public bool SlaWarningSent { get; set; } // Flag để không gửi warning 2 lần + public Guid? BudgetId { get; set; } // Reference Budget (Phase 7) — đối chiếu chi phí HĐ vs ngân sách public List Approvals { get; set; } = new(); public List Comments { get; set; } = new(); diff --git a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs index 91c23fc..9247cee 100644 --- a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs +++ b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs @@ -51,6 +51,15 @@ public static class MenuKeys public const string PurchaseEvaluations = "PurchaseEvaluations"; // root group public const string PeWorkflows = "PeWorkflows"; // workflow admin root + // ============================================================ + // Module Ngân sách (Phase 7) — 4 bảng quản lý ngân sách dự án/gói thầu. + // 1 root + 3 leaf action (Danh sách / Thao tác / Duyệt). + // ============================================================ + public const string Budgets = "Budgets"; + public const string BudgetList = "Bg_List"; + public const string BudgetCreate = "Bg_Create"; + public const string BudgetPending = "Bg_Pending"; + public static readonly string[] PurchaseEvaluationTypeCodes = ["DuyetNcc", "DuyetNccPhuongAn"]; @@ -70,6 +79,7 @@ public static class MenuKeys Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems, Contracts, Forms, Reports, PurchaseEvaluations, + Budgets, BudgetList, BudgetCreate, BudgetPending, System, Users, Roles, Permissions, Workflows, PeWorkflows, ]; diff --git a/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs b/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs index 4c37055..5f12979 100644 --- a/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs +++ b/src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs @@ -26,6 +26,7 @@ public class PurchaseEvaluation : AuditableEntity public string? PaymentTerms { get; set; } // JSON {tamUng, thanhToanTam, quyetToan, baoHanh, hanMucCongNo, danhGia} public Guid? ContractId { get; set; } // FK Contracts — set khi user gen HĐ từ phiếu + public Guid? BudgetId { get; set; } // FK Budget (Phase 7) — đối chiếu báo giá vs ngân sách gói thầu public List Suppliers { get; set; } = new(); public List Details { get; set; } = new(); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs index 35be23a..bff4df7 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Domain.Budgets; using SolutionErp.Domain.Contracts; using SolutionErp.Domain.Contracts.Details; using SolutionErp.Domain.Forms; @@ -59,6 +60,12 @@ public class ApplicationDbContext public DbSet PurchaseEvaluationWorkflowStepApprovers => Set(); public DbSet PurchaseEvaluationCodeSequences => Set(); + // Module Ngân sách (Phase 7) — 4 bảng: Budget header + Details + Approvals + Changelogs. + public DbSet Budgets => Set(); + public DbSet BudgetDetails => Set(); + public DbSet BudgetApprovals => Set(); + public DbSet BudgetChangelogs => Set(); + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/BudgetConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/BudgetConfiguration.cs new file mode 100644 index 0000000..783a2ca --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/BudgetConfiguration.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Budgets; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +public class BudgetConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("Budgets"); + b.HasKey(x => x.Id); + + b.Property(x => x.MaNganSach).HasMaxLength(100); + b.Property(x => x.TenNganSach).HasMaxLength(500).IsRequired(); + b.Property(x => x.Description).HasMaxLength(2000); + b.Property(x => x.Phase).HasConversion(); + b.Property(x => x.TongNganSach).HasPrecision(18, 2); + + b.HasIndex(x => x.MaNganSach).IsUnique().HasFilter("[MaNganSach] IS NOT NULL"); + b.HasIndex(x => new { x.Phase, x.IsDeleted }); + b.HasIndex(x => x.ProjectId); + b.HasIndex(x => x.NamNganSach); + b.HasIndex(x => x.SlaDeadline); + + b.HasMany(x => x.Details).WithOne(d => d.Budget).HasForeignKey(d => d.BudgetId).OnDelete(DeleteBehavior.Cascade); + b.HasMany(x => x.Approvals).WithOne(a => a.Budget).HasForeignKey(a => a.BudgetId).OnDelete(DeleteBehavior.Cascade); + b.HasMany(x => x.Changelogs).WithOne(c => c.Budget).HasForeignKey(c => c.BudgetId).OnDelete(DeleteBehavior.Cascade); + + b.HasQueryFilter(x => !x.IsDeleted); + } +} + +public class BudgetDetailConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("BudgetDetails"); + b.HasKey(x => x.Id); + + b.Property(x => x.GroupCode).HasMaxLength(50).IsRequired(); + b.Property(x => x.GroupName).HasMaxLength(200).IsRequired(); + b.Property(x => x.ItemCode).HasMaxLength(100); + b.Property(x => x.NoiDung).HasMaxLength(500).IsRequired(); + b.Property(x => x.DonViTinh).HasMaxLength(50); + b.Property(x => x.GhiChu).HasMaxLength(1000); + b.Property(x => x.KhoiLuong).HasPrecision(18, 4); + b.Property(x => x.DonGia).HasPrecision(18, 2); + b.Property(x => x.ThanhTien).HasPrecision(18, 2); + + b.HasIndex(x => new { x.BudgetId, x.Order }); + } +} + +public class BudgetApprovalConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("BudgetApprovals"); + b.HasKey(x => x.Id); + + b.Property(x => x.FromPhase).HasConversion(); + b.Property(x => x.ToPhase).HasConversion(); + b.Property(x => x.Decision).HasConversion(); + b.Property(x => x.Comment).HasMaxLength(1000); + + b.HasIndex(x => new { x.BudgetId, x.ApprovedAt }); + } +} + +public class BudgetChangelogConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("BudgetChangelogs"); + b.HasKey(x => x.Id); + + b.Property(x => x.EntityType).HasConversion(); + b.Property(x => x.Action).HasConversion(); + b.Property(x => x.PhaseAtChange).HasConversion(); + b.Property(x => x.UserName).HasMaxLength(200); + b.Property(x => x.Summary).HasMaxLength(500); + b.Property(x => x.ContextNote).HasMaxLength(2000); + b.Property(x => x.FieldChangesJson).HasColumnType("nvarchar(max)"); + + b.HasIndex(x => new { x.BudgetId, x.CreatedAt }); + b.HasIndex(x => new { x.BudgetId, x.EntityType }); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractConfiguration.cs index 1c9f558..fa41462 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractConfiguration.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ContractConfiguration.cs @@ -24,6 +24,7 @@ public class ContractConfiguration : IEntityTypeConfiguration b.HasIndex(x => x.SupplierId); b.HasIndex(x => x.ProjectId); b.HasIndex(x => x.SlaDeadline); + b.HasIndex(x => x.BudgetId); b.HasMany(x => x.Approvals).WithOne(a => a.Contract).HasForeignKey(a => a.ContractId).OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.Comments).WithOne(c => c.Contract).HasForeignKey(c => c.ContractId).OnDelete(DeleteBehavior.Cascade); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs index 92154df..63cb303 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs @@ -25,6 +25,7 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration x.SlaDeadline); b.HasIndex(x => x.WorkflowDefinitionId); b.HasIndex(x => x.ContractId); + b.HasIndex(x => x.BudgetId); b.HasMany(x => x.Suppliers).WithOne(s => s.PurchaseEvaluation).HasForeignKey(s => s.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.Details).WithOne(d => d.PurchaseEvaluation).HasForeignKey(d => d.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 8098d48..6914834 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -1308,6 +1308,11 @@ public static class DbInitializer // Module Duyệt NCC (tiền-HĐ) (MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"), (MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, "GitCompareArrows"), + // Module Ngân sách (Phase 7) + (MenuKeys.Budgets, "Ngân sách", null, 27, "Wallet"), + (MenuKeys.BudgetList, "Danh sách", MenuKeys.Budgets, 1, "List"), + (MenuKeys.BudgetCreate, "Thao tác", MenuKeys.Budgets, 2, "Plus"), + (MenuKeys.BudgetPending, "Duyệt", MenuKeys.Budgets, 3, "CheckCircle2"), }; // Per-type sub-menu under Contracts: 1 group + 3 leaves each diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.Designer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.Designer.cs new file mode 100644 index 0000000..f8d6c41 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.Designer.cs @@ -0,0 +1,3229 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SolutionErp.Infrastructure.Persistence; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260428043508_AddBudgets")] + partial class AddBudgets + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaNganSach") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NamNganSach") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenNganSach") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TongNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaNganSach") + .IsUnique() + .HasFilter("[MaNganSach] IS NOT NULL"); + + b.HasIndex("NamNganSach"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "ApprovedAt"); + + b.ToTable("BudgetApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "CreatedAt"); + + b.HasIndex("BudgetId", "EntityType"); + + b.ToTable("BudgetChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "Order"); + + b.ToTable("BudgetDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("BypassProcurementAndCCM") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DraftData") + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("GiaTri") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaHopDong") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NoiDung") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenHopDong") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.HasIndex("MaHopDong") + .IsUnique() + .HasFilter("[MaHopDong] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("SupplierId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "ApprovedAt"); + + b.ToTable("ContractApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId"); + + b.ToTable("ContractAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.HasIndex("ContractId", "EntityType"); + + b.ToTable("ContractChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ContractCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.ToTable("ContractComments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DenNgay") + .HasColumnType("datetime2"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaDichVu") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGian") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TuNgay") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("DichVuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MaCongViec") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenCongViec") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("YeuCauKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("GiaoKhoanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThueVAT") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("MuaBanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LoaiDichVu") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PhamViDichVu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("SLA") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacDvDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DieuKienGiaoHang") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DieuKienThanhToan") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NhomSP") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacNccDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianGiao") + .HasColumnType("datetime2"); + + b.Property("ThongSoKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NhaCungCapDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("HangMuc") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("ThauPhuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("ContractType", "IsActive"); + + b.ToTable("WorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkflowDefinitionId", "Order"); + + b.ToTable("WorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkflowStepId"); + + b.ToTable("WorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType") + .IsUnique(); + + b.ToTable("WorkflowTypeAssignments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractClause", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ContractClauses", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FieldSpec") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FormCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Format") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType"); + + b.HasIndex("FormCode") + .IsUnique(); + + b.ToTable("ContractTemplates", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Property("Key") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ParentKey") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Key"); + + b.HasIndex("ParentKey"); + + b.ToTable("MenuItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CanCreate") + .HasColumnType("bit"); + + b.Property("CanDelete") + .HasColumnType("bit"); + + b.Property("CanRead") + .HasColumnType("bit"); + + b.Property("CanUpdate") + .HasColumnType("bit"); + + b.Property("MenuKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MenuKey"); + + b.HasIndex("RoleId", "MenuKey") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ShortName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Position") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RefreshToken") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.MaterialItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginCountry") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Specification") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("MaterialItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.ServiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("ServiceItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.UnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("UnitsOfMeasure", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("WorkItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Departments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ContactPerson") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("Suppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Href") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("RefId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId", "ReadAt"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DiaDiem") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaPhieu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SelectedSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenGoiThau") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.HasIndex("ContractId"); + + b.HasIndex("MaPhieu") + .IsUnique() + .HasFilter("[MaPhieu] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("WorkflowDefinitionId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("PurchaseEvaluations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "ApprovedAt"); + + b.ToTable("PurchaseEvaluationApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.ToTable("PurchaseEvaluationAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "CreatedAt"); + + b.HasIndex("PurchaseEvaluationId", "EntityType"); + + b.ToTable("PurchaseEvaluationChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("PurchaseEvaluationCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuongNganSach") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("KhoiLuongThiCong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTienNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Order"); + + b.ToTable("PurchaseEvaluationDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BgVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChuaVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsSelected") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationDetailId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.HasIndex("PurchaseEvaluationDetailId", "PurchaseEvaluationSupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationQuotes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentTermText") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.HasIndex("PurchaseEvaluationId", "SupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationSuppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EvaluationType") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("EvaluationType", "IsActive"); + + b.ToTable("PurchaseEvaluationWorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationWorkflowDefinitionId", "Order"); + + b.ToTable("PurchaseEvaluationWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationWorkflowStepId"); + + b.ToTable("PurchaseEvaluationWorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Approvals") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Changelogs") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Details") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Approvals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Attachments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Changelogs") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Comments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DichVuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("GiaoKhoanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("MuaBanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacDvDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacNccDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NhaCungCapDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("ThauPhuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Contracts.WorkflowDefinition", "WorkflowDefinition") + .WithMany("Steps") + .HasForeignKey("WorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkflowDefinition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.Contracts.WorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("WorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent") + .WithMany("Children") + .HasForeignKey("ParentKey") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Menu") + .WithMany("Permissions") + .HasForeignKey("MenuKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Menu"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Approvals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Attachments") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Changelogs") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Details") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail") + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", null) + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationId"); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", "Supplier") + .WithMany() + .HasForeignKey("PurchaseEvaluationSupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Detail"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Suppliers") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", "Definition") + .WithMany("Steps") + .HasForeignKey("PurchaseEvaluationWorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("PurchaseEvaluationWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b => + { + b.Navigation("Approvals"); + + b.Navigation("Changelogs"); + + b.Navigation("Details"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("Comments"); + + b.Navigation("DichVuDetails"); + + b.Navigation("GiaoKhoanDetails"); + + b.Navigation("MuaBanDetails"); + + b.Navigation("NguyenTacDvDetails"); + + b.Navigation("NguyenTacNccDetails"); + + b.Navigation("NhaCungCapDetails"); + + b.Navigation("ThauPhuDetails"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Navigation("Approvers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Navigation("Children"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("Details"); + + b.Navigation("Quotes"); + + b.Navigation("Suppliers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Navigation("Approvers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.cs new file mode 100644 index 0000000..296b448 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.cs @@ -0,0 +1,236 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddBudgets : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BudgetId", + table: "PurchaseEvaluations", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.AddColumn( + name: "BudgetId", + table: "Contracts", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.CreateTable( + name: "Budgets", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MaNganSach = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + TenNganSach = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + Description = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + NamNganSach = table.Column(type: "int", nullable: false), + ProjectId = table.Column(type: "uniqueidentifier", nullable: false), + DepartmentId = table.Column(type: "uniqueidentifier", nullable: true), + DrafterUserId = table.Column(type: "uniqueidentifier", nullable: true), + Phase = table.Column(type: "int", nullable: false), + TongNganSach = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + SlaDeadline = table.Column(type: "datetime2", nullable: true), + SlaWarningSent = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: true), + UpdatedBy = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false), + DeletedAt = table.Column(type: "datetime2", nullable: true), + DeletedBy = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Budgets", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BudgetApprovals", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + BudgetId = table.Column(type: "uniqueidentifier", nullable: false), + FromPhase = table.Column(type: "int", nullable: false), + ToPhase = table.Column(type: "int", nullable: false), + ApproverUserId = table.Column(type: "uniqueidentifier", nullable: true), + Decision = table.Column(type: "int", nullable: false), + Comment = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + ApprovedAt = table.Column(type: "datetime2", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: true), + UpdatedBy = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetApprovals", x => x.Id); + table.ForeignKey( + name: "FK_BudgetApprovals_Budgets_BudgetId", + column: x => x.BudgetId, + principalTable: "Budgets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BudgetChangelogs", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + BudgetId = table.Column(type: "uniqueidentifier", nullable: false), + EntityType = table.Column(type: "int", nullable: false), + EntityId = table.Column(type: "uniqueidentifier", nullable: true), + Action = table.Column(type: "int", nullable: false), + PhaseAtChange = table.Column(type: "int", nullable: true), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + UserName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Summary = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + FieldChangesJson = table.Column(type: "nvarchar(max)", nullable: true), + ContextNote = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: true), + UpdatedBy = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetChangelogs", x => x.Id); + table.ForeignKey( + name: "FK_BudgetChangelogs_Budgets_BudgetId", + column: x => x.BudgetId, + principalTable: "Budgets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BudgetDetails", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + BudgetId = table.Column(type: "uniqueidentifier", nullable: false), + GroupCode = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + GroupName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + ItemCode = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + NoiDung = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + DonViTinh = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + KhoiLuong = table.Column(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false), + DonGia = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + ThanhTien = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + Order = table.Column(type: "int", nullable: false), + GhiChu = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "uniqueidentifier", nullable: true), + UpdatedBy = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetDetails", x => x.Id); + table.ForeignKey( + name: "FK_BudgetDetails_Budgets_BudgetId", + column: x => x.BudgetId, + principalTable: "Budgets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PurchaseEvaluations_BudgetId", + table: "PurchaseEvaluations", + column: "BudgetId"); + + migrationBuilder.CreateIndex( + name: "IX_Contracts_BudgetId", + table: "Contracts", + column: "BudgetId"); + + migrationBuilder.CreateIndex( + name: "IX_BudgetApprovals_BudgetId_ApprovedAt", + table: "BudgetApprovals", + columns: new[] { "BudgetId", "ApprovedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_BudgetChangelogs_BudgetId_CreatedAt", + table: "BudgetChangelogs", + columns: new[] { "BudgetId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_BudgetChangelogs_BudgetId_EntityType", + table: "BudgetChangelogs", + columns: new[] { "BudgetId", "EntityType" }); + + migrationBuilder.CreateIndex( + name: "IX_BudgetDetails_BudgetId_Order", + table: "BudgetDetails", + columns: new[] { "BudgetId", "Order" }); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_MaNganSach", + table: "Budgets", + column: "MaNganSach", + unique: true, + filter: "[MaNganSach] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_NamNganSach", + table: "Budgets", + column: "NamNganSach"); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_Phase_IsDeleted", + table: "Budgets", + columns: new[] { "Phase", "IsDeleted" }); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_ProjectId", + table: "Budgets", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_Budgets_SlaDeadline", + table: "Budgets", + column: "SlaDeadline"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BudgetApprovals"); + + migrationBuilder.DropTable( + name: "BudgetChangelogs"); + + migrationBuilder.DropTable( + name: "BudgetDetails"); + + migrationBuilder.DropTable( + name: "Budgets"); + + migrationBuilder.DropIndex( + name: "IX_PurchaseEvaluations_BudgetId", + table: "PurchaseEvaluations"); + + migrationBuilder.DropIndex( + name: "IX_Contracts_BudgetId", + table: "Contracts"); + + migrationBuilder.DropColumn( + name: "BudgetId", + table: "PurchaseEvaluations"); + + migrationBuilder.DropColumn( + name: "BudgetId", + table: "Contracts"); + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index f28a665..21db752 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -125,12 +125,274 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.ToTable("UserTokens", (string)null); }); + modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaNganSach") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NamNganSach") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenNganSach") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TongNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaNganSach") + .IsUnique() + .HasFilter("[MaNganSach] IS NOT NULL"); + + b.HasIndex("NamNganSach"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Budgets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "ApprovedAt"); + + b.ToTable("BudgetApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "CreatedAt"); + + b.HasIndex("BudgetId", "EntityType"); + + b.ToTable("BudgetChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId", "Order"); + + b.ToTable("BudgetDetails", (string)null); + }); + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + b.Property("BypassProcurementAndCCM") .HasColumnType("bit"); @@ -206,6 +468,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.HasKey("Id"); + b.HasIndex("BudgetId"); + b.HasIndex("MaHopDong") .IsUnique() .HasFilter("[MaHopDong] IS NOT NULL"); @@ -1910,6 +2174,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("BudgetId") + .HasColumnType("uniqueidentifier"); + b.Property("ContractId") .HasColumnType("uniqueidentifier"); @@ -1983,6 +2250,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.HasKey("Id"); + b.HasIndex("BudgetId"); + b.HasIndex("ContractId"); b.HasIndex("MaPhieu") @@ -2558,6 +2827,39 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations .IsRequired(); }); + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Approvals") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Changelogs") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b => + { + b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget") + .WithMany("Details") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Budget"); + }); + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => { b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") @@ -2838,6 +3140,15 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.Navigation("Step"); }); + modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b => + { + b.Navigation("Approvals"); + + b.Navigation("Changelogs"); + + b.Navigation("Details"); + }); + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => { b.Navigation("Approvals");