Files
PowderCoatingLogix/src/PowderCoating.Infrastructure/Services/SeedDataService.Jobs.cs
T
2026-04-23 21:38:24 -04:00

275 lines
14 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.Core.Enums;
namespace PowderCoating.Infrastructure.Services;
public partial class SeedDataService
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// Idempotency: returns 0 immediately if any non-deleted jobs already exist for this company.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Job numbers follow the production format <c>JOB-YYMM-####</c>. 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// The <c>IgnoreQueryFilters()</c> call on the existence check ensures that soft-deleted
/// leftover jobs from a previous seed run are detected and do not cause duplicate inserts.
/// </para>
/// </remarks>
/// <param name="company">The tenant company to seed jobs for.</param>
/// <returns>Number of jobs inserted, or 0 if already seeded or dependencies are missing.</returns>
private async Task<int> SeedJobsAsync(Company company)
{
var existingCount = await _context.Set<Job>()
.IgnoreQueryFilters()
.CountAsync(j => j.CompanyId == company.Id && !j.IsDeleted);
if (existingCount > 0)
return 0;
var jobStatuses = await _context.Set<JobStatusLookup>()
.IgnoreQueryFilters()
.Where(s => s.CompanyId == company.Id)
.ToDictionaryAsync(s => s.StatusCode, s => s.Id);
var jobPriorities = await _context.Set<JobPriorityLookup>()
.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<Customer>()
.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<Quote>()
.IgnoreQueryFilters()
.Where(q => q.CompanyId == company.Id && q.QuoteStatus.StatusCode == "APPROVED")
.OrderBy(q => q.Id)
.ToListAsync();
var shopUsers = await _context.Set<ApplicationUser>()
.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<Job>()
.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 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"
};
// 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<Job>();
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<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
{
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<Job>().AddRangeAsync(jobs);
await _context.SaveChangesAsync();
return jobs.Count;
}
}