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; } }