[CLAUDE] Domain+App+Infra: Role ShortName + User Department/Position + 13 demo users (migration 11)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m53s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m53s
User feedback: chi tiết hóa Users/Phòng ban + gán roles. Roles label tiếng Việt có Mã (ShortName) + Tên đầy đủ (Description). ## Entity changes ### Role (Domain/Identity/Role.cs) + ShortName (max 50) — Mã viết tắt VN: QTV/BOD/CCM/PRO/FIN/... + Description (đã có) — Tên đầy đủ VN - Identity Name = code English giữ nguyên (FK + [Authorize] attr) ### User (Domain/Identity/User.cs) + DepartmentId Guid? FK Departments (Restrict — không xóa dept nếu user reference) + Position string? max 200 — chức vụ free text ## Migration 11: AddRoleShortNameAndUserDepartment 3-file rule. Apply LocalDB OK. DB total: 36 tables (không tăng — chỉ thêm cột vào existing). ## Seed VN labels (12 roles) | Code | ShortName | Description | |---|---|---| | Admin | QTV | Quản trị viên hệ thống | | Drafter | NV.PB | Nhân viên phòng ban (soạn thảo HĐ) | | DeptManager | TPB | Trưởng phòng ban | | ProjectManager | PM | Giám đốc dự án | | Procurement | PRO | Phòng Cung ứng | | CostControl | CCM | Phòng Kiểm soát chi phí | | Finance | FIN | Phòng Tài chính | | Accounting | ACT | Phòng Kế toán | | Equipment | EQU | Phòng Thiết bị | | Director | BOD | Ban Giám đốc | | AuthorizedSigner | NĐUQ | Người được Ủy quyền ký HĐ | | HrAdmin | HRA | Phòng Nhân sự - Hành chính | SeedRolesAsync idempotent + backfill (existing role thiếu ShortName/ Description → update). ## Seed 13 demo users Default password: User@123456 (warn log để rotate prod). Coverage full org chart: - bod.huynh (Tổng GĐ — Director, BOD dept) - bod.le (Phó GĐ NĐUQ — AuthorizedSigner, BOD dept) - pm.nguyen (PM FLOCK 01 — ProjectManager, PM dept) - ccm.tran (TPB CCM — CostControl + DeptManager, CCM dept) - pro.pham (TPB PRO — Procurement + DeptManager, PRO dept) - fin.do, act.vu, equ.bui, hra.dang (TPB respective dept) - qs.hoang, qs.ngo (Drafter — QS dept) - nv.cao, nv.dinh (Drafter — PRO/FIN dept) Idempotent (skip nếu email đã tồn tại). ## DTOs + Commands updated - RoleDto + ShortName field - UserDto + DepartmentId/DepartmentName/Position - CreateUserCommand + DepartmentId/Position params (defaults null) - UpdateUserCommand + DepartmentId/Position - ListUsersQueryHandler load dept names denormalize per page - UpdateUserCommandHandler set UpdatedAt ## Note FE updates (UsersPage dept dropdown + role label VN) ở commit kế tiếp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -17,12 +17,15 @@ public record UserDto(
|
||||
bool IsActive,
|
||||
bool IsLocked,
|
||||
DateTime CreatedAt,
|
||||
List<string> Roles);
|
||||
List<string> Roles,
|
||||
Guid? DepartmentId,
|
||||
string? DepartmentName,
|
||||
string? Position);
|
||||
|
||||
// ========== LIST ==========
|
||||
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
|
||||
|
||||
public class ListUsersQueryHandler(UserManager<User> userManager, IDateTime dateTime)
|
||||
public class ListUsersQueryHandler(UserManager<User> userManager, IApplicationDbContext db, IDateTime dateTime)
|
||||
: IRequestHandler<ListUsersQuery, PagedResult<UserDto>>
|
||||
{
|
||||
public async Task<PagedResult<UserDto>> Handle(ListUsersQuery request, CancellationToken ct)
|
||||
@ -43,13 +46,20 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IDateTime date
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Lookup department names cho denormalize trong DTO
|
||||
var deptIds = users.Where(u => u.DepartmentId is not null).Select(u => u.DepartmentId!.Value).Distinct().ToList();
|
||||
var deptNames = await db.Departments.AsNoTracking()
|
||||
.Where(d => deptIds.Contains(d.Id))
|
||||
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
|
||||
|
||||
var items = new List<UserDto>(users.Count);
|
||||
var now = dateTime.UtcNow;
|
||||
foreach (var u in users)
|
||||
{
|
||||
var roles = await userManager.GetRolesAsync(u);
|
||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList()));
|
||||
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
|
||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position));
|
||||
}
|
||||
|
||||
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
||||
@ -59,7 +69,7 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IDateTime date
|
||||
// ========== GET ==========
|
||||
public record GetUserQuery(Guid Id) : IRequest<UserDto>;
|
||||
|
||||
public class GetUserQueryHandler(UserManager<User> userManager, IDateTime dateTime)
|
||||
public class GetUserQueryHandler(UserManager<User> userManager, IApplicationDbContext db, IDateTime dateTime)
|
||||
: IRequestHandler<GetUserQuery, UserDto>
|
||||
{
|
||||
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken ct)
|
||||
@ -68,13 +78,17 @@ public class GetUserQueryHandler(UserManager<User> userManager, IDateTime dateTi
|
||||
?? throw new NotFoundException("User", request.Id);
|
||||
var roles = await userManager.GetRolesAsync(u);
|
||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > dateTime.UtcNow;
|
||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList());
|
||||
string? deptName = null;
|
||||
if (u.DepartmentId is { } did)
|
||||
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
|
||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CREATE ==========
|
||||
public record CreateUserCommand(string Email, string FullName, string Password, List<string> Roles)
|
||||
: IRequest<Guid>;
|
||||
public record CreateUserCommand(
|
||||
string Email, string FullName, string Password, List<string> Roles,
|
||||
Guid? DepartmentId = null, string? Position = null) : IRequest<Guid>;
|
||||
|
||||
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
|
||||
{
|
||||
@ -104,6 +118,8 @@ public class CreateUserCommandHandler(
|
||||
FullName = request.FullName,
|
||||
EmailConfirmed = true,
|
||||
IsActive = true,
|
||||
DepartmentId = request.DepartmentId,
|
||||
Position = request.Position,
|
||||
CreatedAt = dateTime.UtcNow,
|
||||
};
|
||||
var result = await userManager.CreateAsync(user, request.Password);
|
||||
@ -122,7 +138,9 @@ public class CreateUserCommandHandler(
|
||||
}
|
||||
|
||||
// ========== UPDATE ==========
|
||||
public record UpdateUserCommand(Guid Id, string FullName, bool IsActive) : IRequest;
|
||||
public record UpdateUserCommand(
|
||||
Guid Id, string FullName, bool IsActive,
|
||||
Guid? DepartmentId = null, string? Position = null) : IRequest;
|
||||
|
||||
public class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
|
||||
{
|
||||
@ -147,6 +165,9 @@ public class UpdateUserCommandHandler(UserManager<User> userManager, ICurrentUse
|
||||
|
||||
user.FullName = request.FullName;
|
||||
user.IsActive = request.IsActive;
|
||||
user.DepartmentId = request.DepartmentId;
|
||||
user.Position = request.Position;
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await userManager.UpdateAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user