Phase 2: Eliminate ApplicationDbContext from domain controllers
Migrated InvoicesController, QuotesController, JobsController, BillsController, PurchaseOrdersController, and CustomersController to route all data access through IUnitOfWork typed/generic repositories instead of injecting ApplicationDbContext directly. New typed repositories added: IJobRepository (GetScheduledJobsForDateAsync, GetActiveJobsForMobileAsync, LoadForCostingAsync), INotificationLogRepository (GetLatestForJobAsync, GetAllForJobAsync), IQuoteRepository (GetItemsWithCoatsAsync with CatalogItem eager load + AsNoTracking), and IJobRepository.GetOrphanedConversionJobAsync. All EF complex include chains relocated into repository methods; controllers now call named query methods rather than composing raw IQueryable chains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,9 @@ public interface IUnitOfWork : IDisposable
|
||||
IRepository<JobDailyPriority> JobDailyPriorities { get; }
|
||||
IRepository<JobItem> JobItems { get; }
|
||||
IRepository<JobItemCoat> JobItemCoats { get; }
|
||||
IRepository<JobItemPrepService> JobItemPrepServices { get; }
|
||||
IRepository<JobChangeHistory> JobChangeHistories { get; }
|
||||
IRepository<JobPrepService> JobPrepServices { get; }
|
||||
IQuoteRepository Quotes { get; }
|
||||
IRepository<QuotePhoto> QuotePhotos { get; }
|
||||
IRepository<QuoteItem> QuoteItems { get; }
|
||||
@@ -87,8 +89,8 @@ public interface IUnitOfWork : IDisposable
|
||||
IRepository<BillPayment> BillPayments { get; }
|
||||
IRepository<Expense> Expenses { get; }
|
||||
|
||||
// Notifications
|
||||
IRepository<NotificationLog> NotificationLogs { get; }
|
||||
// Notifications — typed repository for IgnoreQueryFilters-based history lookups
|
||||
INotificationLogRepository NotificationLogs { get; }
|
||||
IRepository<NotificationTemplate> NotificationTemplates { get; }
|
||||
|
||||
// Subscription
|
||||
|
||||
@@ -20,4 +20,23 @@ public interface IBillRepository : IRepository<Bill>
|
||||
/// navigations since those are read-only after the bill is opened.
|
||||
/// </summary>
|
||||
Task<Bill?> LoadForEditAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all bills for the Index/AP ledger view filtered by status and/or search term.
|
||||
/// Includes Vendor so the list row can display vendor name without a second round trip.
|
||||
/// LineItems are included for the search-in-description condition only.
|
||||
/// </summary>
|
||||
Task<List<Bill>> GetForIndexAsync(string? statusFilter, string? searchTerm, decimal? searchAmount);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last bill number with the given prefix (including soft-deleted records) for
|
||||
/// sequential number generation. Uses IgnoreQueryFilters so deleted bills are counted.
|
||||
/// </summary>
|
||||
Task<string?> GetLastBillNumberAsync(string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last payment number with the given prefix (including soft-deleted records)
|
||||
/// for sequential payment reference generation.
|
||||
/// </summary>
|
||||
Task<string?> GetLastPaymentNumberAsync(string prefix);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces.Repositories;
|
||||
|
||||
@@ -30,4 +31,22 @@ public interface IInvoiceRepository : IRepository<Invoice>
|
||||
/// Returns null if the token does not match any invoice.
|
||||
/// </summary>
|
||||
Task<Invoice?> GetByPaymentTokenAsync(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last invoice number that starts with <paramref name="prefix"/> for the given
|
||||
/// company (including soft-deleted invoices) for sequential number generation.
|
||||
/// </summary>
|
||||
Task<string?> GetLastInvoiceNumberByPrefixAsync(int companyId, string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-deleted invoices that have at least one online payment for the given company
|
||||
/// and date window, with Customer navigation loaded. Used by the Online Payments reconciliation view.
|
||||
/// </summary>
|
||||
Task<List<Invoice>> GetOnlineInvoicesForPeriodAsync(int companyId, DateTime from, DateTime to);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-deleted CreditDebitCard refunds for the given company and date window,
|
||||
/// with Invoice→Customer navigation loaded. Used by the Online Payments reconciliation view.
|
||||
/// </summary>
|
||||
Task<List<Refund>> GetOnlineRefundsForPeriodAsync(int companyId, DateTime from, DateTime to);
|
||||
}
|
||||
|
||||
@@ -42,4 +42,40 @@ public interface IJobRepository : IRepository<Job>
|
||||
/// loaded. Used by the Details view changelog tab.
|
||||
/// </summary>
|
||||
Task<List<JobChangeHistory>> GetChangeHistoryAsync(int jobId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last job number that starts with <paramref name="prefix"/> for the given
|
||||
/// company (including soft-deleted jobs) for sequential number generation.
|
||||
/// </summary>
|
||||
Task<string?> GetLastJobNumberByPrefixAsync(int companyId, string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a job created from <paramref name="quoteId"/> that may be incomplete (items not
|
||||
/// saved). Ignores query filters so it catches soft-deleted leftover rows from a previous
|
||||
/// failed conversion attempt. Used for orphan cleanup before retrying conversion.
|
||||
/// </summary>
|
||||
Task<Job?> GetOrphanedConversionJobAsync(int quoteId, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Loads all jobs scheduled for <paramref name="date"/> that are not in a terminal status,
|
||||
/// with Customer, JobStatus, JobPriority, AssignedUser, and JobItems (with Coats) navigations.
|
||||
/// Optionally filtered to a single worker when <paramref name="userId"/> is supplied.
|
||||
/// Used by the ShopDisplay (TV board) action.
|
||||
/// </summary>
|
||||
Task<List<Job>> GetScheduledJobsForDateAsync(DateTime date, string? userId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Loads all active (non-terminal, non-hold, non-cancelled) jobs for a company's shop mobile
|
||||
/// view, with Customer, JobStatus, JobPriority, AssignedUser, and JobItems (with Coats).
|
||||
/// Optionally filtered to a single worker when <paramref name="workerId"/> is supplied.
|
||||
/// </summary>
|
||||
Task<List<Job>> GetActiveJobsForMobileAsync(int companyId, string? workerId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a single job with the navigations required by the costing breakdown endpoint:
|
||||
/// OvenCost, Invoice, JobItems (with Coats), and TimeEntries (with Worker).
|
||||
/// Scoped to <paramref name="companyId"/> as an extra safety check.
|
||||
/// Returns null if not found.
|
||||
/// </summary>
|
||||
Task<Job?> LoadForCostingAsync(int jobId, int companyId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Typed repository for <see cref="NotificationLog"/> that adds IgnoreQueryFilters-based lookups
|
||||
/// by entity FK (InvoiceId, QuoteId, JobId) on top of the generic CRUD interface.
|
||||
/// All methods bypass soft-delete and tenant filters so notification history is always visible
|
||||
/// regardless of whether the linked entity has been soft-deleted.
|
||||
/// </summary>
|
||||
public interface INotificationLogRepository : IRepository<NotificationLog>
|
||||
{
|
||||
/// <summary>Returns the most recent notification log entry for the given invoice, or null.</summary>
|
||||
Task<NotificationLog?> GetLatestForInvoiceAsync(int invoiceId);
|
||||
|
||||
/// <summary>Returns all notification log entries for the given invoice, newest-first.</summary>
|
||||
Task<List<NotificationLog>> GetAllForInvoiceAsync(int invoiceId);
|
||||
|
||||
/// <summary>Returns the most recent notification log entry for the given quote, or null.</summary>
|
||||
Task<NotificationLog?> GetLatestForQuoteAsync(int quoteId);
|
||||
|
||||
/// <summary>Returns all notification log entries for the given quote, newest-first.</summary>
|
||||
Task<List<NotificationLog>> GetAllForQuoteAsync(int quoteId);
|
||||
|
||||
/// <summary>Returns the most recent notification log entry for the given job, or null.</summary>
|
||||
Task<NotificationLog?> GetLatestForJobAsync(int jobId);
|
||||
|
||||
/// <summary>Returns all notification log entries for the given job, newest-first.</summary>
|
||||
Task<List<NotificationLog>> GetAllForJobAsync(int jobId);
|
||||
}
|
||||
@@ -3,6 +3,9 @@ using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces.Repositories;
|
||||
|
||||
/// <summary>Lightweight projection used by the Index KPI cards — avoids loading full entities for aggregate stats.</summary>
|
||||
public record PurchaseOrderStats(int TotalCount, int OpenCount, decimal CommittedValue, int OverdueCount);
|
||||
|
||||
/// <summary>
|
||||
/// Typed repository for <see cref="PurchaseOrder"/> that adds domain-specific queries on top of
|
||||
/// the generic CRUD interface.
|
||||
@@ -16,6 +19,12 @@ public interface IPurchaseOrderRepository : IRepository<PurchaseOrder>
|
||||
/// </summary>
|
||||
Task<PurchaseOrder?> LoadForViewAsync(int id, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns KPI aggregate stats for the Index view using a server-side projection so only three
|
||||
/// columns are fetched rather than full entities.
|
||||
/// </summary>
|
||||
Task<PurchaseOrderStats> GetStatsAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a paged, filtered, and sorted list of purchase orders for the Index view.
|
||||
/// All filter parameters are optional — passing null/empty applies no restriction for
|
||||
|
||||
@@ -2,6 +2,9 @@ using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Core.Interfaces.Repositories;
|
||||
|
||||
/// <summary>Aggregate counts and totals for the Quotes Index stat cards.</summary>
|
||||
public record QuoteIndexStats(int OpenCount, int ApprovedConvertedCount, decimal TotalValue);
|
||||
|
||||
/// <summary>
|
||||
/// Typed repository for <see cref="Quote"/> that adds domain-specific queries on top of
|
||||
/// the generic CRUD interface.
|
||||
@@ -27,4 +30,24 @@ public interface IQuoteRepository : IRepository<Quote>
|
||||
/// Returns the change history for a quote, ordered newest-first, with ChangedBy loaded.
|
||||
/// </summary>
|
||||
Task<List<QuoteChangeHistory>> GetChangeHistoryAsync(int quoteId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns aggregate stat counts and total value for the Index view stat cards, scoped to the
|
||||
/// current company by the global query filter. Pass status ID sets (derived from QuoteStatusLookup)
|
||||
/// to classify open vs. approved/converted quotes.
|
||||
/// </summary>
|
||||
Task<QuoteIndexStats> GetIndexStatsAsync(List<int> openStatusIds, List<int> approvedConvertedStatusIds);
|
||||
|
||||
/// <summary>
|
||||
/// Loads quote items with their coat passes (InventoryItem + Vendor) and prep services for
|
||||
/// PDF generation and quote→job conversion. Cheaper than <see cref="LoadForDetailsAsync"/>
|
||||
/// because it skips the parent-quote navigations that callers already have.
|
||||
/// </summary>
|
||||
Task<List<QuoteItem>> GetItemsWithCoatsAsync(int quoteId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last quote number that starts with <paramref name="prefix"/> for the given
|
||||
/// company (including soft-deleted quotes) for sequential number generation.
|
||||
/// </summary>
|
||||
Task<string?> GetLastQuoteNumberByPrefixAsync(int companyId, string prefix);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user