[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:
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
22
src/Backend/SolutionErp.Api/Controllers/MenusController.cs
Normal file
22
src/Backend/SolutionErp.Api/Controllers/MenusController.cs
Normal 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));
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
17
src/Backend/SolutionErp.Api/Controllers/RolesController.cs
Normal file
17
src/Backend/SolutionErp.Api/Controllers/RolesController.cs
Normal 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));
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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 =>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
14
src/Backend/SolutionErp.Domain/Identity/MenuItem.cs
Normal file
14
src/Backend/SolutionErp.Domain/Identity/MenuItem.cs
Normal 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();
|
||||
}
|
||||
29
src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs
Normal file
29
src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs
Normal 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"];
|
||||
}
|
||||
15
src/Backend/SolutionErp.Domain/Identity/Permission.cs
Normal file
15
src/Backend/SolutionErp.Domain/Identity/Permission.cs
Normal 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; }
|
||||
}
|
||||
11
src/Backend/SolutionErp.Domain/Master/Department.cs
Normal file
11
src/Backend/SolutionErp.Domain/Master/Department.cs
Normal 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; }
|
||||
}
|
||||
14
src/Backend/SolutionErp.Domain/Master/Project.cs
Normal file
14
src/Backend/SolutionErp.Domain/Master/Project.cs
Normal 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; }
|
||||
}
|
||||
16
src/Backend/SolutionErp.Domain/Master/Supplier.cs
Normal file
16
src/Backend/SolutionErp.Domain/Master/Supplier.cs
Normal 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; }
|
||||
}
|
||||
10
src/Backend/SolutionErp.Domain/Master/SupplierType.cs
Normal file
10
src/Backend/SolutionErp.Domain/Master/SupplierType.cs
Normal 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)
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
494
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421041944_AddMasterData.Designer.cs
generated
Normal file
494
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421041944_AddMasterData.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
595
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421042236_AddPermissions.Designer.cs
generated
Normal file
595
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421042236_AddPermissions.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user