using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Shared.Constants; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Canonical emails of the 3 demo employees (2 workers + 1 manager). Used as fingerprints /// in RemoveSeedDataAsync to avoid needing a special "IsSeeded" flag on ApplicationUser. /// internal static readonly string[] SeededWorkerEmails = [ "mike.sanders@pcldemo.com", "jake.wilson@pcldemo.com", "sarah.brooks@pcldemo.com", ]; /// /// Seeds 3 named employees as ApplicationUser records for the demo company: /// Mike Sanders (Coater, Worker), Jake Wilson (Sandblaster, Worker), /// and Sarah Brooks (Shop Manager, Manager role with broader permissions). /// /// /// Workers are seeded before jobs and time entries so that AssignedUserId on Job /// and UserId on JobTimeEntry and EmployeeClockEntry can reference them. /// Uses @pcldemo.com email domain — will never conflict with real accounts. /// Idempotency: bails early if any of the 3 emails already exist for this company. /// private async Task SeedShopWorkersAsync(Company company) { var anyExists = await _userManager.Users .AnyAsync(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == company.Id); if (anyExists) return 0; const string pwd = "Worker123!Demo"; var hireDate = DateTime.UtcNow.AddMonths(-18); int created = 0; // ── 2 shop workers ──────────────────────────────────────────────────────── foreach (var (email, fn, ln, emp, pos, rate) in new (string, string, string, string, string, decimal)[] { ("mike.sanders@pcldemo.com", "Mike", "Sanders", "EMP-001", "Coater", 22.00m), ("jake.wilson@pcldemo.com", "Jake", "Wilson", "EMP-002", "Sandblaster", 20.00m), }) { if (await _userManager.FindByEmailAsync(email) != null) continue; var user = new ApplicationUser { UserName = email, Email = email, FirstName = fn, LastName = ln, EmployeeNumber = emp, Position = pos, Department = "Shop Floor", LaborCostPerHour = rate, EmailConfirmed = true, IsActive = true, HireDate = hireDate, CompanyId = company.Id, CompanyRole = AppConstants.CompanyRoles.Worker, CanManageJobs = true, CanViewShopFloor = true, CreatedAt = hireDate }; var r = await _userManager.CreateAsync(user, pwd); if (!r.Succeeded) throw new InvalidOperationException( $"Failed to create {email}: {string.Join("; ", r.Errors.Select(e => e.Description))}"); await _userManager.AddToRoleAsync(user, AppConstants.Roles.Employee); created++; } // ── 1 manager ───────────────────────────────────────────────────────────── const string managerEmail = "sarah.brooks@pcldemo.com"; if (await _userManager.FindByEmailAsync(managerEmail) == null) { var mgr = new ApplicationUser { UserName = managerEmail, Email = managerEmail, FirstName = "Sarah", LastName = "Brooks", EmployeeNumber = "EMP-003", Position = "Shop Manager", Department = "Management", LaborCostPerHour = 32.00m, EmailConfirmed = true, IsActive = true, HireDate = hireDate.AddMonths(-6), // hired before the workers CompanyId = company.Id, CompanyRole = AppConstants.CompanyRoles.Manager, CanManageJobs = true, CanViewShopFloor = true, CanManageCustomers = true, CanCreateQuotes = true, CanApproveQuotes = true, CanManageCalendar = true, CanViewCalendar = true, CanManageProducts = true, CanViewProducts = true, CanManageEquipment = true, CanManageMaintenance = true, CanManageInventory = true, CanViewReports = true, CreatedAt = hireDate.AddMonths(-6) }; var r = await _userManager.CreateAsync(mgr, pwd); if (!r.Succeeded) throw new InvalidOperationException( $"Failed to create {managerEmail}: {string.Join("; ", r.Errors.Select(e => e.Description))}"); await _userManager.AddToRoleAsync(mgr, AppConstants.Roles.Employee); created++; } return created; } /// /// Seeds job time entries for completed and in-progress jobs, giving the Worker /// Productivity report meaningful data from day one. /// /// /// Each completed or in-progress job receives 2–4 time entries spread across the /// 5 demo workers, with realistic hours for each coating stage (sandblasting, /// masking, coating, curing, inspection). The total hours roughly correlate with /// the job's EstimatedMinutes from its first JobItem. /// /// Idempotency: bails early if any time entries already exist for this company's jobs. /// private async Task SeedJobTimeEntriesAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(te => te.CompanyId == company.Id && !te.IsDeleted); if (existingCount > 0) return 0; var workers = await _userManager.Users .Where(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == company.Id) .OrderBy(u => u.Email) .ToListAsync(); if (workers.Count == 0) return 0; // Resolve status IDs first — avoids relying on Include(j => j.JobStatus) which can // silently return null navigation properties when query filters interact with IgnoreQueryFilters. var workedStatusIds = await _context.Set() .IgnoreQueryFilters() .Where(s => s.CompanyId == company.Id && new[] { "IN_PREPARATION", "SANDBLASTING", "MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK", "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" }.Contains(s.StatusCode)) .Select(s => s.Id) .ToListAsync(); if (workedStatusIds.Count == 0) return 0; var workedJobs = await _context.Set() .IgnoreQueryFilters() .Where(j => j.CompanyId == company.Id && !j.IsDeleted && workedStatusIds.Contains(j.JobStatusId)) .ToListAsync(); if (workedJobs.Count == 0) return 0; string[] stages = ["Sandblasting", "Masking & Prep", "Coating", "Curing", "Inspection"]; decimal[] stageHours = [1.5m, 0.75m, 1.25m, 0.5m, 0.5m]; var entries = new List(); int jobIdx = 0; foreach (var job in workedJobs) { // 2-4 entries per job cycling through stages var entryCount = 2 + (jobIdx % 3); var workDate = (job.StartedDate ?? job.CreatedAt).AddDays(1); for (int e = 0; e < entryCount; e++) { var worker = workers[(jobIdx + e) % workers.Count]; var stageIdx = e % stages.Length; entries.Add(new JobTimeEntry { JobId = job.Id, UserId = worker.Id, UserDisplayName = worker.FullName, WorkDate = workDate.AddDays(e), HoursWorked = stageHours[stageIdx], Stage = stages[stageIdx], CompanyId = company.Id, CreatedAt = workDate.AddDays(e) }); } jobIdx++; } await _context.Set().AddRangeAsync(entries); await _context.SaveChangesAsync(); return entries.Count; } }