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>
This commit is contained in:
2026-06-12 09:26:40 -04:00
parent 7735fe3cce
commit 6eb7be0193
13 changed files with 963 additions and 221 deletions
@@ -7,32 +7,26 @@ 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.
/// 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",
"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).
/// 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 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.
/// 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)
{
@@ -40,50 +34,71 @@ public partial class SeedDataService
.AnyAsync(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == company.Id);
if (anyExists) return 0;
const string defaultPassword = "Worker123!";
const string pwd = "Worker123!Demo";
var hireDate = DateTime.UtcNow.AddMonths(-18);
int created = 0;
// (email, firstName, lastName, empNum, position, laborRate)
var workers = new (string email, string fn, string ln, string emp, string pos, decimal rate)[]
// ── 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),
("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)
("mike.sanders@pcldemo.com", "Mike", "Sanders", "EMP-001", "Coater", 22.00m),
("jake.wilson@pcldemo.com", "Jake", "Wilson", "EMP-002", "Sandblaster", 20.00m),
})
{
var user = await _userManager.FindByEmailAsync(email);
if (user != null) continue;
user = new ApplicationUser
if (await _userManager.FindByEmailAsync(email) != null) continue;
var user = new ApplicationUser
{
UserName = email,
Email = email,
FirstName = fn,
LastName = ln,
EmployeeNumber = emp,
UserName = email, Email = email,
FirstName = fn, LastName = ln,
EmployeeNumber = emp, Position = pos,
Department = "Shop Floor",
Position = pos,
LaborCostPerHour = rate,
EmailConfirmed = true,
HireDate = DateTime.UtcNow.AddMonths(-12),
IsActive = true,
EmailConfirmed = true, IsActive = true,
HireDate = hireDate,
CompanyId = company.Id,
CompanyRole = AppConstants.CompanyRoles.Worker,
CanManageJobs = true,
CanViewShopFloor = true,
CreatedAt = DateTime.UtcNow.AddMonths(-12)
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++;
}
var result = await _userManager.CreateAsync(user, defaultPassword);
if (result.Succeeded)
// ── 1 manager ─────────────────────────────────────────────────────────────
const string managerEmail = "sarah.brooks@pcldemo.com";
if (await _userManager.FindByEmailAsync(managerEmail) == null)
{
var mgr = new ApplicationUser
{
await _userManager.AddToRoleAsync(user, AppConstants.Roles.Employee);
created++;
}
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;