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