Refactor: centralize accounting helpers, status constants, and query deduplication
- AccountingDropdownHelper: wired into BillsController and ExpensesController, replacing 35-40 lines of duplicated DB queries per controller - AppConstants.StatusCodes: added Job.* and Quote.* constants to replace all magic status strings across Jobs, Quotes, Appointments, OvenScheduler, AiQuickQuote, QuoteApproval, and AccountingDropdownHelper - AccountingRules: extracted IsNormalDebitBalance into shared Infrastructure helper; removed duplicate private method from AccountBalanceService and LedgerService (~50 lines deleted) - AccountDataExportController: extracted 9 Fetch*Async methods (superset of includes) so Add*Sheet and Build*Csv no longer duplicate DB queries; each entity is queried once regardless of whether XLSX or CSV format is requested - BillsController.Create and ExpensesController.Create wrapped in ExecuteInTransactionAsync; blob uploads moved after commit to keep financial data atomic and prevent orphaned blobs from rolling back - Number generators (Appointments, CreditMemo, OvenBatch) fixed from full-table GetAllAsync to prefix-filtered FindAsync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OfficeOpenXml;
|
||||
using OfficeOpenXml.Style;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using System.Drawing;
|
||||
@@ -207,6 +208,81 @@ public class AccountDataExportController : Controller
|
||||
writer.Write(content);
|
||||
}
|
||||
|
||||
// ── Data fetchers (single query per entity, superset of includes for both XLSX + CSV) ─────
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all non-deleted customers for the company, including <c>PricingTier</c> (needed by
|
||||
/// the CSV path; harmlessly unused by the XLSX path). Bypasses the global tenant filter via
|
||||
/// <c>IgnoreQueryFilters</c> because <c>ITenantContext</c> may be null for expired accounts.
|
||||
/// </summary>
|
||||
private Task<List<Customer>> FetchCustomersAsync(int companyId) =>
|
||||
_db.Customers.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(c => c.PricingTier)
|
||||
.Where(c => c.CompanyId == companyId && !c.IsDeleted)
|
||||
.OrderBy(c => c.CompanyName).ToListAsync();
|
||||
|
||||
/// <summary>Fetches all non-deleted jobs with Customer, JobStatus, and JobPriority included.</summary>
|
||||
private Task<List<Job>> FetchJobsAsync(int companyId) =>
|
||||
_db.Jobs.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(j => j.Customer).Include(j => j.JobStatus).Include(j => j.JobPriority)
|
||||
.Where(j => j.CompanyId == companyId && !j.IsDeleted)
|
||||
.OrderByDescending(j => j.CreatedAt).ToListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all non-deleted quotes with Customer and QuoteStatus included.
|
||||
/// The XLSX path only needs QuoteStatus; the CSV path also uses Customer — the superset here
|
||||
/// avoids a second query when both formats include this sheet in the same request.
|
||||
/// </summary>
|
||||
private Task<List<Quote>> FetchQuotesAsync(int companyId) =>
|
||||
_db.Quotes.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(q => q.Customer).Include(q => q.QuoteStatus)
|
||||
.Where(q => q.CompanyId == companyId && !q.IsDeleted)
|
||||
.OrderByDescending(q => q.QuoteDate).ToListAsync();
|
||||
|
||||
/// <summary>Fetches all non-deleted invoices with Customer included.</summary>
|
||||
private Task<List<Invoice>> FetchInvoicesAsync(int companyId) =>
|
||||
_db.Invoices.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(i => i.Customer)
|
||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted)
|
||||
.OrderByDescending(i => i.InvoiceDate).ToListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all non-deleted inventory items with PrimaryVendor and InventoryCategory included.
|
||||
/// Only the CSV path uses these navigations; XLSX reads only scalar fields but the join is cheap.
|
||||
/// </summary>
|
||||
private Task<List<InventoryItem>> FetchInventoryAsync(int companyId) =>
|
||||
_db.InventoryItems.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(i => i.PrimaryVendor).Include(i => i.InventoryCategory)
|
||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted)
|
||||
.OrderBy(i => i.Name).ToListAsync();
|
||||
|
||||
/// <summary>Fetches all non-deleted equipment records for the company.</summary>
|
||||
private Task<List<Equipment>> FetchEquipmentAsync(int companyId) =>
|
||||
_db.Equipment.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(e => e.CompanyId == companyId && !e.IsDeleted)
|
||||
.OrderBy(e => e.EquipmentName).ToListAsync();
|
||||
|
||||
/// <summary>Fetches all non-deleted vendors for the company.</summary>
|
||||
private Task<List<Vendor>> FetchVendorsAsync(int companyId) =>
|
||||
_db.Vendors.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(s => s.CompanyId == companyId && !s.IsDeleted)
|
||||
.OrderBy(s => s.CompanyName).ToListAsync();
|
||||
|
||||
/// <summary>Fetches all non-deleted shop workers for the company.</summary>
|
||||
private Task<List<ShopWorker>> FetchShopWorkersAsync(int companyId) =>
|
||||
_db.ShopWorkers.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(w => w.CompanyId == companyId && !w.IsDeleted)
|
||||
.OrderBy(w => w.Name).ToListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all users for the company. <c>IsDeleted</c> is intentionally omitted because
|
||||
/// Identity users use <c>IsActive = false</c> for soft-deletion, not the base-entity flag.
|
||||
/// </summary>
|
||||
private Task<List<ApplicationUser>> FetchUsersAsync(int companyId) =>
|
||||
_db.Users.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(u => u.CompanyId == companyId)
|
||||
.OrderBy(u => u.LastName).ToListAsync();
|
||||
|
||||
// ── Sheet builders ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -226,16 +302,9 @@ public class AccountDataExportController : Controller
|
||||
ws.Cells[1, 1, 3, 1].Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "Customers" worksheet with one row per non-deleted customer belonging to the
|
||||
/// authenticated user's company. <c>IgnoreQueryFilters()</c> bypasses the global EF
|
||||
/// multi-tenancy filter (which relies on <c>ITenantContext</c>) in favour of the explicit
|
||||
/// <c>CompanyId == companyId</c> predicate, making the filter independent of middleware state.
|
||||
/// </summary>
|
||||
private async Task AddCustomersSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Customers.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(c => c.CompanyId == companyId && !c.IsDeleted).OrderBy(c => c.CompanyName).ToListAsync();
|
||||
var data = await FetchCustomersAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Customers");
|
||||
var headers = new[] { "ID", "Company Name", "First Name", "Last Name", "Email", "Phone",
|
||||
"Commercial", "City", "State", "Active", "Credit Limit", "Current Balance", "Created At" };
|
||||
@@ -256,18 +325,13 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "Jobs" worksheet with one row per non-deleted job belonging to the company.
|
||||
/// Job status and priority are lookup-table entities (not enums) stored in
|
||||
/// <c>JobStatusLookup</c> and a parallel priority table; they are eagerly loaded so their
|
||||
/// <c>DisplayName</c> property is available without additional queries.
|
||||
/// If a lookup navigation is null (data anomaly), the raw FK integer is written as a fallback.
|
||||
/// Adds a "Jobs" worksheet. Job status and priority are lookup-table entities (not enums);
|
||||
/// they are eagerly loaded by <see cref="FetchJobsAsync"/> so their <c>DisplayName</c> is
|
||||
/// available without N+1 queries. Falls back to the raw FK integer on data anomalies.
|
||||
/// </summary>
|
||||
private async Task AddJobsSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Jobs.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == companyId && !j.IsDeleted)
|
||||
.Include(j => j.Customer).Include(j => j.JobStatus).Include(j => j.JobPriority)
|
||||
.OrderByDescending(j => j.CreatedAt).ToListAsync();
|
||||
var data = await FetchJobsAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Jobs");
|
||||
var headers = new[] { "ID", "Job Number", "Customer", "Status", "Priority",
|
||||
"Description", "Due Date", "Final Price", "Created At" };
|
||||
@@ -289,16 +353,12 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "Quotes" worksheet with one row per non-deleted quote belonging to the company.
|
||||
/// Prospect-only quotes (before they are linked to a customer record) show
|
||||
/// <c>ProspectCompanyName</c>; fully linked quotes fall back to the customer FK integer when
|
||||
/// the navigation cannot be resolved — ensuring no row has a blank identifier column.
|
||||
/// Adds a "Quotes" worksheet. Prospect-only quotes show <c>ProspectCompanyName</c>;
|
||||
/// fully linked quotes fall back to <c>Customer #{id}</c> when the navigation is null.
|
||||
/// </summary>
|
||||
private async Task AddQuotesSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Quotes.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(q => q.CompanyId == companyId && !q.IsDeleted)
|
||||
.Include(q => q.QuoteStatus).OrderByDescending(q => q.QuoteDate).ToListAsync();
|
||||
var data = await FetchQuotesAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Quotes");
|
||||
var headers = new[] { "ID", "Quote Number", "Customer / Prospect", "Status",
|
||||
"Quote Date", "Expiration Date", "Subtotal", "Tax", "Total" };
|
||||
@@ -317,16 +377,12 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an "Invoices" worksheet with one row per non-deleted invoice belonging to the company.
|
||||
/// <c>BalanceDue</c> is a computed property (<c>Total - AmountPaid</c>) reflecting partial
|
||||
/// payment state without an additional aggregation query.
|
||||
/// Eagerly loads <c>Customer</c> so the customer name is available for the display column.
|
||||
/// Adds an "Invoices" worksheet. <c>BalanceDue</c> is a computed property on the entity
|
||||
/// (<c>Total - AmountPaid</c>) so no extra aggregation query is needed.
|
||||
/// </summary>
|
||||
private async Task AddInvoicesSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Invoices.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted)
|
||||
.Include(i => i.Customer).OrderByDescending(i => i.InvoiceDate).ToListAsync();
|
||||
var data = await FetchInvoicesAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Invoices");
|
||||
var headers = new[] { "ID", "Invoice #", "Customer", "Status", "Invoice Date",
|
||||
"Due Date", "Subtotal", "Tax", "Total", "Amount Paid", "Balance Due" };
|
||||
@@ -348,15 +404,9 @@ public class AccountDataExportController : Controller
|
||||
AutoFit(ws, headers.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an "Inventory" worksheet with one row per non-deleted inventory item for the company.
|
||||
/// Items are ordered alphabetically so the exported list matches the order users typically
|
||||
/// see in the application's inventory index view.
|
||||
/// </summary>
|
||||
private async Task AddInventorySheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.InventoryItems.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted).OrderBy(i => i.Name).ToListAsync();
|
||||
var data = await FetchInventoryAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Inventory");
|
||||
var headers = new[] { "ID", "Name", "SKU", "Category", "Qty on Hand",
|
||||
"Unit", "Unit Cost", "Reorder Point", "Manufacturer", "Color" };
|
||||
@@ -373,14 +423,9 @@ public class AccountDataExportController : Controller
|
||||
AutoFit(ws, headers.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an "Equipment" worksheet with one row per non-deleted equipment record for the company.
|
||||
/// Equipment status is stored as an enum and serialised via <c>ToString()</c> for a readable label.
|
||||
/// </summary>
|
||||
private async Task AddEquipmentSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Equipment.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(e => e.CompanyId == companyId && !e.IsDeleted).OrderBy(e => e.EquipmentName).ToListAsync();
|
||||
var data = await FetchEquipmentAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Equipment");
|
||||
var headers = new[] { "ID", "Name", "Type", "Serial Number", "Model",
|
||||
"Status", "Purchase Date", "Purchase Price", "Next Maintenance" };
|
||||
@@ -398,13 +443,9 @@ public class AccountDataExportController : Controller
|
||||
AutoFit(ws, headers.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "Vendors" worksheet with one row per non-deleted vendor for the company.
|
||||
/// </summary>
|
||||
private async Task AddVendorsSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Vendors.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(s => s.CompanyId == companyId && !s.IsDeleted).OrderBy(s => s.CompanyName).ToListAsync();
|
||||
var data = await FetchVendorsAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Vendors");
|
||||
var headers = new[] { "ID", "Company Name", "Contact", "Email", "Phone", "City", "State", "Preferred", "Active" };
|
||||
WriteHeader(ws, headers, hdr);
|
||||
@@ -421,13 +462,9 @@ public class AccountDataExportController : Controller
|
||||
AutoFit(ws, headers.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "Shop Workers" worksheet with one row per non-deleted shop worker for the company.
|
||||
/// </summary>
|
||||
private async Task AddShopWorkersSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.ShopWorkers.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(w => w.CompanyId == companyId && !w.IsDeleted).OrderBy(w => w.Name).ToListAsync();
|
||||
var data = await FetchShopWorkersAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Shop Workers");
|
||||
var headers = new[] { "ID", "Name", "Role", "Phone", "Email", "Active", "Notes" };
|
||||
WriteHeader(ws, headers, hdr);
|
||||
@@ -443,16 +480,12 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a "Users" worksheet with one row per user belonging to the company.
|
||||
/// The <c>IsDeleted</c> predicate is intentionally omitted because ASP.NET Identity users
|
||||
/// use <c>IsActive = false</c> as their soft-deletion mechanism, not the base-entity
|
||||
/// <c>IsDeleted</c> flag. All users (active and inactive) are included so the export
|
||||
/// provides a complete workforce record for compliance and audit purposes.
|
||||
/// Adds a "Users" worksheet. All users (active and inactive) are included because Identity
|
||||
/// uses <c>IsActive = false</c> for soft-deletion; <c>IsDeleted</c> is not applicable here.
|
||||
/// </summary>
|
||||
private async Task AddUsersSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||
{
|
||||
var data = await _db.Users.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(u => u.CompanyId == companyId).OrderBy(u => u.LastName).ToListAsync();
|
||||
var data = await FetchUsersAsync(companyId);
|
||||
var ws = pkg.Workbook.Worksheets.Add("Users");
|
||||
var headers = new[] { "ID", "First Name", "Last Name", "Email", "Role", "Active", "Hire Date", "Last Login", "Created At" };
|
||||
WriteHeader(ws, headers, hdr);
|
||||
@@ -472,15 +505,12 @@ public class AccountDataExportController : Controller
|
||||
// ── CSV builders ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds the customers CSV string for the company.
|
||||
/// Column names match <see cref="CustomerImportDto"/> exactly so the file can be re-imported
|
||||
/// Column names match <c>CustomerImportDto</c> exactly so the file can be re-imported
|
||||
/// via Tools → Bulk Import without any manual header editing.
|
||||
/// </summary>
|
||||
private async Task<string> BuildCustomersCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Customers.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(c => c.PricingTier)
|
||||
.Where(c => c.CompanyId == companyId && !c.IsDeleted).OrderBy(c => c.CompanyName).ToListAsync();
|
||||
var data = await FetchCustomersAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("CompanyName,ContactFirstName,ContactLastName,Email,Phone,MobilePhone,Address,City,State,ZipCode,Country,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,TaxId,IsActive,Notes");
|
||||
foreach (var c in data)
|
||||
@@ -492,16 +522,12 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the jobs CSV string for the company.
|
||||
/// Column names match <see cref="JobImportDto"/> exactly so the file can be re-imported.
|
||||
/// CustomerEmail is included (not the display name) because the importer resolves the customer FK by email.
|
||||
/// Column names match <c>JobImportDto</c> exactly so the file can be re-imported.
|
||||
/// CustomerEmail is used (not display name) because the importer resolves the customer FK by email.
|
||||
/// </summary>
|
||||
private async Task<string> BuildJobsCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Jobs.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(j => j.CompanyId == companyId && !j.IsDeleted)
|
||||
.Include(j => j.Customer).Include(j => j.JobStatus).Include(j => j.JobPriority)
|
||||
.OrderByDescending(j => j.CreatedAt).ToListAsync();
|
||||
var data = await FetchJobsAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,FinalPrice,CustomerPO,SpecialInstructions,Notes");
|
||||
foreach (var j in data)
|
||||
@@ -514,15 +540,10 @@ public class AccountDataExportController : Controller
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the quotes CSV string for the company.
|
||||
/// Column names match <see cref="QuoteImportDto"/> exactly so the file can be re-imported.
|
||||
/// </summary>
|
||||
/// <summary>Column names match <c>QuoteImportDto</c> exactly so the file can be re-imported.</summary>
|
||||
private async Task<string> BuildQuotesCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Quotes.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(q => q.CompanyId == companyId && !q.IsDeleted)
|
||||
.Include(q => q.Customer).Include(q => q.QuoteStatus).OrderByDescending(q => q.QuoteDate).ToListAsync();
|
||||
var data = await FetchQuotesAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");
|
||||
foreach (var q in data)
|
||||
@@ -536,15 +557,12 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the invoices CSV string for the company, ordered newest-first.
|
||||
/// Customer name resolution mirrors the XLSX sheet: company name preferred, with
|
||||
/// first+last name concatenation as the fallback for non-commercial customers.
|
||||
/// </summary>
|
||||
private async Task<string> BuildInvoicesCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Invoices.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted)
|
||||
.Include(i => i.Customer).OrderByDescending(i => i.InvoiceDate).ToListAsync();
|
||||
var data = await FetchInvoicesAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("ID,Invoice #,Customer,Status,Invoice Date,Due Date,Subtotal,Tax,Total,Amount Paid,Balance Due");
|
||||
foreach (var inv in data)
|
||||
@@ -557,16 +575,10 @@ public class AccountDataExportController : Controller
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the inventory CSV string for the company.
|
||||
/// Column names match <see cref="InventoryItemImportDto"/> exactly so the file can be re-imported.
|
||||
/// </summary>
|
||||
/// <summary>Column names match <c>InventoryItemImportDto</c> exactly so the file can be re-imported.</summary>
|
||||
private async Task<string> BuildInventoryCsv(int companyId)
|
||||
{
|
||||
var data = await _db.InventoryItems.AsNoTracking().IgnoreQueryFilters()
|
||||
.Include(i => i.PrimaryVendor)
|
||||
.Include(i => i.InventoryCategory)
|
||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted).OrderBy(i => i.Name).ToListAsync();
|
||||
var data = await FetchInventoryAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("SKU,ItemName,Description,CategoryName,Manufacturer,ManufacturerPartNumber,ColorName,ColorCode,Finish,VendorName,VendorPartNumber,QuantityInStock,UnitOfMeasure,UnitCost,LastPurchasePrice,ReorderPoint,ReorderQuantity,MinimumStock,MaximumStock,CoverageSqFtPerLb,TransferEfficiencyPct,Location,IsActive,Notes");
|
||||
foreach (var i in data)
|
||||
@@ -577,14 +589,10 @@ public class AccountDataExportController : Controller
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the equipment CSV string for the company.
|
||||
/// Column names match <see cref="EquipmentImportDto"/> exactly so the file can be re-imported.
|
||||
/// </summary>
|
||||
/// <summary>Column names match <c>EquipmentImportDto</c> exactly so the file can be re-imported.</summary>
|
||||
private async Task<string> BuildEquipmentCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Equipment.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(e => e.CompanyId == companyId && !e.IsDeleted).OrderBy(e => e.EquipmentName).ToListAsync();
|
||||
var data = await FetchEquipmentAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("EquipmentName,EquipmentNumber,EquipmentType,Manufacturer,Model,SerialNumber,PurchaseDate,PurchasePrice,WarrantyExpiration,Location,RecommendedMaintenanceIntervalDays,Status,IsActive,Notes");
|
||||
foreach (var e in data)
|
||||
@@ -592,14 +600,10 @@ public class AccountDataExportController : Controller
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the vendors CSV string for the company.
|
||||
/// Column names match <see cref="VendorImportDto"/> exactly so the file can be re-imported.
|
||||
/// </summary>
|
||||
/// <summary>Column names match <c>VendorImportDto</c> exactly so the file can be re-imported.</summary>
|
||||
private async Task<string> BuildVendorsCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Vendors.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(s => s.CompanyId == companyId && !s.IsDeleted).OrderBy(s => s.CompanyName).ToListAsync();
|
||||
var data = await FetchVendorsAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,Country,Website,AccountNumber,TaxId,PaymentTerms,CreditLimit,IsPreferred,IsActive,Notes");
|
||||
foreach (var s in data)
|
||||
@@ -607,14 +611,10 @@ public class AccountDataExportController : Controller
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the shop workers CSV string for the company.
|
||||
/// Column names match <see cref="ShopWorkerImportDto"/> exactly so the file can be re-imported.
|
||||
/// </summary>
|
||||
/// <summary>Column names match <c>ShopWorkerImportDto</c> exactly so the file can be re-imported.</summary>
|
||||
private async Task<string> BuildShopWorkersCsv(int companyId)
|
||||
{
|
||||
var data = await _db.ShopWorkers.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(w => w.CompanyId == companyId && !w.IsDeleted).OrderBy(w => w.Name).ToListAsync();
|
||||
var data = await FetchShopWorkersAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Name,Role,Phone,Email,IsActive,Notes");
|
||||
foreach (var w in data)
|
||||
@@ -623,15 +623,12 @@ public class AccountDataExportController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the users CSV string for the company.
|
||||
/// Like <see cref="AddUsersSheet"/>, the <c>IsDeleted</c> predicate is omitted because
|
||||
/// Identity users use <c>IsActive</c> for soft-deletion; all users are exported for
|
||||
/// completeness and compliance.
|
||||
/// All users (active and inactive) are exported for completeness and compliance — mirrors
|
||||
/// the reasoning in <see cref="AddUsersSheet"/> and <see cref="FetchUsersAsync"/>.
|
||||
/// </summary>
|
||||
private async Task<string> BuildUsersCsv(int companyId)
|
||||
{
|
||||
var data = await _db.Users.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(u => u.CompanyId == companyId).OrderBy(u => u.LastName).ToListAsync();
|
||||
var data = await FetchUsersAsync(companyId);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("ID,First Name,Last Name,Email,Role,Active,Hire Date,Last Login,Created At");
|
||||
foreach (var u in data)
|
||||
|
||||
Reference in New Issue
Block a user