using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds seven standard appointment status lookup rows for a company: /// Scheduled, Confirmed, In Progress, Completed, Cancelled, No Show, and Rescheduled. /// /// /// Appointment statuses follow the same lookup-table pattern as job statuses: the /// StatusCode strings are referenced by application code; the display name, colour, /// and icon are cosmetic and operator-customisable. /// /// Terminal statuses (Completed, Cancelled, No Show) set IsTerminalStatus = true /// so the calendar and scheduler can filter them out of the "active" appointment view /// without hard-coding status codes. /// /// System-defined statuses (Scheduled, Completed, Cancelled) are marked /// IsSystemDefined = true to prevent operators from deleting them, as the /// appointment-creation workflow always defaults to "SCHEDULED" on new records. /// /// Idempotency: bails early if 7 or more rows already exist for the company. /// /// The tenant company to seed appointment statuses for. /// The number of status rows created (7), or 0 if already seeded. private async Task SeedAppointmentStatusLookupsAsync(Company company) { // Check if appointment statuses already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(s => s.CompanyId == company.Id && !s.IsDeleted); if (existingCount >= 7) { return 0; // Already seeded } var statuses = new List { new AppointmentStatusLookup { StatusCode = "SCHEDULED", DisplayName = "Scheduled", DisplayOrder = 1, ColorClass = "primary", IconClass = "bi-calendar-check", IsActive = true, IsSystemDefined = true, IsTerminalStatus = false, Description = "Appointment has been scheduled", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentStatusLookup { StatusCode = "CONFIRMED", DisplayName = "Confirmed", DisplayOrder = 2, ColorClass = "success", IconClass = "bi-check-circle", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, Description = "Customer has confirmed the appointment", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentStatusLookup { StatusCode = "IN_PROGRESS", DisplayName = "In Progress", DisplayOrder = 3, ColorClass = "warning", IconClass = "bi-hourglass-split", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, Description = "Appointment is currently in progress", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentStatusLookup { StatusCode = "COMPLETED", DisplayName = "Completed", DisplayOrder = 4, ColorClass = "success", IconClass = "bi-check2-all", IsActive = true, IsSystemDefined = true, IsTerminalStatus = true, Description = "Appointment was completed successfully", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentStatusLookup { StatusCode = "CANCELLED", DisplayName = "Cancelled", DisplayOrder = 5, ColorClass = "danger", IconClass = "bi-x-circle", IsActive = true, IsSystemDefined = true, IsTerminalStatus = true, Description = "Appointment was cancelled", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentStatusLookup { StatusCode = "NO_SHOW", DisplayName = "No Show", DisplayOrder = 6, ColorClass = "secondary", IconClass = "bi-person-x", IsActive = true, IsSystemDefined = false, IsTerminalStatus = true, Description = "Customer did not show up for appointment", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentStatusLookup { StatusCode = "RESCHEDULED", DisplayName = "Rescheduled", DisplayOrder = 7, ColorClass = "info", IconClass = "bi-arrow-repeat", IsActive = true, IsSystemDefined = false, IsTerminalStatus = false, Description = "Appointment has been rescheduled to a different time", CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(statuses); await _context.SaveChangesAsync(); return statuses.Count; } /// /// Seeds four standard appointment type lookup rows for a company: /// Customer Drop-Off, Customer Pick-Up, Consultation/Quote, and Scheduled Job Work. /// /// /// Appointment types categorise the purpose of a calendar entry and control optional UI /// behaviour via RequiresJobLink: the JOB_WORK type sets this to true, /// prompting the calendar UI to show a mandatory job selector when scheduling that type. /// All other types leave it false because drop-offs, pick-ups, and consultations /// may exist before a job has been created. /// /// The TypeCode strings are referenced by /// when building appointment titles and determining job-link probability, so they must /// not be changed after initial seeding. /// /// Colour classes (purple, green, blue, orange) map to custom CSS variables in the /// calendar stylesheet — they are distinct from Bootstrap's standard colour palette to /// give each appointment type a unique calendar stripe colour. /// /// Idempotency: bails early if 4 or more rows already exist for the company. /// /// The tenant company to seed appointment types for. /// The number of type rows created (4), or 0 if already seeded. private async Task SeedAppointmentTypeLookupsAsync(Company company) { // Check if appointment types already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(t => t.CompanyId == company.Id && !t.IsDeleted); if (existingCount >= 4) { return 0; // Already seeded } var types = new List { new AppointmentTypeLookup { TypeCode = "DROP_OFF", DisplayName = "Customer Drop-Off", DisplayOrder = 1, ColorClass = "purple", IconClass = "bi-box-arrow-in-down", RequiresJobLink = false, IsActive = true, IsSystemDefined = true, Description = "Customer dropping off items for coating", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentTypeLookup { TypeCode = "PICK_UP", DisplayName = "Customer Pick-Up", DisplayOrder = 2, ColorClass = "green", IconClass = "bi-box-arrow-up", RequiresJobLink = false, IsActive = true, IsSystemDefined = true, Description = "Customer picking up completed items", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentTypeLookup { TypeCode = "CONSULTATION", DisplayName = "Consultation/Quote", DisplayOrder = 3, ColorClass = "blue", IconClass = "bi-chat-dots", RequiresJobLink = false, IsActive = true, IsSystemDefined = true, Description = "Consultation meeting or quote discussion", CompanyId = company.Id, CreatedAt = DateTime.UtcNow }, new AppointmentTypeLookup { TypeCode = "JOB_WORK", DisplayName = "Scheduled Job Work", DisplayOrder = 4, ColorClass = "orange", IconClass = "bi-tools", RequiresJobLink = true, IsActive = true, IsSystemDefined = true, Description = "Scheduled work time for a specific job", CompanyId = company.Id, CreatedAt = DateTime.UtcNow } }; await _context.Set().AddRangeAsync(types); await _context.SaveChangesAsync(); return types.Count; } /// /// Seeds up to 50 sample appointments for a company, distributed across weekdays over /// the next ~60 days, with randomised types, customers, durations, statuses, and optional /// job links and worker assignments. /// /// /// Purpose: Provides a realistic-looking calendar on first login so that the /// Appointments / Calendar feature is immediately useful to evaluate in a demo environment. /// /// Randomisation: A seeded with seed 42 is used so that the /// generated appointments are deterministic — re-seeding the same company always produces /// the same schedule, which simplifies support and testing reproducibility. /// /// Weekend exclusion: Saturday and Sunday are skipped to match typical shop hours. /// The loop iterates calendar days (up to a 90-day safety cap) rather than counting /// business days directly because the simpler approach avoids edge cases around month /// boundaries and holidays. /// /// Job links: JOB_WORK type appointments have a 40% chance of linking to an /// existing job; other types have a 20% chance. Jobs and customers must already be seeded /// (via and ) for links to /// be available — if either collection is empty, the method still seeds appointments but /// without links. /// /// Worker assignments: 50% of appointments are assigned to a random active company /// user. Workers are loaded without IgnoreQueryFilters because the global /// filter for users is identity-based, not company-scoped, and the check /// u.CompanyId == company.Id is applied in the LINQ predicate instead. /// /// Prerequisites: Requires appointment types and statuses to already be seeded; /// returns 0 without creating any records if either collection is empty. /// /// Idempotency: returns 0 if 10 or more appointments already exist for the company. /// The threshold is 10 (not 50) so that a previously partial seed is detected and skipped /// rather than doubled up. /// /// The tenant company to seed appointments for. /// The number of appointment records created (up to 50), or 0 if skipped. private async Task SeedAppointmentsAsync(Company company) { // Check if appointments already exist for this company var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(a => a.CompanyId == company.Id && !a.IsDeleted); if (existingCount >= 10) { return 0; // Already seeded (at least some appointments exist) } // Get required lookup data var appointmentTypes = await _context.Set() .IgnoreQueryFilters() .Where(t => t.CompanyId == company.Id) .OrderBy(t => t.DisplayOrder) .ToListAsync(); var appointmentStatuses = await _context.Set() .IgnoreQueryFilters() .Where(s => s.CompanyId == company.Id) .OrderBy(s => s.DisplayOrder) .ToListAsync(); var customers = await _context.Set() .IgnoreQueryFilters() .Where(c => c.CompanyId == company.Id) .OrderBy(c => c.Id) .ToListAsync(); var jobs = await _context.Set() .IgnoreQueryFilters() .Where(j => j.CompanyId == company.Id) .OrderBy(j => j.Id) .ToListAsync(); var workers = await _context.Set() .Where(u => u.CompanyId == company.Id && u.IsActive) .OrderBy(u => u.Id) .ToListAsync(); if (!appointmentTypes.Any() || !appointmentStatuses.Any() || !customers.Any()) { return 0; // Can't seed appointments without required data } var random = new Random(42); // Deterministic for consistency var appointments = new List(); var startDate = DateTime.Today; var appointmentTitles = new Dictionary { ["DROP_OFF"] = new[] { "Customer Drop-Off", "Parts Delivery", "Item Drop-Off", "Material Drop-Off" }, ["PICK_UP"] = new[] { "Customer Pick-Up", "Collection Appointment", "Order Pick-Up", "Completed Items Pick-Up" }, ["CONSULTATION"] = new[] { "Quote Discussion", "Project Consultation", "Initial Consultation", "Color Selection Meeting" }, ["JOB_WORK"] = new[] { "Sandblasting Session", "Coating Work", "Quality Inspection", "Final Finishing" } }; // Get status IDs by code for easy assignment var scheduledStatusId = appointmentStatuses.First(s => s.StatusCode == "SCHEDULED").Id; var confirmedStatusId = appointmentStatuses.First(s => s.StatusCode == "CONFIRMED").Id; // Generate 50 appointments across next 60 days (weekdays only) int appointmentsCreated = 0; int daysChecked = 0; var currentDate = startDate; while (appointmentsCreated < 50 && daysChecked < 90) // Safety limit { currentDate = startDate.AddDays(daysChecked); daysChecked++; // Skip weekends if (currentDate.DayOfWeek == DayOfWeek.Saturday || currentDate.DayOfWeek == DayOfWeek.Sunday) { continue; } // Create 1-3 appointments per weekday (varied distribution) int appointmentsToday = random.Next(1, 4); for (int i = 0; i < appointmentsToday && appointmentsCreated < 50; i++) { // Random type var appointmentType = appointmentTypes[random.Next(appointmentTypes.Count)]; // Random customer var customer = customers[random.Next(customers.Count)]; // Random time during business hours (8 AM - 4 PM for start times) int startHour = random.Next(8, 17); int startMinute = random.Next(0, 4) * 15; // 0, 15, 30, 45 var scheduledStart = new DateTime(currentDate.Year, currentDate.Month, currentDate.Day, startHour, startMinute, 0, DateTimeKind.Utc); // Duration: 30 min to 2 hours int durationMinutes = random.Next(1, 5) * 30; // 30, 60, 90, or 120 minutes var scheduledEnd = scheduledStart.AddMinutes(durationMinutes); // Status: 80% SCHEDULED, 20% CONFIRMED (for future appointments) int statusId = random.Next(100) < 80 ? scheduledStatusId : confirmedStatusId; // Title string title = $"{customer.CompanyName} - {appointmentTitles[appointmentType.TypeCode][random.Next(appointmentTitles[appointmentType.TypeCode].Length)]}"; // Optional job link (40% chance if type is JOB_WORK, 20% for others) int? jobId = null; if (jobs.Any()) { int jobLinkChance = appointmentType.TypeCode == "JOB_WORK" ? 40 : 20; if (random.Next(100) < jobLinkChance) { jobId = jobs[random.Next(jobs.Count)].Id; } } // Optional user assignment (50% chance) string? assignedWorkerId = null; if (workers.Any() && random.Next(100) < 50) { assignedWorkerId = workers[random.Next(workers.Count)].Id; } // Location (60% chance) string? location = null; if (random.Next(100) < 60) { var locations = new[] { "Main Office", "Loading Dock", "Shop Floor", "Coating Area", "Reception" }; location = locations[random.Next(locations.Length)]; } var appointment = new Appointment { AppointmentNumber = $"APT-{currentDate:yyMM}-{(appointmentsCreated + 1):D4}", CustomerId = customer.Id, JobId = jobId, AppointmentStatusId = statusId, AppointmentTypeId = appointmentType.Id, AssignedUserId = assignedWorkerId, Title = title, Description = random.Next(100) < 40 ? $"Scheduled appointment for {appointmentType.DisplayName.ToLower()}" : null, ScheduledStartTime = scheduledStart, ScheduledEndTime = scheduledEnd, IsAllDay = false, Location = location, IsReminderEnabled = random.Next(100) < 70, // 70% have reminders ReminderMinutesBefore = 30, Notes = random.Next(100) < 30 ? "Auto-generated demo appointment" : null, CompanyId = company.Id, CreatedAt = DateTime.UtcNow }; appointments.Add(appointment); appointmentsCreated++; } } await _context.Set().AddRangeAsync(appointments); await _context.SaveChangesAsync(); return appointments.Count; } }