diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index c87c758..f155e5c 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -757,9 +757,13 @@ 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). + // 16 demo users covering full org chart — RECONCILE PATTERN (idempotent + + // self-healing). Mỗi run: + // - Nếu user chưa có → create + assign roles + dept + position + // - Nếu user đã có → verify dept + position + roles, fix nếu drift + // - Try-catch per-user → 1 fail không abort 12 còn lại + // - Log chi tiết success/fixed/created/failed counts + // Default password: User@123456 (warn log để rotate prod). private const string DemoUserPassword = "User@123456"; private static async Task SeedDemoUsersAsync( @@ -769,7 +773,7 @@ public static class DbInitializer var depts = await db.Departments.ToDictionaryAsync(d => d.Code, d => d.Id); if (depts.Count == 0) { - logger.LogWarning("Skipping SeedDemoUsersAsync — no departments seeded yet."); + logger.LogWarning("SeedDemoUsersAsync: skip — no departments seeded yet (run SeedDepartmentsAsync first)."); return; } @@ -778,53 +782,119 @@ public static class DbInitializer 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 }), + // BOD (3) — director + signer + admin assistant + ("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 }), + ("bod.tran@solutionerp.local", "Trần Quốc Bảo", "BOD", "Phó Giám đốc Kỹ thuật (NĐUQ)", new[] { AppRoles.AuthorizedSigner }), + + // PM (2) — giám đốc dự án + ("pm.nguyen@solutionerp.local", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", new[] { AppRoles.ProjectManager }), + ("pm.le@solutionerp.local", "Lê Thanh Tùng", "PM", "Giám đốc Dự án Vinhomes Ocean Park", new[] { AppRoles.ProjectManager }), + + // CCM/PRO/FIN/ACT/EQU/HRA — Trưởng phòng các phòng ban (1 mỗi) + ("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 }), + + // Drafter (5) — soạn thảo HĐ ở các phòng khác nhau + ("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 }), + ("nv.truong@solutionerp.local", "Trương Minh Quân", "CCM", "Nhân viên Kiểm soát chi phí", new[] { AppRoles.Drafter }), }; - int created = 0; + int created = 0, fixedExisting = 0, failed = 0; foreach (var (email, fullName, deptCode, position, roles) in demoUsers) { - if (await userManager.FindByEmailAsync(email) is not null) continue; + try + { + var existing = await userManager.FindByEmailAsync(email); + if (existing is null) + { + 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))); + failed++; + continue; + } + foreach (var role in roles) + { + var roleResult = await userManager.AddToRoleAsync(user, role); + if (!roleResult.Succeeded) + logger.LogWarning("Demo user {Email} AddRole {Role} fail: {Err}", + email, role, string.Join("; ", roleResult.Errors.Select(e => e.Description))); + } + created++; + } + else + { + // Reconcile — verify dept + position + roles, fix nếu drift + bool changed = false; + var targetDeptId = deptId(deptCode); + if (existing.DepartmentId != targetDeptId) + { + existing.DepartmentId = targetDeptId; changed = true; + } + if (existing.Position != position) + { + existing.Position = position; changed = true; + } + if (existing.FullName != fullName) + { + existing.FullName = fullName; changed = true; + } + if (!existing.IsActive) + { + existing.IsActive = true; changed = true; + } + if (changed) + { + existing.UpdatedAt = DateTime.UtcNow; + await userManager.UpdateAsync(existing); + } - 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; + // Reconcile roles: ensure target roles có trong assigned set + var currentRoles = await userManager.GetRolesAsync(existing); + var missingRoles = roles.Where(r => !currentRoles.Contains(r)).ToList(); + foreach (var role in missingRoles) + { + var addResult = await userManager.AddToRoleAsync(existing, role); + if (!addResult.Succeeded) + logger.LogWarning("Demo user {Email} AddRole {Role} fail: {Err}", + email, role, string.Join("; ", addResult.Errors.Select(e => e.Description))); + changed = true; + } + + if (changed) fixedExisting++; + } + } + catch (Exception ex) + { + logger.LogError(ex, "SeedDemoUsersAsync: unhandled exception cho user {Email}", email); + failed++; } - 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); } + + logger.LogInformation( + "Demo users reconcile xong: {Created} created, {Fixed} fixed (drift), {Failed} failed. (Total demo: {Total}, password={Pwd} — ⚠ rotate prod)", + created, fixedExisting, failed, demoUsers.Length, DemoUserPassword); } private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)