Demo data realism + invoice resend via SMS on any status
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>
This commit is contained in:
+31
-18
@@ -54,17 +54,25 @@ public partial class SeedDataService
|
||||
|
||||
if (items.Count == 0) return 0;
|
||||
|
||||
// Load completed/delivered jobs to generate usage transactions against
|
||||
var completedJobs = await _context.Set<Job>()
|
||||
// Two-query approach: resolve status IDs first to avoid Include() navigation
|
||||
// returning null when global query filters interact with IgnoreQueryFilters().
|
||||
var completedStatusIds = await _context.Set<JobStatusLookup>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == company.Id && !j.IsDeleted
|
||||
&& (j.JobStatus.StatusCode == "COMPLETED"
|
||||
|| j.JobStatus.StatusCode == "READY_FOR_PICKUP"
|
||||
|| j.JobStatus.StatusCode == "DELIVERED"))
|
||||
.Include(j => j.JobStatus)
|
||||
.OrderBy(j => j.Id)
|
||||
.Where(s => s.CompanyId == company.Id
|
||||
&& new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" }.Contains(s.StatusCode))
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var completedJobs = completedStatusIds.Count == 0
|
||||
? new List<Job>()
|
||||
: await _context.Set<Job>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == company.Id && !j.IsDeleted
|
||||
&& completedStatusIds.Contains(j.JobStatusId))
|
||||
.Include(j => j.JobItems)
|
||||
.OrderBy(j => j.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var seeded = 0;
|
||||
var txns = new List<InventoryTransaction>();
|
||||
@@ -90,14 +98,18 @@ public partial class SeedDataService
|
||||
});
|
||||
}
|
||||
|
||||
// ── Purchase transactions — 3 months of restocks ──────────────────────
|
||||
// Simulate monthly powder purchases for top items
|
||||
var powderItems = items.Take(8).ToList(); // focus on powder coat items
|
||||
|
||||
foreach (var (offset, qtyMult) in new (int daysAgo, decimal mult)[] {
|
||||
(85, 1.2m), (55, 1.0m), (25, 0.9m) })
|
||||
// ── Purchase transactions — 12 months of monthly restocks ────────────
|
||||
var powderItems = items.Take(8).ToList();
|
||||
// Quantities vary slightly month to month to give the inventory chart a natural shape
|
||||
var purchaseOffsets = new (int daysAgo, decimal mult)[]
|
||||
{
|
||||
foreach (var item in powderItems.Take(4)) // 4 items per purchase cycle
|
||||
(365, 0.80m), (335, 0.85m), (305, 0.90m), (275, 0.95m),
|
||||
(245, 1.00m), (215, 1.05m), (185, 1.10m), (155, 1.10m),
|
||||
(125, 1.00m), ( 95, 1.10m), ( 65, 1.00m), ( 35, 0.95m)
|
||||
};
|
||||
foreach (var (offset, qtyMult) in purchaseOffsets)
|
||||
{
|
||||
foreach (var item in powderItems.Take(4))
|
||||
{
|
||||
var qty = Math.Round(25m * qtyMult, 0);
|
||||
txns.Add(new InventoryTransaction
|
||||
@@ -144,9 +156,10 @@ public partial class SeedDataService
|
||||
|
||||
foreach (var (job, idx) in completedJobs.Select((j, i) => (j, i)))
|
||||
{
|
||||
// Completed date spread: most within the last 60 days
|
||||
var daysAgo = 10 + (idx % 55);
|
||||
var usageDate = now.AddDays(-daysAgo);
|
||||
// Use the job's actual completion date so powder usage history spans the same
|
||||
// 12-month window as jobs, giving the Powder Usage report non-trivial data in
|
||||
// every month rather than clustering everything in the last 60 days.
|
||||
var usageDate = (job.CompletedDate ?? job.ScheduledDate ?? now.AddDays(-30)).Date;
|
||||
|
||||
// Pick a color-matched powder item (or rotate)
|
||||
var firstItem = job.JobItems?.FirstOrDefault();
|
||||
|
||||
Reference in New Issue
Block a user