using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
///
/// Seeds 50 powder coating jobs that collectively demonstrate all 16 job statuses,
/// realistic date progressions, varied priorities, and quote linkage for the first 25 jobs.
///
///
///
/// Idempotency: returns 0 immediately if any non-deleted jobs already exist for this company.
///
///
/// The method depends on job-status and job-priority lookup rows (populated earlier in the
/// seed sequence), and on at least one customer record. It returns 0 if any of these
/// dependencies are missing so the overall seed degrades gracefully.
///
///
/// Job numbers follow the production format JOB-YYMM-####. The seeder scans
/// existing numbers with the current month prefix and starts its sequence above the current
/// maximum so demo jobs never collide with real jobs created in the same calendar month.
///
///
/// The first 25 jobs are linked to approved quotes (loaded from the previously seeded
/// quotes). When a match is found the job inherits the quote's customer, description,
/// quoted price, and customer PO — matching the production quote-to-job conversion path.
///
///
/// Date logic groups jobs into three buckets: early-stage (future scheduled date),
/// in-progress (past start date, no completion), and completed/terminal (both started
/// and completed dates in the past). This ensures the dashboard pipeline and calendar
/// views display a realistic spread rather than all jobs sharing the same date.
///
///
/// The IgnoreQueryFilters() call on the existence check ensures that soft-deleted
/// leftover jobs from a previous seed run are detected and do not cause duplicate inserts.
///
///
/// The tenant company to seed jobs for.
/// Number of jobs inserted, or 0 if already seeded or dependencies are missing.
private async Task SeedJobsAsync(Company company)
{
var existingCount = await _context.Set()
.IgnoreQueryFilters()
.CountAsync(j => j.CompanyId == company.Id && !j.IsDeleted);
if (existingCount > 0)
return 0;
var jobStatuses = await _context.Set()
.IgnoreQueryFilters()
.Where(s => s.CompanyId == company.Id)
.ToDictionaryAsync(s => s.StatusCode, s => s.Id);
var jobPriorities = await _context.Set()
.IgnoreQueryFilters()
.Where(p => p.CompanyId == company.Id)
.ToDictionaryAsync(p => p.PriorityCode, p => p.Id);
if (jobStatuses.Count == 0 || jobPriorities.Count == 0)
return 0;
var customers = await _context.Set()
.IgnoreQueryFilters()
.Where(c => c.CompanyId == company.Id && !c.IsDeleted)
.OrderBy(c => c.Id)
.ToListAsync();
if (customers.Count == 0)
return 0;
// Grab approved quotes to link to jobs
var approvedQuotes = await _context.Set()
.IgnoreQueryFilters()
.Where(q => q.CompanyId == company.Id && q.QuoteStatus.StatusCode == "APPROVED")
.OrderBy(q => q.Id)
.ToListAsync();
var shopUsers = await _context.Set()
.Where(u => u.CompanyId == company.Id && u.IsActive)
.OrderBy(u => u.Id)
.ToListAsync();
var now = DateTime.UtcNow;
var prefix = $"JOB-{now:yy}{now.Month:D2}-";
var existing = await _context.Set()
.IgnoreQueryFilters()
.Where(j => j.JobNumber.StartsWith(prefix))
.Select(j => j.JobNumber)
.ToListAsync();
var maxNum = 0;
foreach (var n in existing)
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 48–49) 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"
};
// 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
// badges and rush-fee logic are clearly visible in demo data.
static string PriorityFor(int i) => (i % 10) switch
{
0 => "RUSH",
1 => "RUSH",
2 => "URGENT",
3 => "URGENT",
4 => "HIGH",
5 => "HIGH",
6 => "HIGH",
_ => "NORMAL"
};
// Returns description, finish color, prep flags, and estimated minutes for a job item.
// Indexed by (i * 3 + j) % 15 so that item variety cycles independently of the job index,
// preventing every job from having the same first item.
static (string desc, string color, bool sand, bool mask, int mins) ItemSpec(int i, int j) =>
((i * 3 + j) % 15) switch
{
0 => ("18\" Aluminum Wheels — Matte Black", "Matte Black", true, false, 45),
1 => ("17\" Steel Wheels — Gloss White", "Gloss White", false, false, 30),
2 => ("Valve Covers — Wrinkle Red", "Wrinkle Red", true, true, 40),
3 => ("Motorcycle Frame — Flat Black", "Flat Black", true, false, 90),
4 => ("Steel Shelving Units", "Textured Gray", true, false, 55),
5 => ("Industrial Machine Guard Panels", "Safety Yellow", false, false, 35),
6 => ("Aluminum Window Frames", "Satin Bronze", false, true, 50),
7 => ("Steel Handrail — 40 ft run", "Gloss Black", true, false, 120),
8 => ("Wrought Iron Gate", "Hammered Black", true, false, 180),
9 => ("Brake Calipers — Gloss Yellow", "Gloss Yellow", false, true, 35),
10 => ("Restaurant Chair Frames (set of 20)", "Hammered Bronze", false, false, 60),
11 => ("Bicycle Frame — Candy Blue", "Candy Blue", true, true, 60),
12 => ("Compressor Tank", "Safety Orange", true, false, 45),
13 => ("Patio Furniture Set", "Textured Beige", false, false, 50),
_ => ("Custom Steel Parts — Batch", "Matte Gray", true, false, 40)
};
var jobs = new List();
var quoteIdx = 0;
for (int i = 0; i < 50; i++)
{
var statusCode = StatusFor(i);
var priorityCode = PriorityFor(i);
var customer = customers[i % customers.Count];
// Link an approved quote to the first 25 in-progress/active jobs
Quote? linkedQuote = null;
if (i < 25 && quoteIdx < approvedQuotes.Count)
{
// 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;
}
// 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();
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
{
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 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().AddRangeAsync(jobs);
await _context.SaveChangesAsync();
return jobs.Count;
}
}