[CLAUDE] Domain+App+Api: 4 master catalogs cho Details (migration 10)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s

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) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-23 12:21:39 +07:00
parent 51449d6b9d
commit e27c54702a
14 changed files with 3302 additions and 0 deletions

View File

@ -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<List<UnitOfMeasureDto>> ListUnits([FromQuery] string? q, CancellationToken ct)
=> await mediator.Send(new ListUnitsQuery(q), ct);
[HttpPost("units")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> DeleteUnit(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteUnitCommand(id), ct);
return NoContent();
}
// ===== Materials =====
[HttpGet("materials")]
public async Task<List<MaterialItemDto>> 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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteMaterial(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteMaterialCommand(id), ct);
return NoContent();
}
// ===== Services =====
[HttpGet("services")]
public async Task<List<ServiceItemDto>> 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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteService(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteServiceCommand(id), ct);
return NoContent();
}
// ===== Work Items =====
[HttpGet("work-items")]
public async Task<List<WorkItemDto>> 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<IActionResult> 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<IActionResult> 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<IActionResult> DeleteWorkItem(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteWorkItemCommand(id), ct);
return NoContent();
}
}

View File

@ -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<Supplier> Suppliers { get; }
DbSet<Project> Projects { get; }
DbSet<Department> Departments { get; }
// 4 master catalogs cho Details add form (autocomplete)
DbSet<UnitOfMeasure> UnitsOfMeasure { get; }
DbSet<MaterialItem> MaterialItems { get; }
DbSet<ServiceItem> ServiceItems { get; }
DbSet<WorkItem> WorkItems { get; }
DbSet<MenuItem> MenuItems { get; }
DbSet<Permission> Permissions { get; }
DbSet<ContractTemplate> ContractTemplates { get; }

View File

@ -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<List<UnitOfMeasureDto>>;
public class ListUnitsHandler(IApplicationDbContext db) : IRequestHandler<ListUnitsQuery, List<UnitOfMeasureDto>>
{
public async Task<List<UnitOfMeasureDto>> 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<Guid>;
public class CreateUnitValidator : AbstractValidator<CreateUnitCommand>
{
public CreateUnitValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
}
}
public class CreateUnitHandler(IApplicationDbContext db) : IRequestHandler<CreateUnitCommand, Guid>
{
public async Task<Guid> 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<UpdateUnitCommand>
{
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<DeleteUnitCommand>
{
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<List<MaterialItemDto>>;
public class ListMaterialsHandler(IApplicationDbContext db) : IRequestHandler<ListMaterialsQuery, List<MaterialItemDto>>
{
public async Task<List<MaterialItemDto>> 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<Guid>;
public class CreateMaterialValidator : AbstractValidator<CreateMaterialCommand>
{
public CreateMaterialValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
}
}
public class CreateMaterialHandler(IApplicationDbContext db) : IRequestHandler<CreateMaterialCommand, Guid>
{
public async Task<Guid> 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<UpdateMaterialCommand>
{
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<DeleteMaterialCommand>
{
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<List<ServiceItemDto>>;
public class ListServicesHandler(IApplicationDbContext db) : IRequestHandler<ListServicesQuery, List<ServiceItemDto>>
{
public async Task<List<ServiceItemDto>> 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<Guid>;
public class CreateServiceValidator : AbstractValidator<CreateServiceCommand>
{
public CreateServiceValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
}
}
public class CreateServiceHandler(IApplicationDbContext db) : IRequestHandler<CreateServiceCommand, Guid>
{
public async Task<Guid> 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<UpdateServiceCommand>
{
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<DeleteServiceCommand>
{
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<List<WorkItemDto>>;
public class ListWorkItemsHandler(IApplicationDbContext db) : IRequestHandler<ListWorkItemsQuery, List<WorkItemDto>>
{
public async Task<List<WorkItemDto>> 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<Guid>;
public class CreateWorkItemValidator : AbstractValidator<CreateWorkItemCommand>
{
public CreateWorkItemValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
}
}
public class CreateWorkItemHandler(IApplicationDbContext db) : IRequestHandler<CreateWorkItemCommand, Guid>
{
public async Task<Guid> 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<UpdateWorkItemCommand>
{
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<DeleteWorkItemCommand>
{
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);
}
}

View File

@ -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_<TypeCode>[_<Action>] — 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,
];

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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; }
}

View File

@ -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;
}

View File

@ -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<Supplier> Suppliers => Set<Supplier>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<Department> Departments => Set<Department>();
public DbSet<UnitOfMeasure> UnitsOfMeasure => Set<UnitOfMeasure>();
public DbSet<MaterialItem> MaterialItems => Set<MaterialItem>();
public DbSet<ServiceItem> ServiceItems => Set<ServiceItem>();
public DbSet<WorkItem> WorkItems => Set<WorkItem>();
public DbSet<MenuItem> MenuItems => Set<MenuItem>();
public DbSet<Permission> Permissions => Set<Permission>();
public DbSet<ContractTemplate> ContractTemplates => Set<ContractTemplate>();

View File

@ -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<UnitOfMeasure>
{
public void Configure(EntityTypeBuilder<UnitOfMeasure> 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<MaterialItem>
{
public void Configure(EntityTypeBuilder<MaterialItem> 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<ServiceItem>
{
public void Configure(EntityTypeBuilder<ServiceItem> 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<WorkItem>
{
public void Configure(EntityTypeBuilder<WorkItem> 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);
}
}

View File

@ -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"),

View File

@ -0,0 +1,168 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddMasterCatalogs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MaterialItems",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
DefaultUnit = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Specification = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
OriginCountry = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MaterialItems", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ServiceItems",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
DefaultUnit = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ServiceItems", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UnitsOfMeasure",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UnitsOfMeasure", x => x.Id);
});
migrationBuilder.CreateTable(
name: "WorkItems",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Category = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
DefaultUnit = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MaterialItems");
migrationBuilder.DropTable(
name: "ServiceItems");
migrationBuilder.DropTable(
name: "UnitsOfMeasure");
migrationBuilder.DropTable(
name: "WorkItems");
}
}
}

View File

@ -1408,6 +1408,249 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.MaterialItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Category")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("DefaultUnit")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("OriginCountry")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Specification")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Category")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("DefaultUnit")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Category")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("DefaultUnit")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("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<Guid>("Id")