Phase 4: Past appointments + AI prediction demo data

- Appointments: add ~25 past appointments (last 90 days) with Completed,
  Cancelled, No Show, and Rescheduled statuses; completed records carry
  ActualStartTime/ActualEndTime with realistic variance; cancel/no-show
  notes explain why; customer label falls back to ContactFirst/LastName
  for residential customers
- Fix future appointment title for residential customers (was always using
  CompanyName which is null for individuals)
- New SeedDataService.AiPredictions.cs: seeds 8 AiItemPrediction records
  (varied complexity/confidence/tags/reasoning) and attaches them to the
  first 8 eligible QuoteItems, marking those items IsAiItem=true; 3 of 8
  have UserOverrodeEstimate=true for AI Accuracy report demo
- SeedDataService.cs: wire SeedAiPredictionsAsync after Invoices
- Remove.cs: collect QuoteItem.AiPredictionId FKs before deleting items,
  then delete orphaned AiItemPrediction records after quotes are removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:40:12 -04:00
parent dbd39a9fe5
commit c0e4a66126
4 changed files with 278 additions and 6 deletions
@@ -180,6 +180,14 @@ public partial class SeedDataService
if (seededQuoteIds.Any())
{
// Collect prediction IDs before removing items (FK is NoAction — predictions
// must be deleted after the items that reference them are gone).
var predictionIds = await _context.QuoteItems.IgnoreQueryFilters()
.Where(qi => seededQuoteIds.Contains(qi.QuoteId) && qi.AiPredictionId != null)
.Select(qi => qi.AiPredictionId!.Value)
.Distinct()
.ToListAsync();
var quoteItems = await _context.QuoteItems.IgnoreQueryFilters()
.Where(qi => seededQuoteIds.Contains(qi.QuoteId)).ToListAsync();
if (quoteItems.Any()) _context.QuoteItems.RemoveRange(quoteItems);
@@ -193,6 +201,21 @@ public partial class SeedDataService
_context.Quotes.RemoveRange(quotes);
totalRemoved += quotes.Count;
details.Add($"✓ Removed {quotes.Count} seeded quote(s)");
// Remove orphaned AI predictions now that QuoteItems no longer reference them
if (predictionIds.Any())
{
var predictions = await _context.Set<Core.Entities.AiItemPrediction>()
.IgnoreQueryFilters()
.Where(p => predictionIds.Contains(p.Id))
.ToListAsync();
if (predictions.Any())
{
_context.Set<Core.Entities.AiItemPrediction>().RemoveRange(predictions);
totalRemoved += predictions.Count;
details.Add($"✓ Removed {predictions.Count} AI prediction(s)");
}
}
}
// Customer notes