Demo seed Phase 2: workers, time entries, maintenance records
- 5 named shop workers seeded as ApplicationUser (Employee role): Mike Sanders (Coater), Jake Wilson (Sandblaster), Sarah Brooks (Inspector), Tyler Green (General), Chris Mason (Lead) — @pcldemo.com fingerprint domain - Job time entries seeded for all in-progress and completed jobs; Worker Productivity report will have data from day one - Maintenance history seeded per equipment: 2 completed records + 1 upcoming scheduled + 1 overdue record on Pressure Pot for overdue alert demo - Equipment renamed to spec names: Main Batch Oven, Small Batch Oven, Powder Coating Booth, Blast Cabinet, Pressure Pot Blaster, Air Compressor, Wash Station, Forklift (replaced Overhead Conveyor which wasn't in spec) - RemoveSeedDataOptions.Workers added; Remove.cs cleans up workers + time entries on Demo Reset; SeedDataController resets workers in ResetDemoCompany Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Shared.Constants;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Services;
|
||||
|
||||
public partial class SeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical emails of the 5 demo shop workers. Used as fingerprints in RemoveSeedDataAsync
|
||||
/// to avoid needing a special "IsSeeded" flag on ApplicationUser.
|
||||
/// </summary>
|
||||
internal static readonly string[] SeededWorkerEmails =
|
||||
[
|
||||
"mike.sanders@pcldemo.com",
|
||||
"jake.wilson@pcldemo.com",
|
||||
"sarah.brooks@pcldemo.com",
|
||||
"tyler.green@pcldemo.com",
|
||||
"chris.mason@pcldemo.com",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Seeds 5 named shop workers as ApplicationUser records for the demo company:
|
||||
/// Mike Sanders (Coater), Jake Wilson (Sandblaster), Sarah Brooks (Inspector),
|
||||
/// Tyler Green (General Worker), and Chris Mason (Shop Lead).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Workers are ApplicationUser records with CompanyRole = ShopFloor and the
|
||||
/// Employee system role. They are seeded before jobs and time entries so that
|
||||
/// AssignedUserId on Job and UserId on JobTimeEntry can reference them.
|
||||
///
|
||||
/// Uses a consistent email domain (@pcldemo.com) that will never conflict with
|
||||
/// real user accounts, making them safe to identify and remove on Demo Reset.
|
||||
///
|
||||
/// Idempotency: bails early if any of the 5 worker emails already exist.
|
||||
/// </remarks>
|
||||
private async Task<int> SeedShopWorkersAsync(Company company)
|
||||
{
|
||||
var anyExists = await _userManager.Users
|
||||
.AnyAsync(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == company.Id);
|
||||
if (anyExists) return 0;
|
||||
|
||||
const string defaultPassword = "Worker123!";
|
||||
int created = 0;
|
||||
|
||||
// (email, firstName, lastName, empNum, position, laborRate)
|
||||
var workers = new (string email, string fn, string ln, string emp, string pos, decimal rate)[]
|
||||
{
|
||||
("mike.sanders@pcldemo.com", "Mike", "Sanders", "EMP-001", "Coater", 22.00m),
|
||||
("jake.wilson@pcldemo.com", "Jake", "Wilson", "EMP-002", "Sandblaster", 20.00m),
|
||||
("sarah.brooks@pcldemo.com", "Sarah", "Brooks", "EMP-003", "Quality Inspector", 24.00m),
|
||||
("tyler.green@pcldemo.com", "Tyler", "Green", "EMP-004", "General Worker", 18.00m),
|
||||
("chris.mason@pcldemo.com", "Chris", "Mason", "EMP-005", "Shop Lead", 28.00m),
|
||||
};
|
||||
|
||||
foreach (var (email, fn, ln, emp, pos, rate) in workers)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(email);
|
||||
if (user != null) continue;
|
||||
|
||||
user = new ApplicationUser
|
||||
{
|
||||
UserName = email,
|
||||
Email = email,
|
||||
FirstName = fn,
|
||||
LastName = ln,
|
||||
EmployeeNumber = emp,
|
||||
Department = "Shop Floor",
|
||||
Position = pos,
|
||||
LaborCostPerHour = rate,
|
||||
EmailConfirmed = true,
|
||||
HireDate = DateTime.UtcNow.AddMonths(-12),
|
||||
IsActive = true,
|
||||
CompanyId = company.Id,
|
||||
CompanyRole = AppConstants.CompanyRoles.Worker,
|
||||
CanManageJobs = true,
|
||||
CanViewShopFloor = true,
|
||||
CreatedAt = DateTime.UtcNow.AddMonths(-12)
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user, defaultPassword);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await _userManager.AddToRoleAsync(user, AppConstants.Roles.Employee);
|
||||
created++;
|
||||
}
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds job time entries for completed and in-progress jobs, giving the Worker
|
||||
/// Productivity report meaningful data from day one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
private async Task<int> SeedJobTimeEntriesAsync(Company company)
|
||||
{
|
||||
var existingCount = await _context.Set<JobTimeEntry>()
|
||||
.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;
|
||||
|
||||
// Only create entries for jobs that have been worked on
|
||||
var activeJobs = await _context.Set<Job>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == company.Id && !j.IsDeleted)
|
||||
.Include(j => j.JobStatus)
|
||||
.ToListAsync();
|
||||
|
||||
var workedJobs = activeJobs.Where(j =>
|
||||
j.JobStatus?.StatusCode is
|
||||
"IN_PREPARATION" or "SANDBLASTING" or "MASKING_TAPING" or "CLEANING" or
|
||||
"IN_OVEN" or "COATING" or "CURING" or "QUALITY_CHECK" or
|
||||
"COMPLETED" or "READY_FOR_PICKUP" or "DELIVERED"
|
||||
).ToList();
|
||||
|
||||
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<JobTimeEntry>();
|
||||
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<JobTimeEntry>().AddRangeAsync(entries);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return entries.Count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user