[CLAUDE] Phase1.2: CRUD Master + Permission Matrix + FE admin pages

Backend:
- Domain/Master: Supplier (+ SupplierType 5 loai), Project, Department (AuditableEntity)
- Domain/Identity: MenuItem, Permission, MenuKeys const (12 menu)
- EF Configurations voi unique Code + query filter IsDeleted
- DbSets + IApplicationDbContext interface update
- Application: PagedResult + PagedRequest generic
- Application/Master CQRS CRUD 3 entity (Create/Update/Delete/Get/List voi paging search sort)
- Application/Permissions: GetMyMenuTree (union OR role, filter tree), ListMenuItems, ListPermissionsByRole, UpsertPermission (guard admin khong tu giam quyen), ListRoles
- Api/Authorization: MenuPermissionRequirement + Handler (Admin bypass, query DB)
- Program.cs: register 48 policy {menu}.{action} tu MenuKeys x Actions
- Api/Controllers: Suppliers, Projects, Departments, Menus, Roles, Permissions
- DbInitializer: seed 12 menu + admin full CRUD permissions
- Migration AddMasterData + AddPermissions

Frontend (fe-admin):
- Types: menuKeys.ts const, menu.ts (MenuNode/Role/Permission), master.ts (Supplier/Project/Department + SupplierType const-object)
- AuthContext: load menu from /menus/me, cache localStorage, refreshMenu()
- usePermission hook + PermissionGuard component (wrap button)
- UI kit them: Dialog (modal overlay), Textarea, Select
- Generic: DataTable (column config, sortable, loading, empty) + Pagination
- PageHeader component
- apiError helper extract message tu ProblemDetails
- Layout rewrite: render menu dong tu AuthContext.menu (MenuGroup collapsible + NavLink + lucide icon map)
- Pages: master/Suppliers, master/Projects, master/Departments (CRUD + search + sort + paging + Dialog form)
- Page system/Permissions: ma tran Role x MenuKey x CRUD checkbox (tick tu dong PUT upsert)
- App.tsx them 4 route moi

Bug fix:
- MenuPermissionHandler: EF expression tree khong support switch expression -> tach switch ra ngoai AnyAsync
- TS erasableSyntaxOnly khong cho enum -> SupplierType const-object pattern (typeof[keyof])

E2E verified via Vite proxy:
- GET /menus/me -> 6 root + 6 child nodes (12 menus)
- GET /roles -> 12 roles
- POST/GET/PUT/DELETE /suppliers -> full CRUD, soft delete OK
- tsc -b fe-admin pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 11:30:14 +07:00
parent 49a5f57a50
commit 54d6c9ba52
63 changed files with 4422 additions and 93 deletions

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Identity;
namespace SolutionErp.Api.Authorization;
public class MenuPermissionHandler(
IApplicationDbContext db,
UserManager<User> userManager,
RoleManager<Role> roleManager) : AuthorizationHandler<MenuPermissionRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context, MenuPermissionRequirement req)
{
var sub = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
?? context.User.FindFirst("sub")?.Value;
if (!Guid.TryParse(sub, out var userId)) return;
var user = await userManager.FindByIdAsync(userId.ToString());
if (user is null || !user.IsActive) return;
var roleNames = await userManager.GetRolesAsync(user);
// Admin bypass
if (roleNames.Contains(AppRoles.Admin))
{
context.Succeed(req);
return;
}
var roleIds = new List<Guid>();
foreach (var name in roleNames)
{
var r = await roleManager.FindByNameAsync(name);
if (r is not null) roleIds.Add(r.Id);
}
var baseQuery = db.Permissions
.Where(p => roleIds.Contains(p.RoleId) && p.MenuKey == req.MenuKey);
var hasPermission = req.Action switch
{
"Read" => await baseQuery.AnyAsync(p => p.CanRead),
"Create" => await baseQuery.AnyAsync(p => p.CanCreate),
"Update" => await baseQuery.AnyAsync(p => p.CanUpdate),
"Delete" => await baseQuery.AnyAsync(p => p.CanDelete),
_ => false,
};
if (hasPermission) context.Succeed(req);
}
}

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authorization;
namespace SolutionErp.Api.Authorization;
public class MenuPermissionRequirement(string menuKey, string action) : IAuthorizationRequirement
{
public string MenuKey { get; } = menuKey;
public string Action { get; } = action; // "Read" | "Create" | "Update" | "Delete"
}

View File

@ -0,0 +1,46 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Master.Departments;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/departments")]
[Authorize]
public class DepartmentsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResult<DepartmentDto>>> List(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] string? sortBy = null, [FromQuery] bool sortDesc = true,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListDepartmentsQuery { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct));
[HttpGet("{id:guid}")]
public async Task<ActionResult<DepartmentDto>> Get(Guid id, CancellationToken ct)
=> Ok(await mediator.Send(new GetDepartmentQuery(id), ct));
[HttpPost]
public async Task<ActionResult<Guid>> Create([FromBody] CreateDepartmentCommand cmd, CancellationToken ct)
{
var id = await mediator.Send(cmd, ct);
return CreatedAtAction(nameof(Get), new { id }, new { id });
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateDepartmentCommand cmd, CancellationToken ct)
{
if (id != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteDepartmentCommand(id), ct);
return NoContent();
}
}

View File

@ -0,0 +1,22 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Permissions;
using SolutionErp.Application.Permissions.Dtos;
using SolutionErp.Application.Permissions.Queries.GetMyMenuTree;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/menus")]
[Authorize]
public class MenusController(IMediator mediator) : ControllerBase
{
[HttpGet("me")]
public async Task<ActionResult<List<MenuNodeDto>>> Me(CancellationToken ct)
=> Ok(await mediator.Send(new GetMyMenuTreeQuery(), ct));
[HttpGet]
public async Task<ActionResult<List<MenuItemDto>>> List(CancellationToken ct)
=> Ok(await mediator.Send(new ListMenuItemsQuery(), ct));
}

View File

@ -0,0 +1,25 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Permissions;
using SolutionErp.Application.Permissions.Dtos;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/permissions")]
[Authorize(Policy = "Permissions.Read")]
public class PermissionsController(IMediator mediator) : ControllerBase
{
[HttpGet("by-role/{roleId:guid}")]
public async Task<ActionResult<List<PermissionDto>>> ListByRole(Guid roleId, CancellationToken ct)
=> Ok(await mediator.Send(new ListPermissionsByRoleQuery(roleId), ct));
[HttpPut]
[Authorize(Policy = "Permissions.Update")]
public async Task<IActionResult> Upsert([FromBody] UpsertPermissionCommand cmd, CancellationToken ct)
{
await mediator.Send(cmd, ct);
return NoContent();
}
}

View File

@ -0,0 +1,46 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Master.Projects;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/projects")]
[Authorize]
public class ProjectsController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResult<ProjectDto>>> List(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] string? sortBy = null, [FromQuery] bool sortDesc = true,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListProjectsQuery { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct));
[HttpGet("{id:guid}")]
public async Task<ActionResult<ProjectDto>> Get(Guid id, CancellationToken ct)
=> Ok(await mediator.Send(new GetProjectQuery(id), ct));
[HttpPost]
public async Task<ActionResult<Guid>> Create([FromBody] CreateProjectCommand cmd, CancellationToken ct)
{
var id = await mediator.Send(cmd, ct);
return CreatedAtAction(nameof(Get), new { id }, new { id });
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateProjectCommand cmd, CancellationToken ct)
{
if (id != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteProjectCommand(id), ct);
return NoContent();
}
}

View File

@ -0,0 +1,17 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Permissions;
using SolutionErp.Application.Permissions.Dtos;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/roles")]
[Authorize]
public class RolesController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<List<RoleDto>>> List(CancellationToken ct)
=> Ok(await mediator.Send(new ListRolesQuery(), ct));
}

View File

@ -0,0 +1,53 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Master.Suppliers.Commands.CreateSupplier;
using SolutionErp.Application.Master.Suppliers.Commands.DeleteSupplier;
using SolutionErp.Application.Master.Suppliers.Commands.UpdateSupplier;
using SolutionErp.Application.Master.Suppliers.Dtos;
using SolutionErp.Application.Master.Suppliers.Queries.GetSupplier;
using SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers;
using SolutionErp.Domain.Master;
namespace SolutionErp.Api.Controllers;
[ApiController]
[Route("api/suppliers")]
[Authorize]
public class SuppliersController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResult<SupplierDto>>> List(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] string? sortBy = null, [FromQuery] bool sortDesc = true,
[FromQuery] SupplierType? type = null,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListSuppliersQuery(type) { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct));
[HttpGet("{id:guid}")]
public async Task<ActionResult<SupplierDto>> Get(Guid id, CancellationToken ct)
=> Ok(await mediator.Send(new GetSupplierQuery(id), ct));
[HttpPost]
public async Task<ActionResult<Guid>> Create([FromBody] CreateSupplierCommand cmd, CancellationToken ct)
{
var id = await mediator.Send(cmd, ct);
return CreatedAtAction(nameof(Get), new { id }, new { id });
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateSupplierCommand cmd, CancellationToken ct)
{
if (id != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteSupplierCommand(id), ct);
return NoContent();
}
}

View File

@ -1,12 +1,15 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Serilog;
using SolutionErp.Api.Authorization;
using SolutionErp.Api.Middleware;
using SolutionErp.Api.Services;
using SolutionErp.Application;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure;
using SolutionErp.Infrastructure.Identity;
using SolutionErp.Infrastructure.Persistence;
@ -48,7 +51,19 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
ClockSkew = TimeSpan.FromMinutes(1),
};
});
builder.Services.AddAuthorization();
builder.Services.AddScoped<IAuthorizationHandler, MenuPermissionHandler>();
builder.Services.AddAuthorization(opts =>
{
foreach (var menu in MenuKeys.All)
{
foreach (var action in MenuKeys.Actions)
{
opts.AddPolicy($"{menu}.{action}", p =>
p.Requirements.Add(new MenuPermissionRequirement(menu, action)));
}
}
});
// ---------- CORS (2 FE dev origins) ----------
builder.Services.AddCors(opts =>

View File

@ -1,6 +1,16 @@
using Microsoft.EntityFrameworkCore;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Common.Interfaces;
public interface IApplicationDbContext
{
DbSet<Supplier> Suppliers { get; }
DbSet<Project> Projects { get; }
DbSet<Department> Departments { get; }
DbSet<MenuItem> MenuItems { get; }
DbSet<Permission> Permissions { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,24 @@
namespace SolutionErp.Application.Common.Models;
public record PagedResult<T>(
IReadOnlyList<T> Items,
int Total,
int Page,
int PageSize)
{
public int TotalPages => (int)Math.Ceiling(Total / (double)PageSize);
public bool HasNext => Page * PageSize < Total;
public bool HasPrev => Page > 1;
}
public abstract record PagedRequest
{
private int _page = 1;
private int _pageSize = 20;
public int Page { get => _page; init => _page = value < 1 ? 1 : value; }
public int PageSize { get => _pageSize; init => _pageSize = value switch { < 1 => 20, > 200 => 200, _ => value }; }
public string? Search { get; init; }
public string? SortBy { get; init; }
public bool SortDesc { get; init; }
}

View File

@ -0,0 +1,128 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Master.Departments;
public record DepartmentDto(
Guid Id,
string Code,
string Name,
Guid? ManagerUserId,
string? Note,
DateTime CreatedAt,
DateTime? UpdatedAt);
public record ListDepartmentsQuery : PagedRequest, IRequest<PagedResult<DepartmentDto>>;
public class ListDepartmentsQueryHandler(IApplicationDbContext db) : IRequestHandler<ListDepartmentsQuery, PagedResult<DepartmentDto>>
{
public async Task<PagedResult<DepartmentDto>> Handle(ListDepartmentsQuery request, CancellationToken ct)
{
var query = db.Departments.AsNoTracking();
if (!string.IsNullOrWhiteSpace(request.Search))
{
var s = request.Search.Trim();
query = query.Where(x => x.Code.Contains(s) || x.Name.Contains(s));
}
query = (request.SortBy, request.SortDesc) switch
{
("code", true) => query.OrderByDescending(x => x.Code),
("code", false) => query.OrderBy(x => x.Code),
("name", true) => query.OrderByDescending(x => x.Name),
("name", false) => query.OrderBy(x => x.Name),
_ => query.OrderByDescending(x => x.CreatedAt),
};
var total = await query.CountAsync(ct);
var items = await query
.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
.Select(x => new DepartmentDto(x.Id, x.Code, x.Name, x.ManagerUserId, x.Note, x.CreatedAt, x.UpdatedAt))
.ToListAsync(ct);
return new PagedResult<DepartmentDto>(items, total, request.Page, request.PageSize);
}
}
public record GetDepartmentQuery(Guid Id) : IRequest<DepartmentDto>;
public class GetDepartmentQueryHandler(IApplicationDbContext db) : IRequestHandler<GetDepartmentQuery, DepartmentDto>
{
public async Task<DepartmentDto> Handle(GetDepartmentQuery request, CancellationToken ct)
{
var x = await db.Departments.AsNoTracking().FirstOrDefaultAsync(p => p.Id == request.Id, ct)
?? throw new NotFoundException("Department", request.Id);
return new DepartmentDto(x.Id, x.Code, x.Name, x.ManagerUserId, x.Note, x.CreatedAt, x.UpdatedAt);
}
}
public record CreateDepartmentCommand(string Code, string Name, Guid? ManagerUserId, string? Note) : IRequest<Guid>;
public class CreateDepartmentCommandValidator : AbstractValidator<CreateDepartmentCommand>
{
public CreateDepartmentCommandValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
}
}
public class CreateDepartmentCommandHandler(IApplicationDbContext db) : IRequestHandler<CreateDepartmentCommand, Guid>
{
public async Task<Guid> Handle(CreateDepartmentCommand request, CancellationToken ct)
{
if (await db.Departments.AnyAsync(x => x.Code == request.Code, ct))
throw new ConflictException($"Mã phòng ban '{request.Code}' đã tồn tại.");
var entity = new Department
{
Code = request.Code, Name = request.Name,
ManagerUserId = request.ManagerUserId, Note = request.Note,
};
db.Departments.Add(entity);
await db.SaveChangesAsync(ct);
return entity.Id;
}
}
public record UpdateDepartmentCommand(Guid Id, string Code, string Name, Guid? ManagerUserId, string? Note) : IRequest;
public class UpdateDepartmentCommandValidator : AbstractValidator<UpdateDepartmentCommand>
{
public UpdateDepartmentCommandValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
}
}
public class UpdateDepartmentCommandHandler(IApplicationDbContext db) : IRequestHandler<UpdateDepartmentCommand>
{
public async Task Handle(UpdateDepartmentCommand request, CancellationToken ct)
{
var entity = await db.Departments.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("Department", request.Id);
if (entity.Code != request.Code && await db.Departments.AnyAsync(x => x.Code == request.Code && x.Id != request.Id, ct))
throw new ConflictException($"Mã phòng ban '{request.Code}' đã tồn tại.");
entity.Code = request.Code;
entity.Name = request.Name;
entity.ManagerUserId = request.ManagerUserId;
entity.Note = request.Note;
await db.SaveChangesAsync(ct);
}
}
public record DeleteDepartmentCommand(Guid Id) : IRequest;
public class DeleteDepartmentCommandHandler(IApplicationDbContext db) : IRequestHandler<DeleteDepartmentCommand>
{
public async Task Handle(DeleteDepartmentCommand request, CancellationToken ct)
{
var entity = await db.Departments.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("Department", request.Id);
db.Departments.Remove(entity);
await db.SaveChangesAsync(ct);
}
}

View File

@ -0,0 +1,150 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Master.Projects;
public record ProjectDto(
Guid Id,
string Code,
string Name,
DateTime? StartDate,
DateTime? EndDate,
Guid? ManagerUserId,
decimal? BudgetTotal,
string? Note,
DateTime CreatedAt,
DateTime? UpdatedAt);
// ===================== LIST =====================
public record ListProjectsQuery : PagedRequest, IRequest<PagedResult<ProjectDto>>;
public class ListProjectsQueryHandler(IApplicationDbContext db) : IRequestHandler<ListProjectsQuery, PagedResult<ProjectDto>>
{
public async Task<PagedResult<ProjectDto>> Handle(ListProjectsQuery request, CancellationToken ct)
{
var query = db.Projects.AsNoTracking();
if (!string.IsNullOrWhiteSpace(request.Search))
{
var s = request.Search.Trim();
query = query.Where(x => x.Code.Contains(s) || x.Name.Contains(s));
}
query = (request.SortBy, request.SortDesc) switch
{
("code", true) => query.OrderByDescending(x => x.Code),
("code", false) => query.OrderBy(x => x.Code),
("name", true) => query.OrderByDescending(x => x.Name),
("name", false) => query.OrderBy(x => x.Name),
_ => query.OrderByDescending(x => x.CreatedAt),
};
var total = await query.CountAsync(ct);
var items = await query
.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
.Select(x => new ProjectDto(x.Id, x.Code, x.Name, x.StartDate, x.EndDate, x.ManagerUserId, x.BudgetTotal, x.Note, x.CreatedAt, x.UpdatedAt))
.ToListAsync(ct);
return new PagedResult<ProjectDto>(items, total, request.Page, request.PageSize);
}
}
// ===================== GET =====================
public record GetProjectQuery(Guid Id) : IRequest<ProjectDto>;
public class GetProjectQueryHandler(IApplicationDbContext db) : IRequestHandler<GetProjectQuery, ProjectDto>
{
public async Task<ProjectDto> Handle(GetProjectQuery request, CancellationToken ct)
{
var x = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == request.Id, ct)
?? throw new NotFoundException("Project", request.Id);
return new ProjectDto(x.Id, x.Code, x.Name, x.StartDate, x.EndDate, x.ManagerUserId, x.BudgetTotal, x.Note, x.CreatedAt, x.UpdatedAt);
}
}
// ===================== CREATE =====================
public record CreateProjectCommand(
string Code, string Name, DateTime? StartDate, DateTime? EndDate,
Guid? ManagerUserId, decimal? BudgetTotal, string? Note) : IRequest<Guid>;
public class CreateProjectCommandValidator : AbstractValidator<CreateProjectCommand>
{
public CreateProjectCommandValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.BudgetTotal).GreaterThanOrEqualTo(0).When(x => x.BudgetTotal.HasValue);
RuleFor(x => x).Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.EndDate >= x.StartDate)
.WithMessage("Ngày kết thúc phải sau ngày bắt đầu");
}
}
public class CreateProjectCommandHandler(IApplicationDbContext db) : IRequestHandler<CreateProjectCommand, Guid>
{
public async Task<Guid> Handle(CreateProjectCommand request, CancellationToken ct)
{
if (await db.Projects.AnyAsync(x => x.Code == request.Code, ct))
throw new ConflictException($"Mã dự án '{request.Code}' đã tồn tại.");
var entity = new Project
{
Code = request.Code, Name = request.Name,
StartDate = request.StartDate, EndDate = request.EndDate,
ManagerUserId = request.ManagerUserId, BudgetTotal = request.BudgetTotal, Note = request.Note,
};
db.Projects.Add(entity);
await db.SaveChangesAsync(ct);
return entity.Id;
}
}
// ===================== UPDATE =====================
public record UpdateProjectCommand(
Guid Id, string Code, string Name, DateTime? StartDate, DateTime? EndDate,
Guid? ManagerUserId, decimal? BudgetTotal, string? Note) : IRequest;
public class UpdateProjectCommandValidator : AbstractValidator<UpdateProjectCommand>
{
public UpdateProjectCommandValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.BudgetTotal).GreaterThanOrEqualTo(0).When(x => x.BudgetTotal.HasValue);
RuleFor(x => x).Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || x.EndDate >= x.StartDate)
.WithMessage("Ngày kết thúc phải sau ngày bắt đầu");
}
}
public class UpdateProjectCommandHandler(IApplicationDbContext db) : IRequestHandler<UpdateProjectCommand>
{
public async Task Handle(UpdateProjectCommand request, CancellationToken ct)
{
var entity = await db.Projects.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("Project", request.Id);
if (entity.Code != request.Code && await db.Projects.AnyAsync(x => x.Code == request.Code && x.Id != request.Id, ct))
throw new ConflictException($"Mã dự án '{request.Code}' đã tồn tại.");
entity.Code = request.Code;
entity.Name = request.Name;
entity.StartDate = request.StartDate;
entity.EndDate = request.EndDate;
entity.ManagerUserId = request.ManagerUserId;
entity.BudgetTotal = request.BudgetTotal;
entity.Note = request.Note;
await db.SaveChangesAsync(ct);
}
}
// ===================== DELETE =====================
public record DeleteProjectCommand(Guid Id) : IRequest;
public class DeleteProjectCommandHandler(IApplicationDbContext db) : IRequestHandler<DeleteProjectCommand>
{
public async Task Handle(DeleteProjectCommand request, CancellationToken ct)
{
var entity = await db.Projects.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("Project", request.Id);
db.Projects.Remove(entity);
await db.SaveChangesAsync(ct);
}
}

View File

@ -0,0 +1,64 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Master.Suppliers.Commands.CreateSupplier;
public record CreateSupplierCommand(
string Code,
string Name,
SupplierType Type,
string? TaxCode,
string? Phone,
string? Email,
string? Address,
string? ContactPerson,
string? Note) : IRequest<Guid>;
public class CreateSupplierCommandValidator : AbstractValidator<CreateSupplierCommand>
{
public CreateSupplierCommandValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Type).IsInEnum();
RuleFor(x => x.TaxCode).MaximumLength(20);
RuleFor(x => x.Phone).MaximumLength(30);
RuleFor(x => x.Email).MaximumLength(100).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email));
RuleFor(x => x.Address).MaximumLength(500);
RuleFor(x => x.ContactPerson).MaximumLength(200);
RuleFor(x => x.Note).MaximumLength(1000);
}
}
public class CreateSupplierCommandHandler : IRequestHandler<CreateSupplierCommand, Guid>
{
private readonly IApplicationDbContext _db;
public CreateSupplierCommandHandler(IApplicationDbContext db) => _db = db;
public async Task<Guid> Handle(CreateSupplierCommand request, CancellationToken ct)
{
if (await _db.Suppliers.AnyAsync(x => x.Code == request.Code, ct))
throw new ConflictException($"Mã NCC '{request.Code}' đã tồn tại.");
var entity = new Supplier
{
Code = request.Code,
Name = request.Name,
Type = request.Type,
TaxCode = request.TaxCode,
Phone = request.Phone,
Email = request.Email,
Address = request.Address,
ContactPerson = request.ContactPerson,
Note = request.Note,
};
_db.Suppliers.Add(entity);
await _db.SaveChangesAsync(ct);
return entity.Id;
}
}

View File

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
namespace SolutionErp.Application.Master.Suppliers.Commands.DeleteSupplier;
public record DeleteSupplierCommand(Guid Id) : IRequest;
public class DeleteSupplierCommandHandler : IRequestHandler<DeleteSupplierCommand>
{
private readonly IApplicationDbContext _db;
public DeleteSupplierCommandHandler(IApplicationDbContext db) => _db = db;
public async Task Handle(DeleteSupplierCommand request, CancellationToken ct)
{
var entity = await _db.Suppliers.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("Supplier", request.Id);
_db.Suppliers.Remove(entity); // AuditingInterceptor convert sang soft delete
await _db.SaveChangesAsync(ct);
}
}

View File

@ -0,0 +1,61 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Master.Suppliers.Commands.UpdateSupplier;
public record UpdateSupplierCommand(
Guid Id,
string Code,
string Name,
SupplierType Type,
string? TaxCode,
string? Phone,
string? Email,
string? Address,
string? ContactPerson,
string? Note) : IRequest;
public class UpdateSupplierCommandValidator : AbstractValidator<UpdateSupplierCommand>
{
public UpdateSupplierCommandValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Type).IsInEnum();
RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email));
}
}
public class UpdateSupplierCommandHandler : IRequestHandler<UpdateSupplierCommand>
{
private readonly IApplicationDbContext _db;
public UpdateSupplierCommandHandler(IApplicationDbContext db) => _db = db;
public async Task Handle(UpdateSupplierCommand request, CancellationToken ct)
{
var entity = await _db.Suppliers.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("Supplier", request.Id);
if (entity.Code != request.Code &&
await _db.Suppliers.AnyAsync(x => x.Code == request.Code && x.Id != request.Id, ct))
throw new ConflictException($"Mã NCC '{request.Code}' đã tồn tại.");
entity.Code = request.Code;
entity.Name = request.Name;
entity.Type = request.Type;
entity.TaxCode = request.TaxCode;
entity.Phone = request.Phone;
entity.Email = request.Email;
entity.Address = request.Address;
entity.ContactPerson = request.ContactPerson;
entity.Note = request.Note;
await _db.SaveChangesAsync(ct);
}
}

View File

@ -0,0 +1,17 @@
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Master.Suppliers.Dtos;
public record SupplierDto(
Guid Id,
string Code,
string Name,
SupplierType Type,
string? TaxCode,
string? Phone,
string? Email,
string? Address,
string? ContactPerson,
string? Note,
DateTime CreatedAt,
DateTime? UpdatedAt);

View File

@ -0,0 +1,23 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Master.Suppliers.Dtos;
namespace SolutionErp.Application.Master.Suppliers.Queries.GetSupplier;
public record GetSupplierQuery(Guid Id) : IRequest<SupplierDto>;
public class GetSupplierQueryHandler : IRequestHandler<GetSupplierQuery, SupplierDto>
{
private readonly IApplicationDbContext _db;
public GetSupplierQueryHandler(IApplicationDbContext db) => _db = db;
public async Task<SupplierDto> Handle(GetSupplierQuery request, CancellationToken ct)
{
var x = await _db.Suppliers.AsNoTracking().FirstOrDefaultAsync(s => s.Id == request.Id, ct)
?? throw new NotFoundException("Supplier", request.Id);
return new SupplierDto(x.Id, x.Code, x.Name, x.Type, x.TaxCode, x.Phone, x.Email, x.Address, x.ContactPerson, x.Note, x.CreatedAt, x.UpdatedAt);
}
}

View File

@ -0,0 +1,55 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Master.Suppliers.Dtos;
using SolutionErp.Domain.Master;
namespace SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers;
public record ListSuppliersQuery(SupplierType? Type = null) : PagedRequest, IRequest<PagedResult<SupplierDto>>;
public class ListSuppliersQueryHandler : IRequestHandler<ListSuppliersQuery, PagedResult<SupplierDto>>
{
private readonly IApplicationDbContext _db;
public ListSuppliersQueryHandler(IApplicationDbContext db) => _db = db;
public async Task<PagedResult<SupplierDto>> Handle(ListSuppliersQuery request, CancellationToken ct)
{
var query = _db.Suppliers.AsNoTracking();
if (request.Type is not null)
query = query.Where(x => x.Type == request.Type);
if (!string.IsNullOrWhiteSpace(request.Search))
{
var s = request.Search.Trim();
query = query.Where(x =>
x.Code.Contains(s) || x.Name.Contains(s) ||
(x.TaxCode != null && x.TaxCode.Contains(s)));
}
query = (request.SortBy, request.SortDesc) switch
{
("code", true) => query.OrderByDescending(x => x.Code),
("code", false) => query.OrderBy(x => x.Code),
("name", true) => query.OrderByDescending(x => x.Name),
("name", false) => query.OrderBy(x => x.Name),
("type", true) => query.OrderByDescending(x => x.Type),
("type", false) => query.OrderBy(x => x.Type),
_ => query.OrderByDescending(x => x.CreatedAt),
};
var total = await query.CountAsync(ct);
var items = await query
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(x => new SupplierDto(
x.Id, x.Code, x.Name, x.Type, x.TaxCode, x.Phone, x.Email, x.Address, x.ContactPerson, x.Note,
x.CreatedAt, x.UpdatedAt))
.ToListAsync(ct);
return new PagedResult<SupplierDto>(items, total, request.Page, request.PageSize);
}
}

View File

@ -0,0 +1,35 @@
namespace SolutionErp.Application.Permissions.Dtos;
public record MenuNodeDto(
string Key,
string Label,
string? ParentKey,
int Order,
string? Icon,
bool CanRead,
bool CanCreate,
bool CanUpdate,
bool CanDelete,
List<MenuNodeDto> Children);
public record PermissionDto(
Guid Id,
Guid RoleId,
string MenuKey,
bool CanRead,
bool CanCreate,
bool CanUpdate,
bool CanDelete);
public record RoleDto(
Guid Id,
string Name,
string? Description,
DateTime CreatedAt);
public record MenuItemDto(
string Key,
string Label,
string? ParentKey,
int Order,
string? Icon);

View File

@ -0,0 +1,121 @@
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Permissions.Dtos;
using SolutionErp.Domain.Identity;
namespace SolutionErp.Application.Permissions;
// ========== List menu items (for matrix page) ==========
public record ListMenuItemsQuery : IRequest<List<MenuItemDto>>;
public class ListMenuItemsQueryHandler(IApplicationDbContext db) : IRequestHandler<ListMenuItemsQuery, List<MenuItemDto>>
{
public async Task<List<MenuItemDto>> Handle(ListMenuItemsQuery request, CancellationToken ct)
{
return await db.MenuItems.AsNoTracking()
.OrderBy(m => m.Order)
.Select(m => new MenuItemDto(m.Key, m.Label, m.ParentKey, m.Order, m.Icon))
.ToListAsync(ct);
}
}
// ========== List permissions by role (matrix edit) ==========
public record ListPermissionsByRoleQuery(Guid RoleId) : IRequest<List<PermissionDto>>;
public class ListPermissionsByRoleQueryHandler(IApplicationDbContext db) : IRequestHandler<ListPermissionsByRoleQuery, List<PermissionDto>>
{
public async Task<List<PermissionDto>> Handle(ListPermissionsByRoleQuery request, CancellationToken ct)
{
return await db.Permissions.AsNoTracking()
.Where(p => p.RoleId == request.RoleId)
.Select(p => new PermissionDto(p.Id, p.RoleId, p.MenuKey, p.CanRead, p.CanCreate, p.CanUpdate, p.CanDelete))
.ToListAsync(ct);
}
}
// ========== Upsert permission (admin update matrix) ==========
public record UpsertPermissionCommand(
Guid RoleId,
string MenuKey,
bool CanRead,
bool CanCreate,
bool CanUpdate,
bool CanDelete) : IRequest;
public class UpsertPermissionCommandValidator : AbstractValidator<UpsertPermissionCommand>
{
public UpsertPermissionCommandValidator()
{
RuleFor(x => x.RoleId).NotEmpty();
RuleFor(x => x.MenuKey).NotEmpty().MaximumLength(50);
}
}
public class UpsertPermissionCommandHandler(
IApplicationDbContext db,
RoleManager<Role> roleManager,
ICurrentUser currentUser) : IRequestHandler<UpsertPermissionCommand>
{
public async Task Handle(UpsertPermissionCommand request, CancellationToken ct)
{
var role = await roleManager.FindByIdAsync(request.RoleId.ToString())
?? throw new NotFoundException("Role", request.RoleId);
// Guard: không cho tự xóa quyền của role Admin (nếu người đang edit là Admin)
if (role.Name == AppRoles.Admin && currentUser.Roles.Contains(AppRoles.Admin))
{
if (!(request.CanRead && request.CanCreate && request.CanUpdate && request.CanDelete))
throw new ForbiddenException("Không thể giảm quyền của role Admin khi bạn đang là Admin.");
}
var menu = await db.MenuItems.FirstOrDefaultAsync(m => m.Key == request.MenuKey, ct)
?? throw new NotFoundException("MenuItem", request.MenuKey);
var existing = await db.Permissions.FirstOrDefaultAsync(
p => p.RoleId == request.RoleId && p.MenuKey == request.MenuKey, ct);
if (existing is null)
{
db.Permissions.Add(new Permission
{
RoleId = request.RoleId,
MenuKey = request.MenuKey,
CanRead = request.CanRead,
CanCreate = request.CanCreate,
CanUpdate = request.CanUpdate,
CanDelete = request.CanDelete,
});
}
else
{
existing.CanRead = request.CanRead;
existing.CanCreate = request.CanCreate;
existing.CanUpdate = request.CanUpdate;
existing.CanDelete = request.CanDelete;
}
await db.SaveChangesAsync(ct);
}
}
// ========== List all roles ==========
public record ListRolesQuery : IRequest<List<RoleDto>>;
public class ListRolesQueryHandler(RoleManager<Role> roleManager) : IRequestHandler<ListRolesQuery, List<RoleDto>>
{
public async Task<List<RoleDto>> Handle(ListRolesQuery request, CancellationToken ct)
{
return await roleManager.Roles
.OrderBy(r => r.Name)
.Select(r => new RoleDto(r.Id, r.Name!, r.Description, r.CreatedAt))
.ToListAsync(ct);
}
}

View File

@ -0,0 +1,72 @@
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Permissions.Dtos;
using SolutionErp.Domain.Identity;
namespace SolutionErp.Application.Permissions.Queries.GetMyMenuTree;
public record GetMyMenuTreeQuery : IRequest<List<MenuNodeDto>>;
public class GetMyMenuTreeQueryHandler(
IApplicationDbContext db,
ICurrentUser currentUser,
UserManager<User> userManager,
RoleManager<Role> roleManager) : IRequestHandler<GetMyMenuTreeQuery, List<MenuNodeDto>>
{
public async Task<List<MenuNodeDto>> Handle(GetMyMenuTreeQuery request, CancellationToken ct)
{
if (!currentUser.IsAuthenticated || currentUser.UserId is null)
throw new UnauthorizedException();
var user = await userManager.FindByIdAsync(currentUser.UserId.Value.ToString())
?? throw new UnauthorizedException();
var roleNames = await userManager.GetRolesAsync(user);
var roleIds = new List<Guid>();
foreach (var name in roleNames)
{
var role = await roleManager.FindByNameAsync(name);
if (role is not null) roleIds.Add(role.Id);
}
var menus = await db.MenuItems.AsNoTracking().OrderBy(m => m.Order).ToListAsync(ct);
var perms = await db.Permissions.AsNoTracking()
.Where(p => roleIds.Contains(p.RoleId))
.ToListAsync(ct);
// Union CRUD flags qua các role
var resolved = perms
.GroupBy(p => p.MenuKey)
.ToDictionary(g => g.Key, g => (
Read: g.Any(p => p.CanRead),
Create: g.Any(p => p.CanCreate),
Update: g.Any(p => p.CanUpdate),
Delete: g.Any(p => p.CanDelete)));
// Build tree
List<MenuNodeDto> BuildChildren(string? parentKey) => menus
.Where(m => m.ParentKey == parentKey)
.Select(m =>
{
var flags = resolved.TryGetValue(m.Key, out var f) ? f : (false, false, false, false);
return new MenuNodeDto(m.Key, m.Label, m.ParentKey, m.Order, m.Icon,
flags.Item1, flags.Item2, flags.Item3, flags.Item4,
BuildChildren(m.Key));
})
.ToList();
var tree = BuildChildren(null);
// Filter: chỉ trả về node có CanRead=true (hoặc có child CanRead=true)
static bool HasAccess(MenuNodeDto n) => n.CanRead || n.Children.Any(HasAccess);
List<MenuNodeDto> Filter(List<MenuNodeDto> nodes) => nodes
.Where(HasAccess)
.Select(n => n with { Children = Filter(n.Children) })
.ToList();
return Filter(tree);
}
}

View File

@ -8,6 +8,7 @@
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
</ItemGroup>

View File

@ -0,0 +1,14 @@
namespace SolutionErp.Domain.Identity;
public class MenuItem
{
public string Key { get; set; } = string.Empty; // PK, PascalCase
public string Label { get; set; } = string.Empty; // Tiếng Việt display
public string? ParentKey { get; set; } // NULL nếu root
public int Order { get; set; }
public string? Icon { get; set; } // lucide-react icon name
public MenuItem? Parent { get; set; }
public List<MenuItem> Children { get; set; } = new();
public List<Permission> Permissions { get; set; } = new();
}

View File

@ -0,0 +1,29 @@
namespace SolutionErp.Domain.Identity;
// Nguồn duy nhất (single source of truth) cho menu key — dùng ở cả BE (policy) và seed.
// FE có `src/lib/menuKeys.ts` đồng bộ tay.
public static class MenuKeys
{
public const string Dashboard = "Dashboard";
public const string Master = "Master";
public const string Suppliers = "Suppliers";
public const string Projects = "Projects";
public const string Departments = "Departments";
public const string Contracts = "Contracts";
public const string Forms = "Forms";
public const string Reports = "Reports";
public const string System = "System";
public const string Users = "Users";
public const string Roles = "Roles";
public const string Permissions = "Permissions";
public static readonly string[] All =
[
Dashboard,
Master, Suppliers, Projects, Departments,
Contracts, Forms, Reports,
System, Users, Roles, Permissions,
];
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];
}

View File

@ -0,0 +1,15 @@
namespace SolutionErp.Domain.Identity;
public class Permission
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid RoleId { get; set; }
public string MenuKey { get; set; } = string.Empty;
public bool CanRead { get; set; }
public bool CanCreate { get; set; }
public bool CanUpdate { get; set; }
public bool CanDelete { get; set; }
public Role? Role { get; set; }
public MenuItem? Menu { get; set; }
}

View File

@ -0,0 +1,11 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Master;
public class Department : AuditableEntity
{
public string Code { get; set; } = string.Empty; // vd "CCM", "PRO", "FIN"
public string Name { get; set; } = string.Empty; // vd "Phòng Kiểm soát Chi phí"
public Guid? ManagerUserId { get; set; } // TPB — Trưởng Phòng ban
public string? Note { get; set; }
}

View File

@ -0,0 +1,14 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Master;
public class Project : AuditableEntity
{
public string Code { get; set; } = string.Empty; // vd "FLOCK 01"
public string Name { get; set; } = string.Empty;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public Guid? ManagerUserId { get; set; } // PM — Giám đốc Dự án
public decimal? BudgetTotal { get; set; } // Tổng ngân sách dự án (tham chiếu cho CCM check)
public string? Note { get; set; }
}

View File

@ -0,0 +1,16 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Master;
public class Supplier : AuditableEntity
{
public string Code { get; set; } = string.Empty; // Mã NCC (viết tắt, dùng trong mã HĐ)
public string Name { get; set; } = string.Empty; // Tên công ty đầy đủ
public SupplierType Type { get; set; }
public string? TaxCode { get; set; } // Mã số thuế
public string? Phone { get; set; }
public string? Email { get; set; }
public string? Address { get; set; }
public string? ContactPerson { get; set; } // Người liên hệ
public string? Note { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace SolutionErp.Domain.Master;
public enum SupplierType
{
NhaCungCap = 1, // NCC — Nhà cung cấp
NhaThauPhu = 2, // NTP — Nhà thầu phụ
ToDoi = 3, // TĐ — Tổ đội
DonViDichVu = 4, // ĐVDV — Đơn vị dịch vụ
ChuDauTu = 5, // CĐT — Chủ đầu tư (đặc biệt, bypass quy trình CCM)
}

View File

@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master;
namespace SolutionErp.Infrastructure.Persistence;
@ -10,6 +11,12 @@ public class ApplicationDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<Supplier> Suppliers => Set<Supplier>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<Department> Departments => Set<Department>();
public DbSet<MenuItem> MenuItems => Set<MenuItem>();
public DbSet<Permission> Permissions => Set<Permission>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Master;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class DepartmentConfiguration : IEntityTypeConfiguration<Department>
{
public void Configure(EntityTypeBuilder<Department> b)
{
b.ToTable("Departments");
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.Note).HasMaxLength(1000);
b.HasIndex(x => x.Code).IsUnique();
b.HasQueryFilter(x => !x.IsDeleted);
}
}

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Identity;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class MenuItemConfiguration : IEntityTypeConfiguration<MenuItem>
{
public void Configure(EntityTypeBuilder<MenuItem> b)
{
b.ToTable("MenuItems");
b.HasKey(x => x.Key);
b.Property(x => x.Key).HasMaxLength(50);
b.Property(x => x.Label).HasMaxLength(200).IsRequired();
b.Property(x => x.ParentKey).HasMaxLength(50);
b.Property(x => x.Icon).HasMaxLength(50);
b.HasOne(x => x.Parent)
.WithMany(x => x.Children)
.HasForeignKey(x => x.ParentKey)
.OnDelete(DeleteBehavior.Restrict);
b.HasIndex(x => x.ParentKey);
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Identity;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class PermissionConfiguration : IEntityTypeConfiguration<Permission>
{
public void Configure(EntityTypeBuilder<Permission> b)
{
b.ToTable("Permissions");
b.HasKey(x => x.Id);
b.Property(x => x.MenuKey).HasMaxLength(50).IsRequired();
b.HasOne(x => x.Role)
.WithMany()
.HasForeignKey(x => x.RoleId)
.OnDelete(DeleteBehavior.Cascade);
b.HasOne(x => x.Menu)
.WithMany(m => m.Permissions)
.HasForeignKey(x => x.MenuKey)
.OnDelete(DeleteBehavior.Cascade);
b.HasIndex(x => new { x.RoleId, x.MenuKey }).IsUnique();
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Master;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
public void Configure(EntityTypeBuilder<Project> b)
{
b.ToTable("Projects");
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.BudgetTotal).HasPrecision(18, 2);
b.Property(x => x.Note).HasMaxLength(1000);
b.HasIndex(x => x.Code).IsUnique();
b.HasQueryFilter(x => !x.IsDeleted);
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.Master;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class SupplierConfiguration : IEntityTypeConfiguration<Supplier>
{
public void Configure(EntityTypeBuilder<Supplier> b)
{
b.ToTable("Suppliers");
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.Type).HasConversion<int>();
b.Property(x => x.TaxCode).HasMaxLength(20);
b.Property(x => x.Phone).HasMaxLength(30);
b.Property(x => x.Email).HasMaxLength(100);
b.Property(x => x.Address).HasMaxLength(500);
b.Property(x => x.ContactPerson).HasMaxLength(200);
b.Property(x => x.Note).HasMaxLength(1000);
b.HasIndex(x => x.Code).IsUnique();
b.HasIndex(x => x.Type);
b.HasQueryFilter(x => !x.IsDeleted);
}
}

View File

@ -23,6 +23,14 @@ public static class DbInitializer
logger.LogInformation("Applying migrations...");
await db.Database.MigrateAsync();
await SeedRolesAsync(roleManager, logger);
await SeedAdminAsync(userManager, logger);
await SeedMenuTreeAsync(db, logger);
await SeedAdminPermissionsAsync(db, roleManager, logger);
}
private static async Task SeedRolesAsync(RoleManager<Role> roleManager, ILogger logger)
{
foreach (var roleName in AppRoles.All)
{
if (!await roleManager.RoleExistsAsync(roleName))
@ -31,7 +39,10 @@ public static class DbInitializer
logger.LogInformation("Created role {Role}", roleName);
}
}
}
private static async Task SeedAdminAsync(UserManager<User> userManager, ILogger logger)
{
var admin = await userManager.FindByEmailAsync(AdminEmail);
if (admin is null)
{
@ -54,4 +65,67 @@ public static class DbInitializer
logger.LogInformation("Seeded admin user {Email}", AdminEmail);
}
}
private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)
{
// (key, label, parent, order, icon) — icon là lucide-react name
var tree = new (string Key, string Label, string? Parent, int Order, string Icon)[]
{
(MenuKeys.Dashboard, "Tổng quan", null, 10, "LayoutDashboard"),
(MenuKeys.Master, "Danh mục", null, 20, "Database"),
(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"),
(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"),
(MenuKeys.System, "Hệ thống", null, 90, "Settings"),
(MenuKeys.Users, "Người dùng", MenuKeys.System, 91, "User"),
(MenuKeys.Roles, "Vai trò", MenuKeys.System, 92, "Shield"),
(MenuKeys.Permissions, "Phân quyền", MenuKeys.System, 93, "KeyRound"),
};
var existingKeys = await db.MenuItems.Select(m => m.Key).ToListAsync();
var added = 0;
foreach (var (key, label, parent, order, icon) in tree)
{
if (existingKeys.Contains(key)) continue;
db.MenuItems.Add(new MenuItem { Key = key, Label = label, ParentKey = parent, Order = order, Icon = icon });
added++;
}
if (added > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Seeded {Count} menu items", added);
}
}
private static async Task SeedAdminPermissionsAsync(ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
{
var adminRole = await roleManager.FindByNameAsync(AppRoles.Admin);
if (adminRole is null) return;
var existingMenuKeys = await db.Permissions
.Where(p => p.RoleId == adminRole.Id)
.Select(p => p.MenuKey)
.ToListAsync();
var added = 0;
foreach (var menuKey in MenuKeys.All)
{
if (existingMenuKeys.Contains(menuKey)) continue;
db.Permissions.Add(new Permission
{
RoleId = adminRole.Id,
MenuKey = menuKey,
CanRead = true, CanCreate = true, CanUpdate = true, CanDelete = true,
});
added++;
}
if (added > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Seeded {Count} admin permissions", added);
}
}
}

View File

@ -0,0 +1,494 @@
// <auto-generated />
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("20260421041944_AddMasterData")]
partial class AddMasterData
{
/// <inheritdoc />
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<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Identity.Role", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("RefreshToken")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime?>("RefreshTokenExpiresAt")
.HasColumnType("datetime2");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("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.Department", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("Departments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal?>("BudgetTotal")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<DateTime?>("EndDate")
.HasColumnType("datetime2");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("Projects", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Address")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ContactPerson")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
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>("Email")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Phone")
.HasMaxLength(30)
.HasColumnType("nvarchar(30)");
b.Property<string>("TaxCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Suppliers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,125 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddMasterData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Departments",
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),
ManagerUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Note = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, 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_Departments", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Projects",
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),
StartDate = table.Column<DateTime>(type: "datetime2", nullable: true),
EndDate = table.Column<DateTime>(type: "datetime2", nullable: true),
ManagerUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
BudgetTotal = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true),
Note = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, 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_Projects", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Suppliers",
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),
Type = table.Column<int>(type: "int", nullable: false),
TaxCode = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
Phone = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: true),
Email = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Address = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
ContactPerson = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Note = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, 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_Suppliers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Departments_Code",
table: "Departments",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Projects_Code",
table: "Projects",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Suppliers_Code",
table: "Suppliers",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Suppliers_Type",
table: "Suppliers",
column: "Type");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Departments");
migrationBuilder.DropTable(
name: "Projects");
migrationBuilder.DropTable(
name: "Suppliers");
}
}
}

View File

@ -0,0 +1,595 @@
// <auto-generated />
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("20260421042236_AddPermissions")]
partial class AddPermissions
{
/// <inheritdoc />
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<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.Property<string>("Key")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Icon")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("CanCreate")
.HasColumnType("bit");
b.Property<bool>("CanDelete")
.HasColumnType("bit");
b.Property<bool>("CanRead")
.HasColumnType("bit");
b.Property<bool>("CanUpdate")
.HasColumnType("bit");
b.Property<string>("MenuKey")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<Guid>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FullName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("RefreshToken")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<DateTime?>("RefreshTokenExpiresAt")
.HasColumnType("datetime2");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<string>("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.Department", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("Departments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal?>("BudgetTotal")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<DateTime?>("EndDate")
.HasColumnType("datetime2");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("Projects", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Address")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ContactPerson")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
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>("Email")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Phone")
.HasMaxLength(30)
.HasColumnType("nvarchar(30)");
b.Property<string>("TaxCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Suppliers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", 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<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentKey")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b =>
{
b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Menu")
.WithMany("Permissions")
.HasForeignKey("MenuKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SolutionErp.Domain.Identity.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Menu");
b.Navigation("Role");
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.Navigation("Children");
b.Navigation("Permissions");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MenuItems",
columns: table => new
{
Key = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Label = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
ParentKey = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Order = table.Column<int>(type: "int", nullable: false),
Icon = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MenuItems", x => x.Key);
table.ForeignKey(
name: "FK_MenuItems_MenuItems_ParentKey",
column: x => x.ParentKey,
principalTable: "MenuItems",
principalColumn: "Key",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Permissions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RoleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
MenuKey = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
CanRead = table.Column<bool>(type: "bit", nullable: false),
CanCreate = table.Column<bool>(type: "bit", nullable: false),
CanUpdate = table.Column<bool>(type: "bit", nullable: false),
CanDelete = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Permissions", x => x.Id);
table.ForeignKey(
name: "FK_Permissions_MenuItems_MenuKey",
column: x => x.MenuKey,
principalTable: "MenuItems",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Permissions_Roles_RoleId",
column: x => x.RoleId,
principalTable: "Roles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MenuItems_ParentKey",
table: "MenuItems",
column: "ParentKey");
migrationBuilder.CreateIndex(
name: "IX_Permissions_MenuKey",
table: "Permissions",
column: "MenuKey");
migrationBuilder.CreateIndex(
name: "IX_Permissions_RoleId_MenuKey",
table: "Permissions",
columns: new[] { "RoleId", "MenuKey" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Permissions");
migrationBuilder.DropTable(
name: "MenuItems");
}
}
}

View File

@ -125,6 +125,71 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.Property<string>("Key")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Icon")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("CanCreate")
.HasColumnType("bit");
b.Property<bool>("CanDelete")
.HasColumnType("bit");
b.Property<bool>("CanRead")
.HasColumnType("bit");
b.Property<bool>("CanUpdate")
.HasColumnType("bit");
b.Property<string>("MenuKey")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<Guid>("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<Guid>("Id")
@ -247,6 +312,194 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Department", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("Departments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Project", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal?>("BudgetTotal")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
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<DateTime?>("EndDate")
.HasColumnType("datetime2");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<Guid?>("ManagerUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime?>("StartDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.ToTable("Projects", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Address")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ContactPerson")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
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>("Email")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Phone")
.HasMaxLength(30)
.HasColumnType("nvarchar(30)");
b.Property<string>("TaxCode")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Suppliers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("SolutionErp.Domain.Identity.Role", null)
@ -297,6 +550,42 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent")
.WithMany("Children")
.HasForeignKey("ParentKey")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Parent");
});
modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b =>
{
b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Menu")
.WithMany("Permissions")
.HasForeignKey("MenuKey")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SolutionErp.Domain.Identity.Role", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Menu");
b.Navigation("Role");
});
modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b =>
{
b.Navigation("Children");
b.Navigation("Permissions");
});
#pragma warning restore 612, 618
}
}