7735fe3cce
Seed data fixes: - Fix EF interceptor: no longer overwrites explicitly-set CreatedAt on Added entities — root cause of all "same month" chart issues - Customer seeder: generates 15 customers/month from Jan → current month; keeps 10 commercial anchors in deterministic order for job seeder index map - Invoice seeder: historical range bumped from 2→8 paid invoices/month so P&L shows consistent profit (~$5,200 collected vs ~$4,200 monthly expenses) - Month -1 bumped to 7 paid invoices to stay above expenses - Jobs: set UpdatedAt to historical event date so analytics don't need null fallback - Analytics (ReportsController): use CompletedDate ?? UpdatedAt ?? CreatedAt for revenue chart grouping; fixes empty Revenue Trend charts on Overview/Revenue tabs - SeedDataService: inject IAccountBalanceService; auto-recalculate account balances after seeding; patch checking/savings opening balances unconditionally on reset - Customer list: sort by CompanyName ?? ContactLastName so individuals and commercial accounts interleave instead of appearing as two blocks Invoice resend: - ResendInvoice action now accepts sendEmail + sendSms parameters; SMS-only resend no longer requires an email address on file - Ensures PublicViewToken exists before SMS so the view link is always valid - canResend in Details view now allows Paid invoices (removed != Paid guard) - Resend button shows channel-choice modal when customer has both email + SMS, direct SMS button when SMS only, or email button when email only - New #resendChannelModal mirrors the Send channel modal but posts to ResendInvoice - resendInvoice() JS updated to pass sendEmail/sendSms query params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
217 lines
8.5 KiB
C#
217 lines
8.5 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using PowderCoating.Shared.Constants;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using PowderCoating.Application.Interfaces;
|
|
|
|
namespace PowderCoating.Web.Controllers;
|
|
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public class SeedDataController : Controller
|
|
{
|
|
private readonly ISeedDataService _seedDataService;
|
|
private readonly ILogger<SeedDataController> _logger;
|
|
|
|
public SeedDataController(
|
|
ISeedDataService seedDataService,
|
|
ILogger<SeedDataController> logger)
|
|
{
|
|
_seedDataService = seedDataService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays the Seed Data management page with a list of all companies. Seeding is intentionally NOT automatic on startup — it must be triggered manually here by a SuperAdmin to avoid polluting production databases.
|
|
/// </summary>
|
|
public async Task<IActionResult> Index()
|
|
{
|
|
var companies = await _seedDataService.GetCompaniesAsync();
|
|
return View(companies);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Triggers system-level seeding (global data such as SuperAdmin accounts and dashboard tips). Warnings are stored in TempData as a pipe-delimited string to survive the redirect; they display on the Index view after the POST-Redirect-GET cycle.
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> SeedSystem()
|
|
{
|
|
try
|
|
{
|
|
var result = await _seedDataService.SeedSystemDataAsync();
|
|
|
|
if (result.Success)
|
|
{
|
|
TempData["SuccessMessage"] = result.Message;
|
|
TempData["SeedDetails"] = string.Join("|", result.Details);
|
|
TempData["ItemsSeeded"] = result.ItemsSeeded;
|
|
|
|
if (result.Warnings.Any())
|
|
{
|
|
TempData["WarningMessage"] = $"{result.ItemsSkipped} item(s) were skipped";
|
|
TempData["SeedWarnings"] = string.Join("|", result.Warnings);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TempData["ErrorMessage"] = result.Message;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error seeding system data");
|
|
TempData["ErrorMessage"] = $"An error occurred: {ex.Message}";
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeds demo data (customers, jobs, quotes, inventory, etc.) for a specific company. Warnings are capped at 30 entries in TempData to stay within the 4 KB browser cookie limit; any overflow is noted with a count and a pointer to the server logs.
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> SeedCompany(int companyId)
|
|
{
|
|
try
|
|
{
|
|
var result = await _seedDataService.SeedCompanyDataAsync(companyId);
|
|
|
|
if (result.Success)
|
|
{
|
|
TempData["SuccessMessage"] = result.Message;
|
|
TempData["SeedDetails"] = string.Join("|", result.Details);
|
|
TempData["ItemsSeeded"] = result.ItemsSeeded;
|
|
|
|
if (result.Warnings.Any())
|
|
{
|
|
TempData["WarningMessage"] = $"{result.ItemsSkipped} item(s) were skipped";
|
|
// Cap at 30 warnings to keep the TempData cookie under the 4 KB browser limit
|
|
var displayWarnings = result.Warnings.Take(30).ToList();
|
|
if (result.Warnings.Count > 30)
|
|
displayWarnings.Add($"… and {result.Warnings.Count - 30} more (see logs)");
|
|
TempData["SeedWarnings"] = string.Join("|", displayWarnings);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TempData["ErrorMessage"] = result.Message;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error seeding company data");
|
|
TempData["ErrorMessage"] = $"An error occurred: {ex.Message}";
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wipes all seeded data from the DEMO company and immediately re-seeds it with fresh demo data
|
|
/// so all dates are current. Intended for tutorial recording resets — one click returns the demo
|
|
/// company to a clean, realistic state without touching any other tenant.
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> ResetDemoCompany()
|
|
{
|
|
try
|
|
{
|
|
var companies = await _seedDataService.GetCompaniesAsync();
|
|
var demo = companies.FirstOrDefault(c => c.CompanyCode == "DEMO");
|
|
if (demo == null)
|
|
{
|
|
TempData["ErrorMessage"] = "Demo company (code: DEMO) not found. Run Seed System Data first.";
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
// Full wipe — ForceRemoveAll bypasses fingerprint matching so stale seed data from
|
|
// previous code versions (different emails, renamed SKUs, etc.) is always cleared.
|
|
var removeOptions = new RemoveSeedDataOptions
|
|
{
|
|
Customers = true,
|
|
InventoryItems = true,
|
|
Equipment = true,
|
|
Catalog = true,
|
|
PricingTiers = true,
|
|
OperatingCosts = true,
|
|
Bills = true,
|
|
Expenses = true,
|
|
Workers = false, // workers stay static — never deleted on reset
|
|
Vendors = true,
|
|
NamedOvens = true,
|
|
Appointments = true,
|
|
ForceRemoveAll = true,
|
|
};
|
|
|
|
var removeResult = await _seedDataService.RemoveSeedDataAsync(demo.Id, removeOptions);
|
|
if (!removeResult.Success)
|
|
{
|
|
TempData["ErrorMessage"] = $"Wipe step failed: {removeResult.Message}";
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
// Re-seed with today's dates
|
|
var seedResult = await _seedDataService.SeedCompanyDataAsync(demo.Id);
|
|
|
|
if (seedResult.Success)
|
|
{
|
|
TempData["SuccessMessage"] = $"Demo company reset complete. {seedResult.ItemsSeeded} records re-seeded with today's dates.";
|
|
TempData["SeedDetails"] = string.Join("|", seedResult.Details);
|
|
TempData["ItemsSeeded"] = seedResult.ItemsSeeded;
|
|
|
|
if (seedResult.Warnings.Any())
|
|
{
|
|
TempData["WarningMessage"] = $"{seedResult.ItemsSkipped} item(s) were skipped";
|
|
var displayWarnings = seedResult.Warnings.Take(30).ToList();
|
|
if (seedResult.Warnings.Count > 30)
|
|
displayWarnings.Add($"... and {seedResult.Warnings.Count - 30} more (see logs)");
|
|
TempData["SeedWarnings"] = string.Join("|", displayWarnings);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TempData["ErrorMessage"] = $"Wipe succeeded but re-seed failed: {seedResult.Message}";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error resetting demo company");
|
|
TempData["ErrorMessage"] = $"An error occurred during demo reset: {ex.Message}";
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes previously seeded demo data from a company according to the supplied options (e.g., jobs only, or all data). Used during QA/demo resets to return a company to a clean state without a full database drop.
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<IActionResult> RemoveSeedData(int companyId, RemoveSeedDataOptions options)
|
|
{
|
|
try
|
|
{
|
|
var result = await _seedDataService.RemoveSeedDataAsync(companyId, options);
|
|
|
|
if (result.Success)
|
|
{
|
|
TempData["SuccessMessage"] = result.Message;
|
|
TempData["SeedDetails"] = string.Join("|", result.Details);
|
|
TempData["ItemsSeeded"] = result.ItemsSeeded;
|
|
}
|
|
else
|
|
{
|
|
TempData["ErrorMessage"] = result.Message;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error removing seed data for company {CompanyId}", companyId);
|
|
TempData["ErrorMessage"] = $"An error occurred: {ex.Message}";
|
|
}
|
|
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
}
|