Scale demo seed data down for tutorial recordings

Customers: 100 → 27 (15 commercial across auto/industrial/architectural/
fitness/marine/energy, including 2 tax-exempt govts; 12 individuals)

Quotes: 75 → 20; date range extended to 4-6 months (was 90 days);
status distribution adjusted proportionally (2 draft, 3 sent, 10 approved,
3 rejected, 2 expired)

Jobs: fixed 50-loop → per-customer 0-5 jobs (~32 total); jobIdx cycles
all 16 statuses globally so every status is visible; creation dates spread
across 1-5 months for in-progress/early jobs, 2-6 months for completed jobs

SeededCustomerEmails updated to match new 27-customer set (added
gnelson@email.com and carol.evans@email.com)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 21:56:32 -04:00
parent 72382a5dd5
commit 01f6897d08
4 changed files with 165 additions and 247 deletions
@@ -98,34 +98,23 @@ public partial class SeedDataService
if (n.Length >= 13 && int.TryParse(n.Substring(9, 4), out var x) && x > maxNum) maxNum = x;
var seq = maxNum + 1;
// ── Status plan (50 jobs, covering all 16 statuses) ──────────────────
// Active pipeline: PENDING(4) QUOTED(3) APPROVED(4) IN_PREPARATION(4)
// SANDBLASTING(4) MASKING_TAPING(3) CLEANING(3) IN_OVEN(3)
// COATING(4) CURING(3) QUALITY_CHECK(3) COMPLETED(5)
// READY_FOR_PICKUP(4) DELIVERED(3) ON_HOLD(2) CANCELLED(2)
//
// Maps job index to a status code, distributing all 16 statuses across 50 jobs.
// ON_HOLD and CANCELLED are placed last (indices 4849) because they are terminal
// side-branches that affect date logic and status history traversal differently.
static string StatusFor(int i) => i switch
{
< 4 => "PENDING",
< 7 => "QUOTED",
< 11 => "APPROVED",
< 15 => "IN_PREPARATION",
< 19 => "SANDBLASTING",
< 22 => "MASKING_TAPING",
< 25 => "CLEANING",
< 28 => "IN_OVEN",
< 32 => "COATING",
< 35 => "CURING",
< 38 => "QUALITY_CHECK",
< 43 => "COMPLETED",
< 47 => "READY_FOR_PICKUP",
< 48 => "DELIVERED",
< 49 => "ON_HOLD",
_ => "CANCELLED"
};
// ── Per-customer job counts (27 customers, ~32 total jobs) ──────────
// Varied 0-5 jobs per customer; the global jobIdx cycles all 16 statuses
// so every status is visible without requiring a large fixed pool.
static int JobsFor(int ci) => new[]
{ 3, 2, 1, 2, 0, 2, 1, 3, 0, 1, 2, 1, 0, 2, 1, 1, 2, 0, 1, 0, 2, 1, 0, 1, 0, 1, 2 }[ci];
// All 16 statuses in production workflow order — cycled globally across jobs
// so the full pipeline is represented even with fewer total records.
string[] allStatuses =
[
"PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING",
"MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING",
"QUALITY_CHECK", "COMPLETED", "READY_FOR_PICKUP", "DELIVERED",
"ON_HOLD", "CANCELLED"
];
string StatusFor(int jobIdx) => allStatuses[jobIdx % allStatuses.Length];
// Maps job index modulo 10 to a priority code. RUSH and URGENT are intentionally
// over-represented (4 of 10) relative to production averages so the priority colour
@@ -165,105 +154,113 @@ public partial class SeedDataService
_ => ("Custom Steel Parts — Batch", "Matte Gray", true, false, 40)
};
var jobs = new List<Job>();
var quoteIdx = 0;
var jobs = new List<Job>();
var quoteIdx = 0;
var jobIdx = 0; // global counter drives status cycling across all customers
for (int i = 0; i < 50; i++)
for (int ci = 0; ci < customers.Count; ci++)
{
var statusCode = StatusFor(i);
var priorityCode = PriorityFor(i);
var customer = customers[i % customers.Count];
var customer = customers[ci];
var numJobs = JobsFor(ci);
// Link an approved quote to the first 25 in-progress/active jobs
Quote? linkedQuote = null;
if (i < 25 && quoteIdx < approvedQuotes.Count)
for (int j = 0; j < numJobs; j++, jobIdx++, seq++)
{
// Only link if the quote's customer matches OR if customers align by index
linkedQuote = approvedQuotes[quoteIdx++];
customer = customers.FirstOrDefault(c => c.Id == linkedQuote.CustomerId) ?? customer;
}
var statusCode = StatusFor(jobIdx);
var priorityCode = PriorityFor(jobIdx);
// Date logic — creation spread from -21 days to today
// Scheduled: future for early statuses, past for completed ones
var isCompleted = statusCode is "COMPLETED" or "READY_FOR_PICKUP" or "DELIVERED" or "CANCELLED";
var isInProgress = statusCode is "IN_PREPARATION" or "SANDBLASTING" or "MASKING_TAPING"
or "CLEANING" or "IN_OVEN" or "COATING" or "CURING" or "QUALITY_CHECK";
var isEarly = statusCode is "PENDING" or "QUOTED" or "APPROVED";
int daysAgo = isCompleted ? 14 + (i % 7)
: isInProgress ? 5 + (i % 7)
: 0 + (i % 5);
var createdDate = now.AddDays(-daysAgo);
var scheduledDate = isCompleted ? createdDate.AddDays(2)
: isInProgress ? now.AddDays(-(i % 3))
: now.AddDays(2 + (i % 10));
var rushDays = priorityCode == "RUSH" ? 2 : priorityCode == "URGENT" ? 3 : 7;
var dueDate = scheduledDate.AddDays(rushDays);
var startedDate = (!isEarly) ? scheduledDate : (DateTime?)null;
var completedDate = isCompleted ? scheduledDate.AddDays(1) : (DateTime?)null;
var assignedUserId = shopUsers.Count > 0 ? shopUsers[i % shopUsers.Count].Id : null;
var itemCount = 1 + (i % 3);
var items = new List<JobItem>();
for (int j = 0; j < itemCount; j++)
{
var (desc, color, sand, mask, mins) = ItemSpec(i, j);
var qty = 1 + (j % 3);
var unitPrice = linkedQuote != null && j == 0
? Math.Round((linkedQuote.Total / itemCount), 2)
: Math.Round(75m + (i % 8) * 12.5m + j * 15m, 2);
items.Add(new JobItem
// Link an approved quote when one is available
Quote? linkedQuote = null;
if (quoteIdx < approvedQuotes.Count)
{
Description = desc,
Quantity = qty,
ColorName = color,
SurfaceAreaSqFt = 10m + j * 3.5m,
UnitPrice = unitPrice,
TotalPrice = unitPrice * qty,
LaborCost = Math.Round(unitPrice * qty * 0.35m, 2),
RequiresSandblasting = sand,
RequiresMasking = mask,
EstimatedMinutes = mins,
CompanyId = company.Id,
CreatedAt = createdDate
var candidate = approvedQuotes[quoteIdx];
if (candidate.CustomerId == customer.Id || quoteIdx % 3 == 0)
{
linkedQuote = candidate;
quoteIdx++;
}
}
// Date logic — creation spread over 4-6 months
// Older jobs for completed statuses, recent for in-progress, future-scheduled for early statuses
var isCompleted = statusCode is "COMPLETED" or "READY_FOR_PICKUP" or "DELIVERED" or "CANCELLED";
var isInProgress = statusCode is "IN_PREPARATION" or "SANDBLASTING" or "MASKING_TAPING"
or "CLEANING" or "IN_OVEN" or "COATING" or "CURING" or "QUALITY_CHECK";
var isEarly = statusCode is "PENDING" or "QUOTED" or "APPROVED";
// Spread creation over 30-150 days ago (1-5 months), older jobs for completed statuses
int daysAgo = isCompleted ? 60 + (jobIdx % 90)
: isInProgress ? 10 + (jobIdx % 40)
: 2 + (jobIdx % 15);
var createdDate = now.AddDays(-daysAgo);
var scheduledDate = isCompleted ? createdDate.AddDays(3 + (jobIdx % 5))
: isInProgress ? now.AddDays(-(jobIdx % 4))
: now.AddDays(3 + (jobIdx % 12));
var rushDays = priorityCode == "RUSH" ? 2 : priorityCode == "URGENT" ? 3 : 7;
var dueDate = scheduledDate.AddDays(rushDays);
var startedDate = !isEarly ? scheduledDate : (DateTime?)null;
var completedDate = isCompleted ? scheduledDate.AddDays(1) : (DateTime?)null;
var assignedUserId = shopUsers.Count > 0 ? shopUsers[jobIdx % shopUsers.Count].Id : null;
var itemCount = 1 + (jobIdx % 3);
var items = new List<JobItem>();
for (int k = 0; k < itemCount; k++)
{
var (desc, color, sand, mask, mins) = ItemSpec(jobIdx, k);
var qty = 1 + (k % 3);
var unitPrice = linkedQuote != null && k == 0
? Math.Round(linkedQuote.Total / itemCount, 2)
: Math.Round(75m + (jobIdx % 8) * 12.5m + k * 15m, 2);
items.Add(new JobItem
{
Description = desc,
Quantity = qty,
ColorName = color,
SurfaceAreaSqFt = 10m + k * 3.5m,
UnitPrice = unitPrice,
TotalPrice = unitPrice * qty,
LaborCost = Math.Round(unitPrice * qty * 0.35m, 2),
RequiresSandblasting = sand,
RequiresMasking = mask,
EstimatedMinutes = mins,
CompanyId = company.Id,
CreatedAt = createdDate
});
}
var finalPrice = items.Sum(it => it.TotalPrice);
var quotedPrice = linkedQuote?.Total ?? Math.Round(finalPrice * 1.05m, 2);
jobs.Add(new Job
{
JobNumber = $"{prefix}{seq:D4}",
CustomerId = customer.Id,
QuoteId = linkedQuote?.Id,
AssignedUserId = assignedUserId,
Description = linkedQuote?.Description
?? $"Powder coating services for {customer.CompanyName ?? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()}",
JobStatusId = jobStatuses[statusCode],
JobPriorityId = jobPriorities[priorityCode],
ScheduledDate = scheduledDate,
StartedDate = startedDate,
CompletedDate = completedDate,
DueDate = dueDate,
QuotedPrice = quotedPrice,
FinalPrice = finalPrice,
IsRushJob = priorityCode == "RUSH",
CustomerPO = linkedQuote?.CustomerPO ?? (jobIdx % 3 == 0 ? $"PO-{40000 + jobIdx}" : null),
SpecialInstructions = jobIdx % 6 == 0 ? "Customer supplied parts — handle with extra care." :
jobIdx % 11 == 0 ? "Match existing color exactly — bring sample for approval." : null,
InternalNotes = jobIdx % 8 == 0 ? "Vintage parts — do not use aggressive blast media." : null,
RequiresCustomerApproval = jobIdx % 5 == 0,
IsCustomerApproved = jobIdx % 5 != 0 || !isEarly,
JobItems = items,
CompanyId = company.Id,
CreatedAt = createdDate
});
}
var finalPrice = items.Sum(it => it.TotalPrice);
var quotedPrice = linkedQuote?.Total ?? Math.Round(finalPrice * 1.05m, 2);
jobs.Add(new Job
{
JobNumber = $"{prefix}{seq:D4}",
CustomerId = customer.Id,
QuoteId = linkedQuote?.Id,
AssignedUserId = assignedUserId,
Description = linkedQuote?.Description
?? $"Powder coating services for {customer.CompanyName ?? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()}",
JobStatusId = jobStatuses[statusCode],
JobPriorityId = jobPriorities[priorityCode],
ScheduledDate = scheduledDate,
StartedDate = startedDate,
CompletedDate = completedDate,
DueDate = dueDate,
QuotedPrice = quotedPrice,
FinalPrice = finalPrice,
IsRushJob = priorityCode == "RUSH",
CustomerPO = linkedQuote?.CustomerPO ?? (i % 3 == 0 ? $"PO-{40000 + i}" : null),
SpecialInstructions = i % 6 == 0 ? "Customer supplied parts — handle with extra care." :
i % 11 == 0 ? "Match existing color exactly — bring sample for approval." : null,
InternalNotes = i % 8 == 0 ? "Vintage parts — do not use aggressive blast media." : null,
RequiresCustomerApproval = i % 5 == 0,
IsCustomerApproved = i % 5 != 0 || !isEarly,
JobItems = items,
CompanyId = company.Id,
CreatedAt = createdDate
});
seq++;
}
await _context.Set<Job>().AddRangeAsync(jobs);