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:
@@ -47,6 +47,7 @@ public class RemoveSeedDataOptions
|
|||||||
public bool OperatingCosts { get; set; }
|
public bool OperatingCosts { get; set; }
|
||||||
public bool Bills { get; set; }
|
public bool Bills { get; set; }
|
||||||
public bool Expenses { get; set; }
|
public bool Expenses { get; set; }
|
||||||
|
public bool Workers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SeedDataResult
|
public class SeedDataResult
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public partial class SeedDataService
|
|||||||
{
|
{
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Batch Powder Coating Oven #1",
|
EquipmentName = "Main Batch Oven",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-OVN-001",
|
EquipmentNumber = $"{company.CompanyCode}-OVN-001",
|
||||||
EquipmentType = "Oven",
|
EquipmentType = "Oven",
|
||||||
Manufacturer = "Reliant Finishing Systems",
|
Manufacturer = "Reliant Finishing Systems",
|
||||||
@@ -78,7 +78,7 @@ public partial class SeedDataService
|
|||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Batch Powder Coating Oven #2",
|
EquipmentName = "Small Batch Oven",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-OVN-002",
|
EquipmentNumber = $"{company.CompanyCode}-OVN-002",
|
||||||
EquipmentType = "Oven",
|
EquipmentType = "Oven",
|
||||||
Manufacturer = "Reliant Finishing Systems",
|
Manufacturer = "Reliant Finishing Systems",
|
||||||
@@ -99,7 +99,7 @@ public partial class SeedDataService
|
|||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Automated Powder Coating Booth #1",
|
EquipmentName = "Powder Coating Booth",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-BOOTH-001",
|
EquipmentNumber = $"{company.CompanyCode}-BOOTH-001",
|
||||||
EquipmentType = "Spray Booth",
|
EquipmentType = "Spray Booth",
|
||||||
Manufacturer = "Nordson Corporation",
|
Manufacturer = "Nordson Corporation",
|
||||||
@@ -120,7 +120,7 @@ public partial class SeedDataService
|
|||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Manual Powder Coating Booth #2",
|
EquipmentName = "Manual Powder Booth",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-BOOTH-002",
|
EquipmentNumber = $"{company.CompanyCode}-BOOTH-002",
|
||||||
EquipmentType = "Spray Booth",
|
EquipmentType = "Spray Booth",
|
||||||
Manufacturer = "Columbia Coatings",
|
Manufacturer = "Columbia Coatings",
|
||||||
@@ -162,7 +162,7 @@ public partial class SeedDataService
|
|||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Media Blast Room",
|
EquipmentName = "Pressure Pot Blaster",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-BLAST-002",
|
EquipmentNumber = $"{company.CompanyCode}-BLAST-002",
|
||||||
EquipmentType = "Sandblaster",
|
EquipmentType = "Sandblaster",
|
||||||
Manufacturer = "Clemco Industries",
|
Manufacturer = "Clemco Industries",
|
||||||
@@ -183,7 +183,7 @@ public partial class SeedDataService
|
|||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Rotary Screw Air Compressor",
|
EquipmentName = "Air Compressor",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-COMP-001",
|
EquipmentNumber = $"{company.CompanyCode}-COMP-001",
|
||||||
EquipmentType = "Compressor",
|
EquipmentType = "Compressor",
|
||||||
Manufacturer = "Atlas Copco",
|
Manufacturer = "Atlas Copco",
|
||||||
@@ -204,28 +204,28 @@ public partial class SeedDataService
|
|||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Overhead Conveyor System",
|
EquipmentName = "Forklift",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-CONV-001",
|
EquipmentNumber = $"{company.CompanyCode}-FORK-001",
|
||||||
EquipmentType = "Conveyor",
|
EquipmentType = "Forklift",
|
||||||
Manufacturer = "Pacline Conveyors",
|
Manufacturer = "Toyota",
|
||||||
Model = "PAC-500 Overhead",
|
Model = "8FGCU25",
|
||||||
SerialNumber = "PAC50034521",
|
SerialNumber = "PAC50034521",
|
||||||
PurchaseDate = DateTime.UtcNow.AddYears(-4),
|
PurchaseDate = DateTime.UtcNow.AddYears(-4),
|
||||||
PurchasePrice = 52000m,
|
PurchasePrice = 28000m,
|
||||||
WarrantyExpiration = DateTime.UtcNow.AddYears(-2),
|
WarrantyExpiration = DateTime.UtcNow.AddYears(-2),
|
||||||
Status = EquipmentStatus.Operational,
|
Status = EquipmentStatus.Operational,
|
||||||
Location = "Main Production Line",
|
Location = "Loading Area",
|
||||||
RecommendedMaintenanceIntervalDays = 180,
|
RecommendedMaintenanceIntervalDays = 180,
|
||||||
LastMaintenanceDate = DateTime.UtcNow.AddDays(-120),
|
LastMaintenanceDate = DateTime.UtcNow.AddDays(-90),
|
||||||
NextScheduledMaintenance = DateTime.UtcNow.AddDays(60),
|
NextScheduledMaintenance = DateTime.UtcNow.AddDays(90),
|
||||||
Notes = "500 lb capacity overhead conveyor with power and free sections",
|
Notes = "5,000 lb capacity propane forklift — used for loading/unloading customer parts",
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
CompanyId = company.Id,
|
CompanyId = company.Id,
|
||||||
CreatedAt = DateTime.UtcNow.AddYears(-4)
|
CreatedAt = DateTime.UtcNow.AddYears(-4)
|
||||||
},
|
},
|
||||||
new Equipment
|
new Equipment
|
||||||
{
|
{
|
||||||
EquipmentName = "Parts Washer System",
|
EquipmentName = "Wash Station",
|
||||||
EquipmentNumber = $"{company.CompanyCode}-WASH-001",
|
EquipmentNumber = $"{company.CompanyCode}-WASH-001",
|
||||||
EquipmentType = "Washer",
|
EquipmentType = "Washer",
|
||||||
Manufacturer = "Better Engineering",
|
Manufacturer = "Better Engineering",
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PowderCoating.Core.Entities;
|
||||||
|
using PowderCoating.Core.Enums;
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Services;
|
||||||
|
|
||||||
|
public partial class SeedDataService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Seeds maintenance records for all seeded equipment: historical completed records,
|
||||||
|
/// upcoming scheduled records, and one overdue record for the Pressure Pot so the
|
||||||
|
/// Equipment Maintenance report always has meaningful data.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Each piece of equipment gets 2-3 completed historical maintenance records (up to
|
||||||
|
/// 12 months back) plus one upcoming scheduled record. The Pressure Pot additionally
|
||||||
|
/// has one overdue record (past due date, still Scheduled) to populate the overdue
|
||||||
|
/// indicator on the Equipment page.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Labor and parts costs are realistic for shop equipment maintenance, giving the
|
||||||
|
/// Equipment Maintenance Cost report non-trivial totals from day one.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Idempotency: bails early if any maintenance records already exist for this company's equipment.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private async Task<int> SeedMaintenanceRecordsAsync(Company company)
|
||||||
|
{
|
||||||
|
var equipmentIds = await _context.Set<Equipment>()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.Where(e => e.CompanyId == company.Id && !e.IsDeleted && SeededEquipmentSerials.Contains(e.SerialNumber))
|
||||||
|
.Select(e => e.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (equipmentIds.Count == 0) return 0;
|
||||||
|
|
||||||
|
var existingCount = await _context.Set<MaintenanceRecord>()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.CountAsync(m => equipmentIds.Contains(m.EquipmentId));
|
||||||
|
if (existingCount > 0) return 0;
|
||||||
|
|
||||||
|
var equipment = await _context.Set<Equipment>()
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.Where(e => equipmentIds.Contains(e.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Try to grab a worker user to assign as performed-by
|
||||||
|
var worker = await _userManager.Users
|
||||||
|
.Where(u => u.CompanyId == company.Id && u.IsActive)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var records = new List<MaintenanceRecord>();
|
||||||
|
|
||||||
|
// Per-equipment maintenance spec: (type, daysAgoFirst, intervalDays, laborCost, partsCost, notes)
|
||||||
|
// Each equipment gets 2 completed records + 1 scheduled upcoming.
|
||||||
|
// The Pressure Pot also gets 1 overdue record.
|
||||||
|
static (string type, decimal labor, decimal parts, string desc, string work)
|
||||||
|
MaintSpec(int i) => (i % 5) switch
|
||||||
|
{
|
||||||
|
0 => ("Preventive", 120m, 45m, "Quarterly preventive maintenance", "Inspected elements, cleaned contacts, checked gaskets"),
|
||||||
|
1 => ("Inspection", 80m, 0m, "Monthly operational inspection", "Checked all systems, calibrated temperature probes"),
|
||||||
|
2 => ("Repair", 200m, 185m, "Filter and seal replacement", "Replaced intake filters and worn door seals"),
|
||||||
|
3 => ("Preventive", 140m, 60m, "Semi-annual preventive maintenance", "Lubricated moving parts, replaced wear items"),
|
||||||
|
_ => ("Inspection", 60m, 0m, "Pre-season inspection and cleaning", "Full operational test, cleaned all surfaces"),
|
||||||
|
};
|
||||||
|
|
||||||
|
int idx = 0;
|
||||||
|
foreach (var eq in equipment)
|
||||||
|
{
|
||||||
|
var isPressurePot = eq.SerialNumber == "CLM101223456"; // Media Blast Room / Pressure Pot
|
||||||
|
|
||||||
|
for (int r = 0; r < 2; r++)
|
||||||
|
{
|
||||||
|
var (mtype, labor, parts, desc, work) = MaintSpec(idx + r);
|
||||||
|
var daysAgo = 180 - r * 60 - (idx % 4) * 15;
|
||||||
|
var scheduled = now.AddDays(-daysAgo);
|
||||||
|
var total = labor + parts;
|
||||||
|
|
||||||
|
records.Add(new MaintenanceRecord
|
||||||
|
{
|
||||||
|
EquipmentId = eq.Id,
|
||||||
|
MaintenanceType = mtype,
|
||||||
|
Status = MaintenanceStatus.Completed,
|
||||||
|
Priority = MaintenancePriority.Normal,
|
||||||
|
ScheduledDate = scheduled,
|
||||||
|
CompletedDate = scheduled.AddDays(1),
|
||||||
|
PerformedById = worker?.Id,
|
||||||
|
AssignedUserId = worker?.Id,
|
||||||
|
Description = desc,
|
||||||
|
WorkPerformed = work,
|
||||||
|
LaborCost = labor,
|
||||||
|
PartsCost = parts,
|
||||||
|
TotalCost = total,
|
||||||
|
DowntimeHours = 2m + r,
|
||||||
|
CompanyId = company.Id,
|
||||||
|
CreatedAt = scheduled.AddDays(-7)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// One upcoming scheduled record
|
||||||
|
var upcomingDays = 15 + (idx % 30);
|
||||||
|
records.Add(new MaintenanceRecord
|
||||||
|
{
|
||||||
|
EquipmentId = eq.Id,
|
||||||
|
MaintenanceType = "Preventive",
|
||||||
|
Status = MaintenanceStatus.Scheduled,
|
||||||
|
Priority = MaintenancePriority.Normal,
|
||||||
|
ScheduledDate = now.AddDays(upcomingDays),
|
||||||
|
AssignedUserId = worker?.Id,
|
||||||
|
Description = "Scheduled preventive maintenance",
|
||||||
|
LaborCost = 0m, PartsCost = 0m, TotalCost = 0m,
|
||||||
|
CompanyId = company.Id,
|
||||||
|
CreatedAt = now.AddDays(-7)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overdue record for the pressure pot only
|
||||||
|
if (isPressurePot)
|
||||||
|
{
|
||||||
|
records.Add(new MaintenanceRecord
|
||||||
|
{
|
||||||
|
EquipmentId = eq.Id,
|
||||||
|
MaintenanceType = "Repair",
|
||||||
|
Status = MaintenanceStatus.Scheduled,
|
||||||
|
Priority = MaintenancePriority.High,
|
||||||
|
ScheduledDate = now.AddDays(-20), // overdue
|
||||||
|
AssignedUserId = worker?.Id,
|
||||||
|
Description = "Filter replacement — OVERDUE",
|
||||||
|
Notes = "Media filter became clogged ahead of schedule. Shop is running reduced blast capacity until repaired.",
|
||||||
|
LaborCost = 0m, PartsCost = 0m, TotalCost = 0m,
|
||||||
|
CompanyId = company.Id,
|
||||||
|
CreatedAt = now.AddDays(-30)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.Set<MaintenanceRecord>().AddRangeAsync(records);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return records.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,10 @@ public partial class SeedDataService
|
|||||||
.Where(p => seededJobIds.Contains(p.JobId)).ToListAsync();
|
.Where(p => seededJobIds.Contains(p.JobId)).ToListAsync();
|
||||||
if (jobPrepServices.Any()) _context.JobPrepServices.RemoveRange(jobPrepServices);
|
if (jobPrepServices.Any()) _context.JobPrepServices.RemoveRange(jobPrepServices);
|
||||||
|
|
||||||
|
var timeEntries = await _context.Set<Core.Entities.JobTimeEntry>().IgnoreQueryFilters()
|
||||||
|
.Where(te => seededJobIds.Contains(te.JobId)).ToListAsync();
|
||||||
|
if (timeEntries.Any()) _context.Set<Core.Entities.JobTimeEntry>().RemoveRange(timeEntries);
|
||||||
|
|
||||||
var jobs = await _context.Jobs.IgnoreQueryFilters()
|
var jobs = await _context.Jobs.IgnoreQueryFilters()
|
||||||
.Where(j => seededJobIds.Contains(j.Id)).ToListAsync();
|
.Where(j => seededJobIds.Contains(j.Id)).ToListAsync();
|
||||||
_context.Jobs.RemoveRange(jobs);
|
_context.Jobs.RemoveRange(jobs);
|
||||||
@@ -397,6 +401,26 @@ public partial class SeedDataService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Shop Workers ---
|
||||||
|
if (options.Workers)
|
||||||
|
{
|
||||||
|
var workerUsers = await _userManager.Users
|
||||||
|
.Where(u => SeededWorkerEmails.Contains(u.Email) && u.CompanyId == companyId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (workerUsers.Any())
|
||||||
|
{
|
||||||
|
foreach (var wu in workerUsers)
|
||||||
|
await _userManager.DeleteAsync(wu);
|
||||||
|
totalRemoved += workerUsers.Count;
|
||||||
|
details.Add($"✓ Removed {workerUsers.Count} demo shop worker(s)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
details.Add("• No demo shop workers found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.ItemsSeeded = totalRemoved;
|
result.ItemsSeeded = totalRemoved;
|
||||||
result.Details = details;
|
result.Details = details;
|
||||||
result.Message = totalRemoved > 0
|
result.Message = totalRemoved > 0
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -411,13 +411,17 @@ public partial class SeedDataService : ISeedDataService
|
|||||||
}
|
}
|
||||||
catch (Exception ex) { errors.Add($"✗ Customers: {ex.Message}"); _context.ChangeTracker.Clear(); }
|
catch (Exception ex) { errors.Add($"✗ Customers: {ex.Message}"); _context.ChangeTracker.Clear(); }
|
||||||
|
|
||||||
|
// Workers must be seeded before jobs so AssignedUserId FK resolves
|
||||||
|
await RunSeeder("Shop workers", details, errors, result, () => SeedShopWorkersAsync(company));
|
||||||
await RunSeeder("Equipment", details, errors, result, () => SeedEquipmentAsync(company));
|
await RunSeeder("Equipment", details, errors, result, () => SeedEquipmentAsync(company));
|
||||||
|
await RunSeeder("Maintenance", details, errors, result, () => SeedMaintenanceRecordsAsync(company));
|
||||||
await RunSeeder("Vendors", details, errors, result, () => SeedVendorsAsync(company));
|
await RunSeeder("Vendors", details, errors, result, () => SeedVendorsAsync(company));
|
||||||
await RunSeeder("Named ovens", details, errors, result, () => SeedOvenCostsAsync(company));
|
await RunSeeder("Named ovens", details, errors, result, () => SeedOvenCostsAsync(company));
|
||||||
await RunSeeder("Catalog", details, errors, result, () => SeedCatalogAsync(company));
|
await RunSeeder("Catalog", details, errors, result, () => SeedCatalogAsync(company));
|
||||||
await RunSeeder("Quotes", details, errors, result, () => SeedQuotesAsync(company));
|
await RunSeeder("Quotes", details, errors, result, () => SeedQuotesAsync(company));
|
||||||
await RunSeeder("Jobs", details, errors, result, () => SeedJobsAsync(company));
|
await RunSeeder("Jobs", details, errors, result, () => SeedJobsAsync(company));
|
||||||
await RunSeeder("Job history", details, errors, result, () => SeedJobStatusHistoryAsync(company));
|
await RunSeeder("Job history", details, errors, result, () => SeedJobStatusHistoryAsync(company));
|
||||||
|
await RunSeeder("Time entries", details, errors, result, () => SeedJobTimeEntriesAsync(company));
|
||||||
await RunSeeder("Inv. txns", details, errors, result, () => SeedInventoryTransactionsAsync(company));
|
await RunSeeder("Inv. txns", details, errors, result, () => SeedInventoryTransactionsAsync(company));
|
||||||
await RunSeeder("Invoices", details, errors, result, () => SeedInvoicesAsync(company));
|
await RunSeeder("Invoices", details, errors, result, () => SeedInvoicesAsync(company));
|
||||||
await RunSeeder("Vendor bills", details, errors, result, () => SeedBillsAsync(company));
|
await RunSeeder("Vendor bills", details, errors, result, () => SeedBillsAsync(company));
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ public class SeedDataController : Controller
|
|||||||
OperatingCosts = true,
|
OperatingCosts = true,
|
||||||
Bills = true,
|
Bills = true,
|
||||||
Expenses = true,
|
Expenses = true,
|
||||||
|
Workers = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var removeResult = await _seedDataService.RemoveSeedDataAsync(demo.Id, removeOptions);
|
var removeResult = await _seedDataService.RemoveSeedDataAsync(demo.Id, removeOptions);
|
||||||
|
|||||||
Reference in New Issue
Block a user