Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/SeedDataService.Workers.cs
T
spouliot 6eb7be0193 Demo reset + dev banner suppression for DEMO company
- DemoController: company-code-gated reset action (DEMO only, CSRF protected)
- SeedDataService.Remove: FK-safe topological pre-sweep, all deletes scoped to companyId
- SeedDataService: clock entries, extra seed data, updated customer/worker/job-status seeders
- CompanySettingsController + Index.cshtml: Reset Demo Data button for DEMO company users
- ReportsController + FinancialReportService: supporting report fixes
- _Layout.cshtml: suppress env banner when current company is DEMO (all auth paths)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:26:40 -04:00

195 lines
8.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Shared.Constants;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
/// <summary>
/// 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.
/// </summary>
internal static readonly string[] SeededWorkerEmails =
[
"mike.sanders@pcldemo.com",
"jake.wilson@pcldemo.com",
"sarah.brooks@pcldemo.com",
];
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </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 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;
}
/// <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 24 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;
// 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<JobStatusLookup>()
.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<Job>()
.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<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;
}
}