From e27c54702ad526dce75757323c833effd4375cfe Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 23 Apr 2026 12:21:39 +0700 Subject: [PATCH] [CLAUDE] Domain+App+Api: 4 master catalogs cho Details (migration 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback: thêm Master Data cho phần Chi tiết (line items) — autocomplete khi user nhập field thay vì gõ tay free text. ## 4 entities mới (Domain/Master/Catalogs/) | Entity | Bảng | Dùng cho HĐ Detail | |---|---|---| | UnitOfMeasure | UnitsOfMeasure | Tất cả 7 type (DonViTinh) | | MaterialItem | MaterialItems | NCC + Mua bán + Nguyên tắc NCC (MaSP/TenSP) | | ServiceItem | ServiceItems | Dịch vụ + Nguyên tắc DV (MaDichVu/TenDichVu) | | WorkItem | WorkItems | Thầu phụ + Giao khoán (HangMuc/MaCongViec) | Common pattern: Code unique (filter IsDeleted=0) + Name + Category + DefaultUnit + AuditableEntity (soft delete) + IsActive flag. ## Migration 10: AddMasterCatalogs 3-file rule (gotcha #17). Total DB: 32 → 36 tables. Apply LocalDB OK. ## Seed defaults (idempotent — skip per-table nếu có row) - 20 UnitsOfMeasure: m2, m3, kg, tấn, lít, ngc (ngày công), giờ, gói, ... - 15 MaterialItems demo: xi măng PCB40, cát vàng, đá 1x2, thép D10, gạch 4 lỗ, sơn lót, ống PVC... - 10 ServiceItems demo: vận chuyển, bảo trì, tư vấn, kiểm định, vệ sinh... - 15 WorkItems demo: đào móng, đổ bê tông, xây tường, trát, lát gạch, sơn nước, lắp điện, lắp nước, thấm chống... ## CQRS (Application/Master/Catalogs/CatalogsFeatures.cs ~290 dòng) Mỗi catalog 5 handlers (List filter q+category, Create với unique code guard, Update, Delete soft). FluentValidation max length per spec EF. ## Controller (Api/Controllers/CatalogsController.cs) 13 endpoints: - GET /api/catalogs/{units|materials|services|work-items} - POST /api/catalogs/{kind} (Admin role) - PUT /api/catalogs/{kind}/{id} (Admin role) - DELETE /api/catalogs/{kind}/{id} (Admin role) Read open cho mọi role (FE Details add form autocomplete cần list). ## Menu (5 mới) - Catalogs (group, Master parent, order 24, "Library" icon) - CatalogUnits "Đơn vị tính" (Ruler) - CatalogMaterials "Vật tư / SP" (Package) - CatalogServices "Dịch vụ" (Wrench) - CatalogWorkItems "Hạng mục công việc" (ListChecks) Admin auto-grant tất cả CRUD action (qua SeedAdminPermissionsAsync loop MenuKeys.All). ## Build dotnet build BE pass (0 error) ## Note FE admin page CatalogsPage + datalist autocomplete trong Details form sẽ ở commit kế tiếp. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Controllers/CatalogsController.cs | 136 ++ .../Interfaces/IApplicationDbContext.cs | 6 + .../Master/Catalogs/CatalogsFeatures.cs | 334 +++ .../SolutionErp.Domain/Identity/MenuKeys.cs | 8 + .../Master/Catalogs/MaterialItem.cs | 16 + .../Master/Catalogs/ServiceItem.cs | 14 + .../Master/Catalogs/UnitOfMeasure.cs | 12 + .../Master/Catalogs/WorkItem.cs | 14 + .../Persistence/ApplicationDbContext.cs | 5 + .../Configurations/CatalogsConfiguration.cs | 75 + .../Persistence/DbInitializer.cs | 107 + ...260423051650_AddMasterCatalogs.Designer.cs | 2164 +++++++++++++++++ .../20260423051650_AddMasterCatalogs.cs | 168 ++ .../ApplicationDbContextModelSnapshot.cs | 243 ++ 14 files changed, 3302 insertions(+) create mode 100644 src/Backend/SolutionErp.Api/Controllers/CatalogsController.cs create mode 100644 src/Backend/SolutionErp.Application/Master/Catalogs/CatalogsFeatures.cs create mode 100644 src/Backend/SolutionErp.Domain/Master/Catalogs/MaterialItem.cs create mode 100644 src/Backend/SolutionErp.Domain/Master/Catalogs/ServiceItem.cs create mode 100644 src/Backend/SolutionErp.Domain/Master/Catalogs/UnitOfMeasure.cs create mode 100644 src/Backend/SolutionErp.Domain/Master/Catalogs/WorkItem.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/CatalogsConfiguration.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.cs diff --git a/src/Backend/SolutionErp.Api/Controllers/CatalogsController.cs b/src/Backend/SolutionErp.Api/Controllers/CatalogsController.cs new file mode 100644 index 0000000..1f8acd2 --- /dev/null +++ b/src/Backend/SolutionErp.Api/Controllers/CatalogsController.cs @@ -0,0 +1,136 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SolutionErp.Application.Master.Catalogs; + +namespace SolutionErp.Api.Controllers; + +// Master catalogs API — 4 sub-resource (units, materials, services, work-items) +// dùng Mediator dispatch theo command/query type. Authorize = đăng nhập đủ +// (mọi role được đọc cho autocomplete; ghi cần Admin). + +[ApiController] +[Route("api/catalogs")] +[Authorize] +public class CatalogsController(IMediator mediator) : ControllerBase +{ + // ===== Units ===== + [HttpGet("units")] + public async Task> ListUnits([FromQuery] string? q, CancellationToken ct) + => await mediator.Send(new ListUnitsQuery(q), ct); + + [HttpPost("units")] + [Authorize(Roles = "Admin")] + public async Task CreateUnit([FromBody] CreateUnitCommand body, CancellationToken ct) + { + var id = await mediator.Send(body, ct); + return CreatedAtAction(nameof(ListUnits), new { id }, new { id }); + } + + [HttpPut("units/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task UpdateUnit(Guid id, [FromBody] UpdateUnitCommand body, CancellationToken ct) + { + if (id != body.Id) return BadRequest("Id mismatch"); + await mediator.Send(body, ct); + return NoContent(); + } + + [HttpDelete("units/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteUnit(Guid id, CancellationToken ct) + { + await mediator.Send(new DeleteUnitCommand(id), ct); + return NoContent(); + } + + // ===== Materials ===== + [HttpGet("materials")] + public async Task> ListMaterials([FromQuery] string? q, [FromQuery] string? category, CancellationToken ct) + => await mediator.Send(new ListMaterialsQuery(q, category), ct); + + [HttpPost("materials")] + [Authorize(Roles = "Admin")] + public async Task CreateMaterial([FromBody] CreateMaterialCommand body, CancellationToken ct) + { + var id = await mediator.Send(body, ct); + return CreatedAtAction(nameof(ListMaterials), new { id }, new { id }); + } + + [HttpPut("materials/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task UpdateMaterial(Guid id, [FromBody] UpdateMaterialCommand body, CancellationToken ct) + { + if (id != body.Id) return BadRequest("Id mismatch"); + await mediator.Send(body, ct); + return NoContent(); + } + + [HttpDelete("materials/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteMaterial(Guid id, CancellationToken ct) + { + await mediator.Send(new DeleteMaterialCommand(id), ct); + return NoContent(); + } + + // ===== Services ===== + [HttpGet("services")] + public async Task> ListServices([FromQuery] string? q, [FromQuery] string? category, CancellationToken ct) + => await mediator.Send(new ListServicesQuery(q, category), ct); + + [HttpPost("services")] + [Authorize(Roles = "Admin")] + public async Task CreateService([FromBody] CreateServiceCommand body, CancellationToken ct) + { + var id = await mediator.Send(body, ct); + return CreatedAtAction(nameof(ListServices), new { id }, new { id }); + } + + [HttpPut("services/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task UpdateService(Guid id, [FromBody] UpdateServiceCommand body, CancellationToken ct) + { + if (id != body.Id) return BadRequest("Id mismatch"); + await mediator.Send(body, ct); + return NoContent(); + } + + [HttpDelete("services/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteService(Guid id, CancellationToken ct) + { + await mediator.Send(new DeleteServiceCommand(id), ct); + return NoContent(); + } + + // ===== Work Items ===== + [HttpGet("work-items")] + public async Task> ListWorkItems([FromQuery] string? q, [FromQuery] string? category, CancellationToken ct) + => await mediator.Send(new ListWorkItemsQuery(q, category), ct); + + [HttpPost("work-items")] + [Authorize(Roles = "Admin")] + public async Task CreateWorkItem([FromBody] CreateWorkItemCommand body, CancellationToken ct) + { + var id = await mediator.Send(body, ct); + return CreatedAtAction(nameof(ListWorkItems), new { id }, new { id }); + } + + [HttpPut("work-items/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task UpdateWorkItem(Guid id, [FromBody] UpdateWorkItemCommand body, CancellationToken ct) + { + if (id != body.Id) return BadRequest("Id mismatch"); + await mediator.Send(body, ct); + return NoContent(); + } + + [HttpDelete("work-items/{id:guid}")] + [Authorize(Roles = "Admin")] + public async Task DeleteWorkItem(Guid id, CancellationToken ct) + { + await mediator.Send(new DeleteWorkItemCommand(id), ct); + return NoContent(); + } +} diff --git a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs index aa40f18..1d0a648 100644 --- a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs @@ -4,6 +4,7 @@ using SolutionErp.Domain.Contracts.Details; using SolutionErp.Domain.Forms; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Master; +using SolutionErp.Domain.Master.Catalogs; using SolutionErp.Domain.Notifications; namespace SolutionErp.Application.Common.Interfaces; @@ -13,6 +14,11 @@ public interface IApplicationDbContext DbSet Suppliers { get; } DbSet Projects { get; } DbSet Departments { get; } + // 4 master catalogs cho Details add form (autocomplete) + DbSet UnitsOfMeasure { get; } + DbSet MaterialItems { get; } + DbSet ServiceItems { get; } + DbSet WorkItems { get; } DbSet MenuItems { get; } DbSet Permissions { get; } DbSet ContractTemplates { get; } diff --git a/src/Backend/SolutionErp.Application/Master/Catalogs/CatalogsFeatures.cs b/src/Backend/SolutionErp.Application/Master/Catalogs/CatalogsFeatures.cs new file mode 100644 index 0000000..236b044 --- /dev/null +++ b/src/Backend/SolutionErp.Application/Master/Catalogs/CatalogsFeatures.cs @@ -0,0 +1,334 @@ +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using SolutionErp.Application.Common.Exceptions; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Domain.Master.Catalogs; + +namespace SolutionErp.Application.Master.Catalogs; + +// CRUD CQRS cho 4 master catalogs (Units, Materials, Services, WorkItems). +// Pattern lặp đi lặp lại với mỗi catalog — gộp 1 file để dễ maintain, mỗi +// catalog 5 handlers (List + GetById + Create + Update + Delete). +// +// Endpoints expose qua CatalogsController: +// GET /api/catalogs/{kind} — list (filter q + category) +// GET /api/catalogs/{kind}/{id} — get by id +// POST /api/catalogs/{kind} — create +// PUT /api/catalogs/{kind}/{id} — update +// DELETE /api/catalogs/{kind}/{id} — soft delete +// kind ∈ {units, materials, services, work-items} + +// ===== DTOs ===== + +public record UnitOfMeasureDto(Guid Id, string Code, string Name, string? Description); +public record MaterialItemDto(Guid Id, string Code, string Name, string? Category, string? DefaultUnit, string? Specification, string? OriginCountry, bool IsActive); +public record ServiceItemDto(Guid Id, string Code, string Name, string? Category, string? DefaultUnit, string? Description, bool IsActive); +public record WorkItemDto(Guid Id, string Code, string Name, string? Category, string? DefaultUnit, string? Description, bool IsActive); + +// ===== UnitOfMeasure ===== + +public record ListUnitsQuery(string? Q = null) : IRequest>; +public class ListUnitsHandler(IApplicationDbContext db) : IRequestHandler> +{ + public async Task> Handle(ListUnitsQuery req, CancellationToken ct) + { + var q = db.UnitsOfMeasure.AsNoTracking().AsQueryable(); + if (!string.IsNullOrWhiteSpace(req.Q)) + { + var s = req.Q.ToLower(); + q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s)); + } + return await q.OrderBy(x => x.Code) + .Select(x => new UnitOfMeasureDto(x.Id, x.Code, x.Name, x.Description)) + .ToListAsync(ct); + } +} + +public record CreateUnitCommand(string Code, string Name, string? Description) : IRequest; +public class CreateUnitValidator : AbstractValidator +{ + public CreateUnitValidator() + { + RuleFor(x => x.Code).NotEmpty().MaximumLength(20); + RuleFor(x => x.Name).NotEmpty().MaximumLength(100); + } +} +public class CreateUnitHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(CreateUnitCommand req, CancellationToken ct) + { + if (await db.UnitsOfMeasure.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + var entity = new UnitOfMeasure { Code = req.Code, Name = req.Name, Description = req.Description }; + db.UnitsOfMeasure.Add(entity); + await db.SaveChangesAsync(ct); + return entity.Id; + } +} + +public record UpdateUnitCommand(Guid Id, string Code, string Name, string? Description) : IRequest; +public class UpdateUnitHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(UpdateUnitCommand req, CancellationToken ct) + { + var entity = await db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("UnitOfMeasure", req.Id); + if (entity.Code != req.Code && await db.UnitsOfMeasure.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + entity.Code = req.Code; + entity.Name = req.Name; + entity.Description = req.Description; + await db.SaveChangesAsync(ct); + } +} + +public record DeleteUnitCommand(Guid Id) : IRequest; +public class DeleteUnitHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(DeleteUnitCommand req, CancellationToken ct) + { + var entity = await db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("UnitOfMeasure", req.Id); + db.UnitsOfMeasure.Remove(entity); // Soft delete via AuditingInterceptor + await db.SaveChangesAsync(ct); + } +} + +// ===== MaterialItem ===== + +public record ListMaterialsQuery(string? Q = null, string? Category = null) : IRequest>; +public class ListMaterialsHandler(IApplicationDbContext db) : IRequestHandler> +{ + public async Task> Handle(ListMaterialsQuery req, CancellationToken ct) + { + var q = db.MaterialItems.AsNoTracking().AsQueryable(); + if (!string.IsNullOrWhiteSpace(req.Q)) + { + var s = req.Q.ToLower(); + q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s)); + } + if (!string.IsNullOrWhiteSpace(req.Category)) + q = q.Where(x => x.Category == req.Category); + return await q.OrderBy(x => x.Category).ThenBy(x => x.Code) + .Select(x => new MaterialItemDto(x.Id, x.Code, x.Name, x.Category, x.DefaultUnit, x.Specification, x.OriginCountry, x.IsActive)) + .ToListAsync(ct); + } +} + +public record CreateMaterialCommand(string Code, string Name, string? Category, string? DefaultUnit, string? Specification, string? OriginCountry, bool IsActive) : IRequest; +public class CreateMaterialValidator : AbstractValidator +{ + public CreateMaterialValidator() + { + RuleFor(x => x.Code).NotEmpty().MaximumLength(50); + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + } +} +public class CreateMaterialHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(CreateMaterialCommand req, CancellationToken ct) + { + if (await db.MaterialItems.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + var entity = new MaterialItem + { + Code = req.Code, Name = req.Name, Category = req.Category, + DefaultUnit = req.DefaultUnit, Specification = req.Specification, + OriginCountry = req.OriginCountry, IsActive = req.IsActive, + }; + db.MaterialItems.Add(entity); + await db.SaveChangesAsync(ct); + return entity.Id; + } +} + +public record UpdateMaterialCommand(Guid Id, string Code, string Name, string? Category, string? DefaultUnit, string? Specification, string? OriginCountry, bool IsActive) : IRequest; +public class UpdateMaterialHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(UpdateMaterialCommand req, CancellationToken ct) + { + var entity = await db.MaterialItems.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("MaterialItem", req.Id); + if (entity.Code != req.Code && await db.MaterialItems.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + entity.Code = req.Code; + entity.Name = req.Name; + entity.Category = req.Category; + entity.DefaultUnit = req.DefaultUnit; + entity.Specification = req.Specification; + entity.OriginCountry = req.OriginCountry; + entity.IsActive = req.IsActive; + await db.SaveChangesAsync(ct); + } +} + +public record DeleteMaterialCommand(Guid Id) : IRequest; +public class DeleteMaterialHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(DeleteMaterialCommand req, CancellationToken ct) + { + var entity = await db.MaterialItems.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("MaterialItem", req.Id); + db.MaterialItems.Remove(entity); + await db.SaveChangesAsync(ct); + } +} + +// ===== ServiceItem ===== + +public record ListServicesQuery(string? Q = null, string? Category = null) : IRequest>; +public class ListServicesHandler(IApplicationDbContext db) : IRequestHandler> +{ + public async Task> Handle(ListServicesQuery req, CancellationToken ct) + { + var q = db.ServiceItems.AsNoTracking().AsQueryable(); + if (!string.IsNullOrWhiteSpace(req.Q)) + { + var s = req.Q.ToLower(); + q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s)) +; + } + if (!string.IsNullOrWhiteSpace(req.Category)) + q = q.Where(x => x.Category == req.Category) +; + return await q.OrderBy(x => x.Category).ThenBy(x => x.Code) + .Select(x => new ServiceItemDto(x.Id, x.Code, x.Name, x.Category, x.DefaultUnit, x.Description, x.IsActive)) + .ToListAsync(ct); + } +} + +public record CreateServiceCommand(string Code, string Name, string? Category, string? DefaultUnit, string? Description, bool IsActive) : IRequest; +public class CreateServiceValidator : AbstractValidator +{ + public CreateServiceValidator() + { + RuleFor(x => x.Code).NotEmpty().MaximumLength(50); + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + } +} +public class CreateServiceHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(CreateServiceCommand req, CancellationToken ct) + { + if (await db.ServiceItems.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + var entity = new ServiceItem + { + Code = req.Code, Name = req.Name, Category = req.Category, + DefaultUnit = req.DefaultUnit, Description = req.Description, IsActive = req.IsActive, + }; + db.ServiceItems.Add(entity); + await db.SaveChangesAsync(ct); + return entity.Id; + } +} + +public record UpdateServiceCommand(Guid Id, string Code, string Name, string? Category, string? DefaultUnit, string? Description, bool IsActive) : IRequest; +public class UpdateServiceHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(UpdateServiceCommand req, CancellationToken ct) + { + var entity = await db.ServiceItems.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("ServiceItem", req.Id); + if (entity.Code != req.Code && await db.ServiceItems.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + entity.Code = req.Code; + entity.Name = req.Name; + entity.Category = req.Category; + entity.DefaultUnit = req.DefaultUnit; + entity.Description = req.Description; + entity.IsActive = req.IsActive; + await db.SaveChangesAsync(ct); + } +} + +public record DeleteServiceCommand(Guid Id) : IRequest; +public class DeleteServiceHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(DeleteServiceCommand req, CancellationToken ct) + { + var entity = await db.ServiceItems.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("ServiceItem", req.Id); + db.ServiceItems.Remove(entity); + await db.SaveChangesAsync(ct); + } +} + +// ===== WorkItem ===== + +public record ListWorkItemsQuery(string? Q = null, string? Category = null) : IRequest>; +public class ListWorkItemsHandler(IApplicationDbContext db) : IRequestHandler> +{ + public async Task> Handle(ListWorkItemsQuery req, CancellationToken ct) + { + var q = db.WorkItems.AsNoTracking().AsQueryable(); + if (!string.IsNullOrWhiteSpace(req.Q)) + { + var s = req.Q.ToLower(); + q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s)); + } + if (!string.IsNullOrWhiteSpace(req.Category)) + q = q.Where(x => x.Category == req.Category); + return await q.OrderBy(x => x.Category).ThenBy(x => x.Code) + .Select(x => new WorkItemDto(x.Id, x.Code, x.Name, x.Category, x.DefaultUnit, x.Description, x.IsActive)) + .ToListAsync(ct); + } +} + +public record CreateWorkItemCommand(string Code, string Name, string? Category, string? DefaultUnit, string? Description, bool IsActive) : IRequest; +public class CreateWorkItemValidator : AbstractValidator +{ + public CreateWorkItemValidator() + { + RuleFor(x => x.Code).NotEmpty().MaximumLength(50); + RuleFor(x => x.Name).NotEmpty().MaximumLength(200); + } +} +public class CreateWorkItemHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(CreateWorkItemCommand req, CancellationToken ct) + { + if (await db.WorkItems.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + var entity = new WorkItem + { + Code = req.Code, Name = req.Name, Category = req.Category, + DefaultUnit = req.DefaultUnit, Description = req.Description, IsActive = req.IsActive, + }; + db.WorkItems.Add(entity); + await db.SaveChangesAsync(ct); + return entity.Id; + } +} + +public record UpdateWorkItemCommand(Guid Id, string Code, string Name, string? Category, string? DefaultUnit, string? Description, bool IsActive) : IRequest; +public class UpdateWorkItemHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(UpdateWorkItemCommand req, CancellationToken ct) + { + var entity = await db.WorkItems.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("WorkItem", req.Id); + if (entity.Code != req.Code && await db.WorkItems.AnyAsync(x => x.Code == req.Code, ct)) + throw new ConflictException($"Mã '{req.Code}' đã tồn tại."); + entity.Code = req.Code; + entity.Name = req.Name; + entity.Category = req.Category; + entity.DefaultUnit = req.DefaultUnit; + entity.Description = req.Description; + entity.IsActive = req.IsActive; + await db.SaveChangesAsync(ct); + } +} + +public record DeleteWorkItemCommand(Guid Id) : IRequest; +public class DeleteWorkItemHandler(IApplicationDbContext db) : IRequestHandler +{ + public async Task Handle(DeleteWorkItemCommand req, CancellationToken ct) + { + var entity = await db.WorkItems.FirstOrDefaultAsync(x => x.Id == req.Id, ct) + ?? throw new NotFoundException("WorkItem", req.Id); + db.WorkItems.Remove(entity); + await db.SaveChangesAsync(ct); + } +} diff --git a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs index 7e95ead..2bd9b25 100644 --- a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs +++ b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs @@ -18,6 +18,13 @@ public static class MenuKeys public const string Permissions = "Permissions"; public const string Workflows = "Workflows"; + // 4 master catalogs cho Details add form (autocomplete) — admin CRUD + public const string Catalogs = "Catalogs"; // group + public const string CatalogUnits = "CatalogUnits"; + public const string CatalogMaterials = "CatalogMaterials"; + public const string CatalogServices = "CatalogServices"; + public const string CatalogWorkItems = "CatalogWorkItems"; + // Per-contract-type menu groups + 3 action leaves each. // Key format: Ct_[_] — prefix `Ct_` distinguishes from // top-level. Menu tree endpoint (GetMyMenuTreeQuery) treats descendants of @@ -39,6 +46,7 @@ public static class MenuKeys [ Dashboard, Master, Suppliers, Projects, Departments, + Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems, Contracts, Forms, Reports, System, Users, Roles, Permissions, Workflows, ]; diff --git a/src/Backend/SolutionErp.Domain/Master/Catalogs/MaterialItem.cs b/src/Backend/SolutionErp.Domain/Master/Catalogs/MaterialItem.cs new file mode 100644 index 0000000..bfe3840 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Master/Catalogs/MaterialItem.cs @@ -0,0 +1,16 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Master.Catalogs; + +// Master danh mục vật tư / sản phẩm — dùng cho HĐ NCC, Mua bán, Nguyên tắc NCC. +// Khi user nhập Details: autocomplete từ danh mục này để autofill Mã/Tên/ĐVT/Spec. +public class MaterialItem : AuditableEntity +{ + public string Code { get; set; } = ""; // Mã SP (unique) + public string Name { get; set; } = ""; // Tên SP + public string? Category { get; set; } // Nhóm SP (vd "Vật liệu xây dựng", "Sắt thép") + public string? DefaultUnit { get; set; } // ĐVT mặc định (autofill khi chọn) + public string? Specification { get; set; } // Thông số kỹ thuật mẫu + public string? OriginCountry { get; set; } // Xuất xứ + public bool IsActive { get; set; } = true; +} diff --git a/src/Backend/SolutionErp.Domain/Master/Catalogs/ServiceItem.cs b/src/Backend/SolutionErp.Domain/Master/Catalogs/ServiceItem.cs new file mode 100644 index 0000000..8e9ef36 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Master/Catalogs/ServiceItem.cs @@ -0,0 +1,14 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Master.Catalogs; + +// Master danh mục dịch vụ — dùng cho HĐ Dịch vụ + Nguyên tắc Dịch vụ. +public class ServiceItem : AuditableEntity +{ + public string Code { get; set; } = ""; // Mã DV + public string Name { get; set; } = ""; // Tên DV + public string? Category { get; set; } // Loại DV (vd "Vận chuyển", "Bảo trì", "Tư vấn") + public string? DefaultUnit { get; set; } // ĐVT mặc định (giờ, ngày, gói, lần...) + public string? Description { get; set; } + public bool IsActive { get; set; } = true; +} diff --git a/src/Backend/SolutionErp.Domain/Master/Catalogs/UnitOfMeasure.cs b/src/Backend/SolutionErp.Domain/Master/Catalogs/UnitOfMeasure.cs new file mode 100644 index 0000000..6b9d23a --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Master/Catalogs/UnitOfMeasure.cs @@ -0,0 +1,12 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Master.Catalogs; + +// Master danh mục đơn vị tính dùng cho mọi loại HĐ Details (m2, kg, ngày +// công, gói, bộ, ...). Code = unique short identifier; Name = label tiếng Việt. +public class UnitOfMeasure : AuditableEntity +{ + public string Code { get; set; } = ""; // vd "m2", "kg", "ngc" + public string Name { get; set; } = ""; // vd "Mét vuông", "Kilogram", "Ngày công" + public string? Description { get; set; } +} diff --git a/src/Backend/SolutionErp.Domain/Master/Catalogs/WorkItem.cs b/src/Backend/SolutionErp.Domain/Master/Catalogs/WorkItem.cs new file mode 100644 index 0000000..c8d4be3 --- /dev/null +++ b/src/Backend/SolutionErp.Domain/Master/Catalogs/WorkItem.cs @@ -0,0 +1,14 @@ +using SolutionErp.Domain.Common; + +namespace SolutionErp.Domain.Master.Catalogs; + +// Master danh mục hạng mục công việc — dùng cho HĐ Thầu phụ + Giao khoán. +public class WorkItem : AuditableEntity +{ + public string Code { get; set; } = ""; // Mã hạng mục (vd "DAO_MONG", "DO_BTONG") + public string Name { get; set; } = ""; // Tên hạng mục + public string? Category { get; set; } // Nhóm (vd "Phần thô", "Hoàn thiện", "Cơ điện") + public string? DefaultUnit { get; set; } // ĐVT mặc định + public string? Description { get; set; } // Mô tả + spec yêu cầu + public bool IsActive { get; set; } = true; +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs index dd45e88..3779fc0 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs @@ -6,6 +6,7 @@ using SolutionErp.Domain.Contracts.Details; using SolutionErp.Domain.Forms; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Master; +using SolutionErp.Domain.Master.Catalogs; using SolutionErp.Domain.Notifications; namespace SolutionErp.Infrastructure.Persistence; @@ -18,6 +19,10 @@ public class ApplicationDbContext public DbSet Suppliers => Set(); public DbSet Projects => Set(); public DbSet Departments => Set(); + public DbSet UnitsOfMeasure => Set(); + public DbSet MaterialItems => Set(); + public DbSet ServiceItems => Set(); + public DbSet WorkItems => Set(); public DbSet MenuItems => Set(); public DbSet Permissions => Set(); public DbSet ContractTemplates => Set(); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/CatalogsConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/CatalogsConfiguration.cs new file mode 100644 index 0000000..13ad298 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/CatalogsConfiguration.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SolutionErp.Domain.Master.Catalogs; + +namespace SolutionErp.Infrastructure.Persistence.Configurations; + +// EF config cho 4 master catalogs (UnitsOfMeasure, MaterialItems, +// ServiceItems, WorkItems) — phục vụ autocomplete trong Details add form. +// Pattern: Code unique + IsDeleted query filter + IsActive flag soft-archive. + +public class UnitOfMeasureConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("UnitsOfMeasure"); + b.HasKey(x => x.Id); + b.Property(x => x.Code).HasMaxLength(20).IsRequired(); + b.Property(x => x.Name).HasMaxLength(100).IsRequired(); + b.Property(x => x.Description).HasMaxLength(500); + b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); + b.HasQueryFilter(x => !x.IsDeleted); + } +} + +public class MaterialItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("MaterialItems"); + b.HasKey(x => x.Id); + b.Property(x => x.Code).HasMaxLength(50).IsRequired(); + b.Property(x => x.Name).HasMaxLength(200).IsRequired(); + b.Property(x => x.Category).HasMaxLength(100); + b.Property(x => x.DefaultUnit).HasMaxLength(50); + b.Property(x => x.Specification).HasMaxLength(1000); + b.Property(x => x.OriginCountry).HasMaxLength(100); + b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); + b.HasIndex(x => x.Category); + b.HasQueryFilter(x => !x.IsDeleted); + } +} + +public class ServiceItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("ServiceItems"); + b.HasKey(x => x.Id); + b.Property(x => x.Code).HasMaxLength(50).IsRequired(); + b.Property(x => x.Name).HasMaxLength(200).IsRequired(); + b.Property(x => x.Category).HasMaxLength(100); + b.Property(x => x.DefaultUnit).HasMaxLength(50); + b.Property(x => x.Description).HasMaxLength(1000); + b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); + b.HasIndex(x => x.Category); + b.HasQueryFilter(x => !x.IsDeleted); + } +} + +public class WorkItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.ToTable("WorkItems"); + b.HasKey(x => x.Id); + b.Property(x => x.Code).HasMaxLength(50).IsRequired(); + b.Property(x => x.Name).HasMaxLength(200).IsRequired(); + b.Property(x => x.Category).HasMaxLength(100); + b.Property(x => x.DefaultUnit).HasMaxLength(50); + b.Property(x => x.Description).HasMaxLength(1000); + b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); + b.HasIndex(x => x.Category); + b.HasQueryFilter(x => !x.IsDeleted); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index cd61c10..3c7ff07 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -7,6 +7,7 @@ using SolutionErp.Domain.Contracts; using SolutionErp.Domain.Forms; using SolutionErp.Domain.Identity; using SolutionErp.Domain.Master; +using SolutionErp.Domain.Master.Catalogs; namespace SolutionErp.Infrastructure.Persistence; @@ -35,6 +36,7 @@ public static class DbInitializer await SeedDemoMasterDataAsync(db, logger); await SeedContractTemplatesAsync(db, logger); await SeedWorkflowDefinitionsAsync(db, logger); + await SeedCatalogsAsync(db, logger); // Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create). // Idempotent: chỉ HĐ MaHopDong IS NULL được gen. @@ -44,6 +46,105 @@ public static class DbInitializer await WarnDefaultAdminPasswordAsync(userManager, logger); } + // Seed 4 master catalogs với defaults cho user nhập liệu Details. Idempotent: + // skip per-table nếu đã có row (admin có thể đã thêm/sửa — không clobber). + private static async Task SeedCatalogsAsync(ApplicationDbContext db, ILogger logger) + { + // 1. UnitsOfMeasure (~17) + if (!await db.UnitsOfMeasure.AnyAsync()) + { + var units = new[] + { + ("m2", "Mét vuông"), ("m3", "Mét khối"), ("m", "Mét"), ("md", "Mét dài"), + ("kg", "Kilogram"), ("tan", "Tấn"), ("g", "Gram"), + ("l", "Lít"), ("ml", "Mililít"), + ("cai", "Cái"), ("chiec", "Chiếc"), ("bo", "Bộ"), ("goi", "Gói"), ("hop", "Hộp"), + ("ngc", "Ngày công"), ("h", "Giờ"), ("ca", "Ca"), + ("nguoi", "Người"), ("luot", "Lượt"), ("lan", "Lần"), + }; + foreach (var (code, name) in units) + db.UnitsOfMeasure.Add(new UnitOfMeasure { Code = code, Name = name }); + await db.SaveChangesAsync(); + logger.LogInformation("Seed {Count} UnitsOfMeasure", units.Length); + } + + // 2. MaterialItems (~15 demo) + if (!await db.MaterialItems.AnyAsync()) + { + var materials = new[] + { + ("XM-PCB40", "Xi măng PCB40", "Xi măng", "kg"), + ("XM-PCB30", "Xi măng PCB30", "Xi măng", "kg"), + ("CAT-VANG", "Cát vàng đổ bê tông", "Cát đá", "m3"), + ("CAT-DEN", "Cát đen xây trát", "Cát đá", "m3"), + ("DA-1x2", "Đá 1x2", "Cát đá", "m3"), + ("DA-4x6", "Đá 4x6", "Cát đá", "m3"), + ("THEP-D6", "Thép cuộn phi 6", "Sắt thép", "kg"), + ("THEP-D10", "Thép cây phi 10", "Sắt thép", "kg"), + ("THEP-D14", "Thép cây phi 14", "Sắt thép", "kg"), + ("THEP-D18", "Thép cây phi 18", "Sắt thép", "kg"), + ("GACH-XANH", "Gạch xây 4 lỗ", "Gạch ngói", "cai"), + ("GACH-DAT-NUNG", "Gạch đặc đất nung", "Gạch ngói", "cai"), + ("SON-LOT", "Sơn lót chống kiềm", "Sơn", "l"), + ("SON-NGOAI", "Sơn ngoại thất", "Sơn", "l"), + ("ONG-PVC-90", "Ống PVC phi 90", "Vật tư cấp thoát nước", "m"), + }; + foreach (var (code, name, cat, unit) in materials) + db.MaterialItems.Add(new MaterialItem { Code = code, Name = name, Category = cat, DefaultUnit = unit }); + await db.SaveChangesAsync(); + logger.LogInformation("Seed {Count} MaterialItems", materials.Length); + } + + // 3. ServiceItems (~10 demo) + if (!await db.ServiceItems.AnyAsync()) + { + var services = new[] + { + ("VC-OTO", "Vận chuyển ô tô tải", "Vận chuyển", "ca"), + ("VC-CAN-TRUC", "Cẩu tháp / cẩu tự hành", "Vận chuyển", "h"), + ("BT-MAY", "Bảo trì máy móc thiết bị", "Bảo trì", "lan"), + ("BT-DIEN", "Bảo trì hệ thống điện", "Bảo trì", "lan"), + ("TV-THIET-KE", "Tư vấn thiết kế", "Tư vấn", "goi"), + ("TV-GIAM-SAT", "Giám sát thi công", "Tư vấn", "ngc"), + ("KIEM-DINH", "Kiểm định an toàn", "Kiểm định", "lan"), + ("VS-CT", "Vệ sinh công trường", "Vệ sinh", "ca"), + ("BV-CT", "Bảo vệ công trường", "Bảo vệ", "ngc"), + ("THUE-MAY", "Thuê máy móc thiết bị", "Thuê thiết bị", "ca"), + }; + foreach (var (code, name, cat, unit) in services) + db.ServiceItems.Add(new ServiceItem { Code = code, Name = name, Category = cat, DefaultUnit = unit }); + await db.SaveChangesAsync(); + logger.LogInformation("Seed {Count} ServiceItems", services.Length); + } + + // 4. WorkItems (~15 demo) + if (!await db.WorkItems.AnyAsync()) + { + var works = new[] + { + ("DAO-MONG", "Đào móng công trình", "Phần thô", "m3"), + ("DO-BTONG", "Đổ bê tông móng/cột/dầm", "Phần thô", "m3"), + ("LAP-COT-THEP", "Lắp dựng cốt thép", "Phần thô", "kg"), + ("LAP-COPPHA", "Lắp dựng cốp pha", "Phần thô", "m2"), + ("XAY-TUONG", "Xây tường gạch", "Phần thô", "m3"), + ("TRAT-TUONG", "Trát tường", "Hoàn thiện", "m2"), + ("LAT-GACH-NEN", "Lát gạch nền", "Hoàn thiện", "m2"), + ("OP-GACH-TUONG", "Ốp gạch tường", "Hoàn thiện", "m2"), + ("SON-NUOC", "Sơn nước", "Hoàn thiện", "m2"), + ("LAP-CUA", "Lắp cửa", "Hoàn thiện", "cai"), + ("LAP-DIEN", "Lắp đặt hệ thống điện", "Cơ điện", "goi"), + ("LAP-NUOC", "Lắp đặt hệ thống cấp thoát nước", "Cơ điện", "goi"), + ("LAP-DIEU-HOA", "Lắp điều hòa", "Cơ điện", "cai"), + ("THAM-CHONG", "Thấm chống thấm", "Hoàn thiện", "m2"), + ("VC-PHE-THAI", "Vận chuyển phế thải", "Khác", "m3"), + }; + foreach (var (code, name, cat, unit) in works) + db.WorkItems.Add(new WorkItem { Code = code, Name = name, Category = cat, DefaultUnit = unit }); + await db.SaveChangesAsync(); + logger.LogInformation("Seed {Count} WorkItems", works.Length); + } + } + private static async Task BackfillContractCodesAsync( ApplicationDbContext db, IContractCodeGenerator codeGen, ILogger logger) { @@ -236,6 +337,12 @@ public static class DbInitializer (MenuKeys.Suppliers, "Nhà cung cấp", MenuKeys.Master, 21, "Building2"), (MenuKeys.Projects, "Dự án", MenuKeys.Master, 22, "FolderKanban"), (MenuKeys.Departments, "Phòng ban", MenuKeys.Master, 23, "Users"), + // 4 master catalog cho Details add form (autocomplete suggestion) + (MenuKeys.Catalogs, "Danh mục chi tiết", MenuKeys.Master, 24, "Library"), + (MenuKeys.CatalogUnits, "Đơn vị tính", MenuKeys.Catalogs, 1, "Ruler"), + (MenuKeys.CatalogMaterials,"Vật tư / SP", MenuKeys.Catalogs, 2, "Package"), + (MenuKeys.CatalogServices, "Dịch vụ", MenuKeys.Catalogs, 3, "Wrench"), + (MenuKeys.CatalogWorkItems,"Hạng mục công việc", MenuKeys.Catalogs, 4, "ListChecks"), (MenuKeys.Contracts, "Hợp đồng", null, 30, "FileText"), (MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"), (MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"), diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.Designer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.Designer.cs new file mode 100644 index 0000000..6ba435d --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.Designer.cs @@ -0,0 +1,2164 @@ +// +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("20260423051650_AddMasterCatalogs")] + partial class AddMasterCatalogs + { + /// + 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.Contracts.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .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("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.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("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("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("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("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.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.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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.cs new file mode 100644 index 0000000..5ba5c65 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260423051650_AddMasterCatalogs.cs @@ -0,0 +1,168 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddMasterCatalogs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "MaterialItems", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Code = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Category = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + DefaultUnit = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Specification = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + OriginCountry = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + IsActive = 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_MaterialItems", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ServiceItems", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Code = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Category = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + DefaultUnit = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + IsActive = 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_ServiceItems", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UnitsOfMeasure", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Code = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, 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), + 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_UnitsOfMeasure", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "WorkItems", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Code = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Category = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + DefaultUnit = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + IsActive = 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_WorkItems", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_MaterialItems_Category", + table: "MaterialItems", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_MaterialItems_Code", + table: "MaterialItems", + column: "Code", + unique: true, + filter: "[IsDeleted] = 0"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceItems_Category", + table: "ServiceItems", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_ServiceItems_Code", + table: "ServiceItems", + column: "Code", + unique: true, + filter: "[IsDeleted] = 0"); + + migrationBuilder.CreateIndex( + name: "IX_UnitsOfMeasure_Code", + table: "UnitsOfMeasure", + column: "Code", + unique: true, + filter: "[IsDeleted] = 0"); + + migrationBuilder.CreateIndex( + name: "IX_WorkItems_Category", + table: "WorkItems", + column: "Category"); + + migrationBuilder.CreateIndex( + name: "IX_WorkItems_Code", + table: "WorkItems", + column: "Code", + unique: true, + filter: "[IsDeleted] = 0"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MaterialItems"); + + migrationBuilder.DropTable( + name: "ServiceItems"); + + migrationBuilder.DropTable( + name: "UnitsOfMeasure"); + + migrationBuilder.DropTable( + name: "WorkItems"); + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index d6ce7bf..9d3ff1d 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1408,6 +1408,249 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations 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")