[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:
@ -24,6 +24,7 @@ public record PermissionDto(
|
|||||||
public record RoleDto(
|
public record RoleDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Name,
|
string Name,
|
||||||
|
string? ShortName,
|
||||||
string? Description,
|
string? Description,
|
||||||
DateTime CreatedAt);
|
DateTime CreatedAt);
|
||||||
|
|
||||||
|
|||||||
@ -115,7 +115,7 @@ public class ListRolesQueryHandler(RoleManager<Role> roleManager) : IRequestHand
|
|||||||
{
|
{
|
||||||
return await roleManager.Roles
|
return await roleManager.Roles
|
||||||
.OrderBy(r => r.Name)
|
.OrderBy(r => r.Name)
|
||||||
.Select(r => new RoleDto(r.Id, r.Name!, r.Description, r.CreatedAt))
|
.Select(r => new RoleDto(r.Id, r.Name!, r.ShortName, r.Description, r.CreatedAt))
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,15 @@ public record UserDto(
|
|||||||
bool IsActive,
|
bool IsActive,
|
||||||
bool IsLocked,
|
bool IsLocked,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
List<string> Roles);
|
List<string> Roles,
|
||||||
|
Guid? DepartmentId,
|
||||||
|
string? DepartmentName,
|
||||||
|
string? Position);
|
||||||
|
|
||||||
// ========== LIST ==========
|
// ========== LIST ==========
|
||||||
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
|
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>>
|
: IRequestHandler<ListUsersQuery, PagedResult<UserDto>>
|
||||||
{
|
{
|
||||||
public async Task<PagedResult<UserDto>> Handle(ListUsersQuery request, CancellationToken ct)
|
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)
|
.Take(request.PageSize)
|
||||||
.ToListAsync(ct);
|
.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 items = new List<UserDto>(users.Count);
|
||||||
var now = dateTime.UtcNow;
|
var now = dateTime.UtcNow;
|
||||||
foreach (var u in users)
|
foreach (var u in users)
|
||||||
{
|
{
|
||||||
var roles = await userManager.GetRolesAsync(u);
|
var roles = await userManager.GetRolesAsync(u);
|
||||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
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);
|
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
||||||
@ -59,7 +69,7 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IDateTime date
|
|||||||
// ========== GET ==========
|
// ========== GET ==========
|
||||||
public record GetUserQuery(Guid Id) : IRequest<UserDto>;
|
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>
|
: IRequestHandler<GetUserQuery, UserDto>
|
||||||
{
|
{
|
||||||
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken ct)
|
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);
|
?? throw new NotFoundException("User", request.Id);
|
||||||
var roles = await userManager.GetRolesAsync(u);
|
var roles = await userManager.GetRolesAsync(u);
|
||||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > dateTime.UtcNow;
|
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 ==========
|
// ========== CREATE ==========
|
||||||
public record CreateUserCommand(string Email, string FullName, string Password, List<string> Roles)
|
public record CreateUserCommand(
|
||||||
: IRequest<Guid>;
|
string Email, string FullName, string Password, List<string> Roles,
|
||||||
|
Guid? DepartmentId = null, string? Position = null) : IRequest<Guid>;
|
||||||
|
|
||||||
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
|
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
|
||||||
{
|
{
|
||||||
@ -104,6 +118,8 @@ public class CreateUserCommandHandler(
|
|||||||
FullName = request.FullName,
|
FullName = request.FullName,
|
||||||
EmailConfirmed = true,
|
EmailConfirmed = true,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
DepartmentId = request.DepartmentId,
|
||||||
|
Position = request.Position,
|
||||||
CreatedAt = dateTime.UtcNow,
|
CreatedAt = dateTime.UtcNow,
|
||||||
};
|
};
|
||||||
var result = await userManager.CreateAsync(user, request.Password);
|
var result = await userManager.CreateAsync(user, request.Password);
|
||||||
@ -122,7 +138,9 @@ public class CreateUserCommandHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== UPDATE ==========
|
// ========== 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>
|
public class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
|
||||||
{
|
{
|
||||||
@ -147,6 +165,9 @@ public class UpdateUserCommandHandler(UserManager<User> userManager, ICurrentUse
|
|||||||
|
|
||||||
user.FullName = request.FullName;
|
user.FullName = request.FullName;
|
||||||
user.IsActive = request.IsActive;
|
user.IsActive = request.IsActive;
|
||||||
|
user.DepartmentId = request.DepartmentId;
|
||||||
|
user.Position = request.Position;
|
||||||
|
user.UpdatedAt = DateTime.UtcNow;
|
||||||
await userManager.UpdateAsync(user);
|
await userManager.UpdateAsync(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,10 @@ namespace SolutionErp.Domain.Identity;
|
|||||||
|
|
||||||
public class Role : IdentityRole<Guid>
|
public class Role : IdentityRole<Guid>
|
||||||
{
|
{
|
||||||
public string? Description { get; set; }
|
// Identity Name = code English (Admin, CostControl) — kept cho tech (FK
|
||||||
|
// UserRoles, WorkflowStepApprover.AssignmentValue, [Authorize(Roles=...)] attr).
|
||||||
|
// 2 field bổ sung cho display tiếng Việt:
|
||||||
|
public string? ShortName { get; set; } // Mã viết tắt VN (vd "QTV", "BOD", "CCM")
|
||||||
|
public string? Description { get; set; } // Tên đầy đủ VN (vd "Quản trị viên hệ thống")
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,9 @@ public class User : IdentityUser<Guid>
|
|||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
// Org chart — user thuộc 1 phòng ban + chức vụ (free text). Nullable
|
||||||
|
// cho admin/system user không thuộc dept cụ thể.
|
||||||
|
public Guid? DepartmentId { get; set; }
|
||||||
|
public string? Position { get; set; } // vd "Trưởng phòng CCM", "QS công trường", "Phó GĐ"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,10 +56,18 @@ public class ApplicationDbContext
|
|||||||
e.ToTable("Users");
|
e.ToTable("Users");
|
||||||
e.Property(u => u.FullName).HasMaxLength(200).IsRequired();
|
e.Property(u => u.FullName).HasMaxLength(200).IsRequired();
|
||||||
e.Property(u => u.RefreshToken).HasMaxLength(512);
|
e.Property(u => u.RefreshToken).HasMaxLength(512);
|
||||||
|
e.Property(u => u.Position).HasMaxLength(200);
|
||||||
|
e.HasIndex(u => u.DepartmentId);
|
||||||
|
// FK Department — Restrict (không xóa dept nếu còn user assigned)
|
||||||
|
e.HasOne<Department>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(u => u.DepartmentId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
builder.Entity<Role>(e =>
|
builder.Entity<Role>(e =>
|
||||||
{
|
{
|
||||||
e.ToTable("Roles");
|
e.ToTable("Roles");
|
||||||
|
e.Property(r => r.ShortName).HasMaxLength(50);
|
||||||
e.Property(r => r.Description).HasMaxLength(500);
|
e.Property(r => r.Description).HasMaxLength(500);
|
||||||
});
|
});
|
||||||
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>>(e => e.ToTable("UserRoles"));
|
builder.Entity<Microsoft.AspNetCore.Identity.IdentityUserRole<Guid>>(e => e.ToTable("UserRoles"));
|
||||||
|
|||||||
@ -30,9 +30,10 @@ public static class DbInitializer
|
|||||||
|
|
||||||
await SeedRolesAsync(roleManager, logger);
|
await SeedRolesAsync(roleManager, logger);
|
||||||
await SeedAdminAsync(userManager, logger);
|
await SeedAdminAsync(userManager, logger);
|
||||||
|
await SeedDepartmentsAsync(db, logger);
|
||||||
|
await SeedDemoUsersAsync(db, userManager, logger);
|
||||||
await SeedMenuTreeAsync(db, logger);
|
await SeedMenuTreeAsync(db, logger);
|
||||||
await SeedAdminPermissionsAsync(db, roleManager, logger);
|
await SeedAdminPermissionsAsync(db, roleManager, logger);
|
||||||
await SeedDepartmentsAsync(db, logger);
|
|
||||||
await SeedDemoMasterDataAsync(db, logger);
|
await SeedDemoMasterDataAsync(db, logger);
|
||||||
await SeedContractTemplatesAsync(db, logger);
|
await SeedContractTemplatesAsync(db, logger);
|
||||||
await SeedWorkflowDefinitionsAsync(db, logger);
|
await SeedWorkflowDefinitionsAsync(db, logger);
|
||||||
@ -280,14 +281,51 @@ public static class DbInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 12 role với ShortName (Vietnamese abbreviation) + Description (Vietnamese
|
||||||
|
// full name). Identity Name = code English (kept cho FK + [Authorize] attr).
|
||||||
|
// Idempotent: tạo role mới nếu chưa có, backfill ShortName/Description nếu null.
|
||||||
|
private static readonly Dictionary<string, (string ShortName, string Description)> RoleLabels = new()
|
||||||
|
{
|
||||||
|
[AppRoles.Admin] = ("QTV", "Quản trị viên hệ thống"),
|
||||||
|
[AppRoles.Drafter] = ("NV.PB", "Nhân viên phòng ban (soạn thảo HĐ)"),
|
||||||
|
[AppRoles.DeptManager] = ("TPB", "Trưởng phòng ban"),
|
||||||
|
[AppRoles.ProjectManager] = ("PM", "Giám đốc dự án"),
|
||||||
|
[AppRoles.Procurement] = ("PRO", "Phòng Cung ứng"),
|
||||||
|
[AppRoles.CostControl] = ("CCM", "Phòng Kiểm soát chi phí"),
|
||||||
|
[AppRoles.Finance] = ("FIN", "Phòng Tài chính"),
|
||||||
|
[AppRoles.Accounting] = ("ACT", "Phòng Kế toán"),
|
||||||
|
[AppRoles.Equipment] = ("EQU", "Phòng Thiết bị"),
|
||||||
|
[AppRoles.Director] = ("BOD", "Ban Giám đốc"),
|
||||||
|
[AppRoles.AuthorizedSigner] = ("NĐUQ", "Người được Ủy quyền ký HĐ"),
|
||||||
|
[AppRoles.HrAdmin] = ("HRA", "Phòng Nhân sự - Hành chính"),
|
||||||
|
};
|
||||||
|
|
||||||
private static async Task SeedRolesAsync(RoleManager<Role> roleManager, ILogger logger)
|
private static async Task SeedRolesAsync(RoleManager<Role> roleManager, ILogger logger)
|
||||||
{
|
{
|
||||||
foreach (var roleName in AppRoles.All)
|
foreach (var roleName in AppRoles.All)
|
||||||
{
|
{
|
||||||
if (!await roleManager.RoleExistsAsync(roleName))
|
var (shortName, desc) = RoleLabels.TryGetValue(roleName, out var v)
|
||||||
|
? v : (roleName, roleName);
|
||||||
|
|
||||||
|
var existing = await roleManager.FindByNameAsync(roleName);
|
||||||
|
if (existing is null)
|
||||||
{
|
{
|
||||||
await roleManager.CreateAsync(new Role { Name = roleName, CreatedAt = DateTime.UtcNow });
|
await roleManager.CreateAsync(new Role
|
||||||
logger.LogInformation("Created role {Role}", roleName);
|
{
|
||||||
|
Name = roleName,
|
||||||
|
ShortName = shortName,
|
||||||
|
Description = desc,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
logger.LogInformation("Created role {Role} ({Short})", roleName, shortName);
|
||||||
|
}
|
||||||
|
else if (existing.ShortName != shortName || existing.Description != desc)
|
||||||
|
{
|
||||||
|
// Backfill labels for existing roles (post-migration)
|
||||||
|
existing.ShortName = shortName;
|
||||||
|
existing.Description = desc;
|
||||||
|
await roleManager.UpdateAsync(existing);
|
||||||
|
logger.LogInformation("Backfilled role {Role} labels", roleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,6 +355,76 @@ public static class DbInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed ~13 demo users covering full org chart (1+ user per role, distributed
|
||||||
|
// across 9 departments). Idempotent: skip nếu user đã tồn tại theo email.
|
||||||
|
// Default password: User@123456 (warn ở log để rotate).
|
||||||
|
private const string DemoUserPassword = "User@123456";
|
||||||
|
|
||||||
|
private static async Task SeedDemoUsersAsync(
|
||||||
|
ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
|
||||||
|
{
|
||||||
|
// Lookup department Id theo Code
|
||||||
|
var depts = await db.Departments.ToDictionaryAsync(d => d.Code, d => d.Id);
|
||||||
|
if (depts.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Skipping SeedDemoUsersAsync — no departments seeded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid? deptId(string code) => depts.TryGetValue(code, out var id) ? id : null;
|
||||||
|
|
||||||
|
var demoUsers = new[]
|
||||||
|
{
|
||||||
|
// (Email, FullName, DeptCode, Position, RoleNames[])
|
||||||
|
("bod.huynh@solutionerp.local", "Huỳnh Văn Hùng", "BOD", "Tổng Giám đốc", new[] { AppRoles.Director }),
|
||||||
|
("bod.le@solutionerp.local", "Lê Thị Mai", "BOD", "Phó Giám đốc (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
|
||||||
|
("pm.nguyen@solutionerp.local", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", new[] { AppRoles.ProjectManager }),
|
||||||
|
("ccm.tran@solutionerp.local", "Trần Văn Bình", "CCM", "Trưởng phòng Kiểm soát chi phí", new[] { AppRoles.CostControl, AppRoles.DeptManager }),
|
||||||
|
("pro.pham@solutionerp.local", "Phạm Thị Hồng", "PRO", "Trưởng phòng Cung ứng", new[] { AppRoles.Procurement, AppRoles.DeptManager }),
|
||||||
|
("fin.do@solutionerp.local", "Đỗ Minh Tuấn", "FIN", "Trưởng phòng Tài chính", new[] { AppRoles.Finance, AppRoles.DeptManager }),
|
||||||
|
("act.vu@solutionerp.local", "Vũ Thị Lan", "ACT", "Kế toán trưởng", new[] { AppRoles.Accounting, AppRoles.DeptManager }),
|
||||||
|
("equ.bui@solutionerp.local", "Bùi Văn Khánh", "EQU", "Trưởng phòng Thiết bị", new[] { AppRoles.Equipment, AppRoles.DeptManager }),
|
||||||
|
("hra.dang@solutionerp.local", "Đặng Thị Thanh", "HRA", "Trưởng phòng Nhân sự - Hành chính", new[] { AppRoles.HrAdmin, AppRoles.DeptManager }),
|
||||||
|
("qs.hoang@solutionerp.local", "Hoàng Văn Đức", "QS", "QS công trường — soạn thảo HĐ", new[] { AppRoles.Drafter }),
|
||||||
|
("qs.ngo@solutionerp.local", "Ngô Thị Hà", "QS", "QS dự án FLOCK 01", new[] { AppRoles.Drafter }),
|
||||||
|
("nv.cao@solutionerp.local", "Cao Văn Long", "PRO", "Nhân viên Cung ứng", new[] { AppRoles.Drafter }),
|
||||||
|
("nv.dinh@solutionerp.local", "Đinh Thị Yến", "FIN", "Nhân viên Tài chính", new[] { AppRoles.Drafter }),
|
||||||
|
};
|
||||||
|
|
||||||
|
int created = 0;
|
||||||
|
foreach (var (email, fullName, deptCode, position, roles) in demoUsers)
|
||||||
|
{
|
||||||
|
if (await userManager.FindByEmailAsync(email) is not null) continue;
|
||||||
|
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
UserName = email,
|
||||||
|
Email = email,
|
||||||
|
FullName = fullName,
|
||||||
|
EmailConfirmed = true,
|
||||||
|
IsActive = true,
|
||||||
|
DepartmentId = deptId(deptCode),
|
||||||
|
Position = position,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
var result = await userManager.CreateAsync(user, DemoUserPassword);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Demo user {Email} create fail: {Err}",
|
||||||
|
email, string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (var role in roles) await userManager.AddToRoleAsync(user, role);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
if (created > 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Seed {Count} demo users (password={Pwd} — ⚠ rotate in prod).",
|
||||||
|
created, DemoUserPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)
|
private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)
|
||||||
{
|
{
|
||||||
var typeLabels = new Dictionary<string, string>
|
var typeLabels = new Dictionary<string, string>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRoleShortNameAndUserDepartment : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "DepartmentId",
|
||||||
|
table: "Users",
|
||||||
|
type: "uniqueidentifier",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Position",
|
||||||
|
table: "Users",
|
||||||
|
type: "nvarchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ShortName",
|
||||||
|
table: "Roles",
|
||||||
|
type: "nvarchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_DepartmentId",
|
||||||
|
table: "Users",
|
||||||
|
column: "DepartmentId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Users_Departments_DepartmentId",
|
||||||
|
table: "Users",
|
||||||
|
column: "DepartmentId",
|
||||||
|
principalTable: "Departments",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Users_Departments_DepartmentId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Users_DepartmentId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DepartmentId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Position",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ShortName",
|
||||||
|
table: "Roles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1311,6 +1311,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("nvarchar(256)");
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ShortName")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
b.HasIndex("NormalizedName")
|
||||||
@ -1337,6 +1341,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DepartmentId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<string>("Email")
|
b.Property<string>("Email")
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("nvarchar(256)");
|
.HasColumnType("nvarchar(256)");
|
||||||
@ -1375,6 +1382,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<bool>("PhoneNumberConfirmed")
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Position")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
b.Property<string>("RefreshToken")
|
b.Property<string>("RefreshToken")
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
.HasColumnType("nvarchar(512)");
|
.HasColumnType("nvarchar(512)");
|
||||||
@ -1397,6 +1408,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DepartmentId");
|
||||||
|
|
||||||
b.HasIndex("NormalizedEmail")
|
b.HasIndex("NormalizedEmail")
|
||||||
.HasDatabaseName("EmailIndex");
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
@ -2114,6 +2127,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("Role");
|
b.Navigation("Role");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.Identity.User", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SolutionErp.Domain.Master.Department", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DepartmentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Approvals");
|
b.Navigation("Approvals");
|
||||||
|
|||||||
Reference in New Issue
Block a user