Patch export/import for missing fields; add CustomerContacts export
- DataExportController + AccountDataExportController: add ProjectName to Jobs, Quotes, Invoices (XLSX + CSV); add LeadSource + ShipTo fields to Customers (XLSX + CSV); add CustomerContacts sheet/CSV (new) - Both export views: add Customer Contacts checkbox (checked by default) - CustomerImportDto: add LeadSource + ShipTo* fields - JobImportDto: add ProjectName - QuoteImportDto: add ProjectName - InvoiceImportDto: add Project Name (dual-name alias for round-trip) - CsvImportService: wire all new import fields to entity creation; also patch invoice update path for ProjectName - Add scripts/purge_imported_data.sql (dry-run T-SQL for data cleanup) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,403 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Company Data Purge Script
|
||||||
|
-- Removes financial, job, and quote data created before a cutoff date.
|
||||||
|
-- Customers and Vendors are always preserved.
|
||||||
|
--
|
||||||
|
-- WHAT THIS DELETES (for records where CreatedAt < @CutoffDate):
|
||||||
|
-- • Journal entries & bank reconciliations
|
||||||
|
-- • Bills, bill payments, purchase orders, vendor credits, expenses
|
||||||
|
-- • Invoices, invoice payments, credit memos, refunds, deposits
|
||||||
|
-- • Jobs and all child records (items, coats, prep, notes, photos, etc.)
|
||||||
|
-- • Quotes and all child records
|
||||||
|
--
|
||||||
|
-- WHAT THIS KEEPS (always):
|
||||||
|
-- • Customers, customer notes, customer contacts, preferred powders
|
||||||
|
-- • Vendors
|
||||||
|
-- • Inventory items and inventory transactions
|
||||||
|
-- • Equipment, catalog items, pricing tiers
|
||||||
|
-- • Company settings and configuration
|
||||||
|
-- • Any record with CreatedAt >= @CutoffDate
|
||||||
|
--
|
||||||
|
-- INSTRUCTIONS:
|
||||||
|
-- 1. Set @CompanyId — find it with: SELECT Id, Name FROM Companies
|
||||||
|
-- 2. Set @CutoffDate — records created BEFORE this date are deleted
|
||||||
|
-- 3. Run with @DryRun = 1 first and review the row counts printed
|
||||||
|
-- 4. Back up the database before setting @DryRun = 0
|
||||||
|
-- 5. Set @DryRun = 0 and run again to apply
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
DECLARE @CutoffDate DATE = '2026-01-01'; -- Delete records created BEFORE this date
|
||||||
|
DECLARE @CompanyId INT = 0; -- !! Set to your company ID before running
|
||||||
|
DECLARE @DryRun BIT = 1; -- 1 = preview counts only | 0 = apply deletes
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @CompanyId = 0
|
||||||
|
BEGIN
|
||||||
|
RAISERROR('ERROR: Set @CompanyId before running this script. Run: SELECT Id, Name FROM Companies', 16, 1);
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
PRINT '============================================================';
|
||||||
|
PRINT 'Purge run at: ' + CONVERT(NVARCHAR, GETDATE(), 120);
|
||||||
|
PRINT 'Company ID : ' + CAST(@CompanyId AS NVARCHAR);
|
||||||
|
PRINT 'Cutoff date : ' + CAST(@CutoffDate AS NVARCHAR) + ' (records BEFORE this date)';
|
||||||
|
PRINT 'Dry run : ' + CASE @DryRun WHEN 1 THEN 'YES — no changes will be made' ELSE 'NO — deletes will be applied' END;
|
||||||
|
PRINT '============================================================';
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SECTION 1 — JOURNAL ENTRIES & GL
|
||||||
|
-- ===========================================================================
|
||||||
|
PRINT '';
|
||||||
|
PRINT '--- Section 1: Journal Entries & GL ---';
|
||||||
|
|
||||||
|
-- Null the self-referential ReversalOfId FK before deleting
|
||||||
|
UPDATE JournalEntries
|
||||||
|
SET ReversalOfId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND ReversalOfId IN (SELECT Id FROM JournalEntries WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
DELETE FROM JournalEntryLines
|
||||||
|
WHERE JournalEntryId IN (
|
||||||
|
SELECT Id FROM JournalEntries WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JournalEntryLines deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JournalEntries
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'JournalEntries deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM BankReconciliations
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'BankReconciliations deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SECTION 2 — BILLS, PURCHASE ORDERS & EXPENSES
|
||||||
|
-- ===========================================================================
|
||||||
|
PRINT '';
|
||||||
|
PRINT '--- Section 2: Bills, Purchase Orders & Expenses ---';
|
||||||
|
|
||||||
|
-- Vendor credits (must come before Bills because VendorCreditApplications references both)
|
||||||
|
DELETE FROM VendorCreditApplications
|
||||||
|
WHERE VendorCreditId IN (
|
||||||
|
SELECT Id FROM VendorCredits WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'VendorCreditApplications deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM VendorCreditLineItems
|
||||||
|
WHERE VendorCreditId IN (
|
||||||
|
SELECT Id FROM VendorCredits WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'VendorCreditLineItems deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM VendorCredits
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'VendorCredits deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Bills
|
||||||
|
DELETE FROM BillPayments
|
||||||
|
WHERE BillId IN (
|
||||||
|
SELECT Id FROM Bills WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'BillPayments deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM BillLineItems
|
||||||
|
WHERE BillId IN (
|
||||||
|
SELECT Id FROM Bills WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'BillLineItems deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM Bills
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'Bills deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Purchase orders
|
||||||
|
DELETE FROM PurchaseOrderItems
|
||||||
|
WHERE PurchaseOrderId IN (
|
||||||
|
SELECT Id FROM PurchaseOrders WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'PurchaseOrderItems deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM PurchaseOrders
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'PurchaseOrders deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Expenses
|
||||||
|
DELETE FROM Expenses
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'Expenses deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SECTION 3 — INVOICES, PAYMENTS & DEPOSITS
|
||||||
|
-- ===========================================================================
|
||||||
|
PRINT '';
|
||||||
|
PRINT '--- Section 3: Invoices, Payments & Deposits ---';
|
||||||
|
|
||||||
|
-- CreditMemos: NULL the OriginalInvoiceId FK before deleting the invoice it points to
|
||||||
|
UPDATE CreditMemos
|
||||||
|
SET OriginalInvoiceId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND OriginalInvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
DELETE FROM CreditMemoApplications
|
||||||
|
WHERE InvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate)
|
||||||
|
OR CreditMemoId IN (
|
||||||
|
SELECT Id FROM CreditMemos WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'CreditMemoApplications deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM CreditMemos
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'CreditMemos deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Refunds and gift-cert redemptions tied to deleted invoices
|
||||||
|
DELETE FROM Refunds
|
||||||
|
WHERE InvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'Refunds deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM GiftCertificateRedemptions
|
||||||
|
WHERE InvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'GiftCertificateRedemptions deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Payments
|
||||||
|
DELETE FROM Payments
|
||||||
|
WHERE InvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'Payments deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- InvoiceItems (NULL SourceJobItemId on any invoice items that survive but point at deleted jobs)
|
||||||
|
UPDATE InvoiceItems
|
||||||
|
SET SourceJobItemId = NULL
|
||||||
|
WHERE SourceJobItemId IN (
|
||||||
|
SELECT ji.Id FROM JobItems ji
|
||||||
|
INNER JOIN Jobs j ON ji.JobId = j.Id
|
||||||
|
WHERE j.CompanyId = @CompanyId AND j.CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
DELETE FROM InvoiceItems
|
||||||
|
WHERE InvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'InvoiceItems deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Deposits: clear the AppliedToInvoiceId FK before deleting the invoices
|
||||||
|
UPDATE Deposits
|
||||||
|
SET AppliedToInvoiceId = NULL,
|
||||||
|
AppliedDate = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND AppliedToInvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- Now delete deposits that were created before the cutoff
|
||||||
|
DELETE FROM Deposits
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'Deposits deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Notification logs referencing deleted invoices
|
||||||
|
UPDATE NotificationLogs
|
||||||
|
SET InvoiceId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND InvoiceId IN (
|
||||||
|
SELECT Id FROM Invoices WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
DELETE FROM Invoices
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'Invoices deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SECTION 4 — JOBS
|
||||||
|
-- ===========================================================================
|
||||||
|
PRINT '';
|
||||||
|
PRINT '--- Section 4: Jobs ---';
|
||||||
|
|
||||||
|
-- NULL FKs in other tables that point to jobs/job-items being deleted --
|
||||||
|
|
||||||
|
-- BillLineItems.JobId (bill survived but referenced a deleted job)
|
||||||
|
UPDATE BillLineItems
|
||||||
|
SET JobId = NULL
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- Expenses.JobId
|
||||||
|
UPDATE Expenses
|
||||||
|
SET JobId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- Appointments.JobId
|
||||||
|
UPDATE Appointments
|
||||||
|
SET JobId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- Deposits.JobId (NoAction FK — must NULL before deleting job)
|
||||||
|
UPDATE Deposits
|
||||||
|
SET JobId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- NotificationLogs.JobId
|
||||||
|
UPDATE NotificationLogs
|
||||||
|
SET JobId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- OvenBatchItems: delete before OvenBatches and before JobItems
|
||||||
|
DELETE FROM OvenBatchItems
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'OvenBatchItems deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Clean up now-empty OvenBatches (batches belonging to this company with no remaining items)
|
||||||
|
DELETE FROM OvenBatches
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND CreatedAt < @CutoffDate
|
||||||
|
AND Id NOT IN (SELECT DISTINCT OvenBatchId FROM OvenBatchItems);
|
||||||
|
PRINT 'OvenBatches deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- ReworkRecords (JobId required FK; ReworkJobId optional)
|
||||||
|
DELETE FROM ReworkRecords
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate)
|
||||||
|
OR ReworkJobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'ReworkRecords deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- JobItem children (coats and prep services)
|
||||||
|
DELETE FROM JobItemCoats
|
||||||
|
WHERE JobItemId IN (
|
||||||
|
SELECT Id FROM JobItems WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate));
|
||||||
|
PRINT 'JobItemCoats deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobItemPrepServices
|
||||||
|
WHERE JobItemId IN (
|
||||||
|
SELECT Id FROM JobItems WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate));
|
||||||
|
PRINT 'JobItemPrepServices deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobItems
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobItems deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- Job metadata tables
|
||||||
|
DELETE FROM JobChangeHistories
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobChangeHistories deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobTimeEntries
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobTimeEntries deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobPhotos
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobPhotos deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobNotes
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobNotes deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobStatusHistory
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobStatusHistory deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM JobDailyPriorities
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'JobDailyPriorities deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM PowderUsageLogs
|
||||||
|
WHERE JobId IN (
|
||||||
|
SELECT Id FROM Jobs WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'PowderUsageLogs deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM AiItemPredictions
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'AiItemPredictions deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM Jobs
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'Jobs deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SECTION 5 — QUOTES
|
||||||
|
-- ===========================================================================
|
||||||
|
PRINT '';
|
||||||
|
PRINT '--- Section 5: Quotes ---';
|
||||||
|
|
||||||
|
-- NULL FKs that point to quotes being deleted
|
||||||
|
UPDATE Deposits
|
||||||
|
SET QuoteId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
UPDATE NotificationLogs
|
||||||
|
SET QuoteId = NULL
|
||||||
|
WHERE CompanyId = @CompanyId
|
||||||
|
AND QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
|
||||||
|
-- QuoteItem children
|
||||||
|
DELETE FROM QuoteItemCoats
|
||||||
|
WHERE QuoteItemId IN (
|
||||||
|
SELECT Id FROM QuoteItems WHERE QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate));
|
||||||
|
PRINT 'QuoteItemCoats deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM QuoteItemPrepServices
|
||||||
|
WHERE QuoteItemId IN (
|
||||||
|
SELECT Id FROM QuoteItems WHERE QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate));
|
||||||
|
PRINT 'QuoteItemPrepServices deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM QuoteItems
|
||||||
|
WHERE QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'QuoteItems deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM QuoteChangeHistories
|
||||||
|
WHERE QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'QuoteChangeHistories deleted: ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM QuotePhotos
|
||||||
|
WHERE QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'QuotePhotos deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- QuotePrepServices are scoped to the quote's company via the quote FK, not directly
|
||||||
|
DELETE FROM QuotePrepServices
|
||||||
|
WHERE QuoteId IN (
|
||||||
|
SELECT Id FROM Quotes WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate);
|
||||||
|
PRINT 'QuotePrepServices deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
DELETE FROM Quotes
|
||||||
|
WHERE CompanyId = @CompanyId AND CreatedAt < @CutoffDate;
|
||||||
|
PRINT 'Quotes deleted : ' + CAST(@@ROWCOUNT AS NVARCHAR);
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SUMMARY & COMMIT / ROLLBACK
|
||||||
|
-- ===========================================================================
|
||||||
|
PRINT '';
|
||||||
|
PRINT '============================================================';
|
||||||
|
|
||||||
|
IF @DryRun = 1
|
||||||
|
BEGIN
|
||||||
|
PRINT 'DRY RUN complete — rolling back. No data was changed.';
|
||||||
|
PRINT 'Set @DryRun = 0 and run again to apply the deletes.';
|
||||||
|
ROLLBACK TRANSACTION;
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
PRINT 'Purge complete — committing.';
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END
|
||||||
@@ -63,4 +63,22 @@ public class CustomerImportDto
|
|||||||
|
|
||||||
[Name("Notes")]
|
[Name("Notes")]
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
[Name("LeadSource")]
|
||||||
|
public string? LeadSource { get; set; }
|
||||||
|
|
||||||
|
[Name("ShipToAddress")]
|
||||||
|
public string? ShipToAddress { get; set; }
|
||||||
|
|
||||||
|
[Name("ShipToCity")]
|
||||||
|
public string? ShipToCity { get; set; }
|
||||||
|
|
||||||
|
[Name("ShipToState")]
|
||||||
|
public string? ShipToState { get; set; }
|
||||||
|
|
||||||
|
[Name("ShipToZipCode")]
|
||||||
|
public string? ShipToZipCode { get; set; }
|
||||||
|
|
||||||
|
[Name("ShipToCountry")]
|
||||||
|
public string? ShipToCountry { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ public class InvoiceImportDto
|
|||||||
[Name("DueDate")]
|
[Name("DueDate")]
|
||||||
public DateTime? DueDate { get; set; }
|
public DateTime? DueDate { get; set; }
|
||||||
|
|
||||||
|
[Name("Project Name", "ProjectName")]
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
|
||||||
[Name("SubTotal")]
|
[Name("SubTotal")]
|
||||||
public decimal SubTotal { get; set; }
|
public decimal SubTotal { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ public class JobImportDto
|
|||||||
[Name("SpecialInstructions")]
|
[Name("SpecialInstructions")]
|
||||||
public string? SpecialInstructions { get; set; }
|
public string? SpecialInstructions { get; set; }
|
||||||
|
|
||||||
|
[Name("ProjectName")]
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
|
||||||
[Name("Notes")]
|
[Name("Notes")]
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public class QuoteImportDto
|
|||||||
[Name("ExpirationDate")]
|
[Name("ExpirationDate")]
|
||||||
public DateTime? ExpirationDate { get; set; }
|
public DateTime? ExpirationDate { get; set; }
|
||||||
|
|
||||||
|
[Name("ProjectName")]
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
|
|
||||||
[Name("Subtotal")]
|
[Name("Subtotal")]
|
||||||
public decimal Subtotal { get; set; }
|
public decimal Subtotal { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -605,6 +605,12 @@ public class CsvImportService : ICsvImportService
|
|||||||
PaymentTerms = record.PaymentTerms?.Trim() ?? "Net 30",
|
PaymentTerms = record.PaymentTerms?.Trim() ?? "Net 30",
|
||||||
IsTaxExempt = record.TaxExempt ?? false,
|
IsTaxExempt = record.TaxExempt ?? false,
|
||||||
GeneralNotes = record.Notes?.Trim(),
|
GeneralNotes = record.Notes?.Trim(),
|
||||||
|
LeadSource = record.LeadSource?.Trim(),
|
||||||
|
ShipToAddress = record.ShipToAddress?.Trim(),
|
||||||
|
ShipToCity = record.ShipToCity?.Trim(),
|
||||||
|
ShipToState = record.ShipToState?.Trim(),
|
||||||
|
ShipToZipCode = record.ShipToZipCode?.Trim(),
|
||||||
|
ShipToCountry = record.ShipToCountry?.Trim(),
|
||||||
IsActive = record.IsActive ?? true,
|
IsActive = record.IsActive ?? true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow
|
||||||
@@ -1284,6 +1290,7 @@ public class CsvImportService : ICsvImportService
|
|||||||
Total = record.Total,
|
Total = record.Total,
|
||||||
Notes = record.Notes?.Trim(),
|
Notes = record.Notes?.Trim(),
|
||||||
Terms = record.TermsAndConditions?.Trim(),
|
Terms = record.TermsAndConditions?.Trim(),
|
||||||
|
ProjectName = record.ProjectName?.Trim(),
|
||||||
IsCommercial = customerId.HasValue,
|
IsCommercial = customerId.HasValue,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow
|
||||||
@@ -1557,6 +1564,7 @@ public class CsvImportService : ICsvImportService
|
|||||||
CustomerPO = record.CustomerPO?.Trim(),
|
CustomerPO = record.CustomerPO?.Trim(),
|
||||||
SpecialInstructions = record.SpecialInstructions?.Trim(),
|
SpecialInstructions = record.SpecialInstructions?.Trim(),
|
||||||
InternalNotes = record.Notes?.Trim(),
|
InternalNotes = record.Notes?.Trim(),
|
||||||
|
ProjectName = record.ProjectName?.Trim(),
|
||||||
Description = record.Description?.Trim()
|
Description = record.Description?.Trim()
|
||||||
?? record.SpecialInstructions?.Trim()
|
?? record.SpecialInstructions?.Trim()
|
||||||
?? "Imported job",
|
?? "Imported job",
|
||||||
@@ -3146,9 +3154,10 @@ public class CsvImportService : ICsvImportService
|
|||||||
existing.DiscountAmount = record.DiscountAmount;
|
existing.DiscountAmount = record.DiscountAmount;
|
||||||
existing.Total = record.Total;
|
existing.Total = record.Total;
|
||||||
existing.AmountPaid = record.AmountPaid;
|
existing.AmountPaid = record.AmountPaid;
|
||||||
existing.CustomerPO = string.IsNullOrWhiteSpace(record.CustomerPO) ? null : record.CustomerPO.Trim();
|
existing.CustomerPO = string.IsNullOrWhiteSpace(record.CustomerPO) ? null : record.CustomerPO.Trim();
|
||||||
existing.Terms = string.IsNullOrWhiteSpace(record.Terms) ? null : record.Terms.Trim();
|
existing.Terms = string.IsNullOrWhiteSpace(record.Terms) ? null : record.Terms.Trim();
|
||||||
existing.Notes = string.IsNullOrWhiteSpace(record.Notes) ? null : record.Notes.Trim();
|
existing.Notes = string.IsNullOrWhiteSpace(record.Notes) ? null : record.Notes.Trim();
|
||||||
|
existing.ProjectName = string.IsNullOrWhiteSpace(record.ProjectName) ? null : record.ProjectName.Trim();
|
||||||
await _unitOfWork.CompleteAsync();
|
await _unitOfWork.CompleteAsync();
|
||||||
result.SuccessCount++;
|
result.SuccessCount++;
|
||||||
}
|
}
|
||||||
@@ -3170,9 +3179,10 @@ public class CsvImportService : ICsvImportService
|
|||||||
DiscountAmount = record.DiscountAmount,
|
DiscountAmount = record.DiscountAmount,
|
||||||
Total = record.Total,
|
Total = record.Total,
|
||||||
AmountPaid = record.AmountPaid,
|
AmountPaid = record.AmountPaid,
|
||||||
CustomerPO = string.IsNullOrWhiteSpace(record.CustomerPO) ? null : record.CustomerPO.Trim(),
|
CustomerPO = string.IsNullOrWhiteSpace(record.CustomerPO) ? null : record.CustomerPO.Trim(),
|
||||||
Terms = string.IsNullOrWhiteSpace(record.Terms) ? null : record.Terms.Trim(),
|
Terms = string.IsNullOrWhiteSpace(record.Terms) ? null : record.Terms.Trim(),
|
||||||
Notes = string.IsNullOrWhiteSpace(record.Notes) ? null : record.Notes.Trim()
|
Notes = string.IsNullOrWhiteSpace(record.Notes) ? null : record.Notes.Trim(),
|
||||||
|
ProjectName = string.IsNullOrWhiteSpace(record.ProjectName) ? null : record.ProjectName.Trim()
|
||||||
};
|
};
|
||||||
await _unitOfWork.Invoices.AddAsync(invoice);
|
await _unitOfWork.Invoices.AddAsync(invoice);
|
||||||
await _unitOfWork.CompleteAsync();
|
await _unitOfWork.CompleteAsync();
|
||||||
|
|||||||
@@ -126,8 +126,9 @@ public class AccountDataExportController : Controller
|
|||||||
{
|
{
|
||||||
switch (sheet)
|
switch (sheet)
|
||||||
{
|
{
|
||||||
case "Customers": await AddCustomersSheet(package, companyId, headerColor); break;
|
case "Customers": await AddCustomersSheet(package, companyId, headerColor); break;
|
||||||
case "Jobs": await AddJobsSheet(package, companyId, headerColor); break;
|
case "CustomerContacts": await AddCustomerContactsSheet(package, companyId, headerColor); break;
|
||||||
|
case "Jobs": await AddJobsSheet(package, companyId, headerColor); break;
|
||||||
case "Quotes": await AddQuotesSheet(package, companyId, headerColor); break;
|
case "Quotes": await AddQuotesSheet(package, companyId, headerColor); break;
|
||||||
case "Invoices": await AddInvoicesSheet(package, companyId, headerColor); break;
|
case "Invoices": await AddInvoicesSheet(package, companyId, headerColor); break;
|
||||||
case "Inventory": await AddInventorySheet(package, companyId, headerColor); break;
|
case "Inventory": await AddInventorySheet(package, companyId, headerColor); break;
|
||||||
@@ -174,8 +175,9 @@ public class AccountDataExportController : Controller
|
|||||||
{
|
{
|
||||||
switch (sheet)
|
switch (sheet)
|
||||||
{
|
{
|
||||||
case "Customers": WriteCsvEntry(zip, "Customers.csv", await BuildCustomersCsv(companyId)); break;
|
case "Customers": WriteCsvEntry(zip, "Customers.csv", await BuildCustomersCsv(companyId)); break;
|
||||||
case "Jobs": WriteCsvEntry(zip, "Jobs.csv", await BuildJobsCsv(companyId)); break;
|
case "CustomerContacts": WriteCsvEntry(zip, "CustomerContacts.csv", await BuildCustomerContactsCsv(companyId)); break;
|
||||||
|
case "Jobs": WriteCsvEntry(zip, "Jobs.csv", await BuildJobsCsv(companyId)); break;
|
||||||
case "Quotes": WriteCsvEntry(zip, "Quotes.csv", await BuildQuotesCsv(companyId)); break;
|
case "Quotes": WriteCsvEntry(zip, "Quotes.csv", await BuildQuotesCsv(companyId)); break;
|
||||||
case "Invoices": WriteCsvEntry(zip, "Invoices.csv", await BuildInvoicesCsv(companyId)); break;
|
case "Invoices": WriteCsvEntry(zip, "Invoices.csv", await BuildInvoicesCsv(companyId)); break;
|
||||||
case "Inventory": WriteCsvEntry(zip, "Inventory.csv", await BuildInventoryCsv(companyId)); break;
|
case "Inventory": WriteCsvEntry(zip, "Inventory.csv", await BuildInventoryCsv(companyId)); break;
|
||||||
@@ -299,7 +301,9 @@ public class AccountDataExportController : Controller
|
|||||||
var data = await FetchCustomersAsync(companyId);
|
var data = await FetchCustomersAsync(companyId);
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Customers");
|
var ws = pkg.Workbook.Worksheets.Add("Customers");
|
||||||
var headers = new[] { "ID", "Company Name", "First Name", "Last Name", "Email", "Phone",
|
var headers = new[] { "ID", "Company Name", "First Name", "Last Name", "Email", "Phone",
|
||||||
"Commercial", "City", "State", "Active", "Credit Limit", "Current Balance", "Created At" };
|
"Commercial", "City", "State", "Active", "Credit Limit", "Current Balance",
|
||||||
|
"Lead Source", "Ship-To Address", "Ship-To City", "Ship-To State", "Ship-To Zip", "Ship-To Country",
|
||||||
|
"Created At" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -311,7 +315,34 @@ public class AccountDataExportController : Controller
|
|||||||
ws.Cells[r, 8].Value = c.City; ws.Cells[r, 9].Value = c.State;
|
ws.Cells[r, 8].Value = c.City; ws.Cells[r, 9].Value = c.State;
|
||||||
ws.Cells[r, 10].Value = c.IsActive ? "Yes" : "No";
|
ws.Cells[r, 10].Value = c.IsActive ? "Yes" : "No";
|
||||||
ws.Cells[r, 11].Value = c.CreditLimit; ws.Cells[r, 12].Value = c.CurrentBalance;
|
ws.Cells[r, 11].Value = c.CreditLimit; ws.Cells[r, 12].Value = c.CurrentBalance;
|
||||||
ws.Cells[r, 13].Value = c.CreatedAt.ToString("yyyy-MM-dd");
|
ws.Cells[r, 13].Value = c.LeadSource;
|
||||||
|
ws.Cells[r, 14].Value = c.ShipToAddress; ws.Cells[r, 15].Value = c.ShipToCity;
|
||||||
|
ws.Cells[r, 16].Value = c.ShipToState; ws.Cells[r, 17].Value = c.ShipToZipCode;
|
||||||
|
ws.Cells[r, 18].Value = c.ShipToCountry;
|
||||||
|
ws.Cells[r, 19].Value = c.CreatedAt.ToString("yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
AutoFit(ws, headers.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddCustomerContactsSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||||
|
{
|
||||||
|
var data = await _db.CustomerContacts.AsNoTracking()
|
||||||
|
.Include(cc => cc.Customer)
|
||||||
|
.Where(cc => cc.CompanyId == companyId && !cc.IsDeleted)
|
||||||
|
.OrderBy(cc => cc.Customer!.CompanyName).ThenBy(cc => cc.LastName).ThenBy(cc => cc.FirstName)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var ws = pkg.Workbook.Worksheets.Add("CustomerContacts");
|
||||||
|
var headers = new[] { "CustomerEmail", "FirstName", "LastName", "Title", "ContactRole", "Email", "Phone", "MobilePhone", "Notes" };
|
||||||
|
WriteHeader(ws, headers, hdr);
|
||||||
|
for (int i = 0; i < data.Count; i++)
|
||||||
|
{
|
||||||
|
var r = i + 2; var cc = data[i];
|
||||||
|
ws.Cells[r, 1].Value = cc.Customer?.Email;
|
||||||
|
ws.Cells[r, 2].Value = cc.FirstName; ws.Cells[r, 3].Value = cc.LastName;
|
||||||
|
ws.Cells[r, 4].Value = cc.Title; ws.Cells[r, 5].Value = cc.ContactRole;
|
||||||
|
ws.Cells[r, 6].Value = cc.Email; ws.Cells[r, 7].Value = cc.Phone;
|
||||||
|
ws.Cells[r, 8].Value = cc.MobilePhone; ws.Cells[r, 9].Value = cc.Notes;
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -326,7 +357,7 @@ public class AccountDataExportController : Controller
|
|||||||
var data = await FetchJobsAsync(companyId);
|
var data = await FetchJobsAsync(companyId);
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Jobs");
|
var ws = pkg.Workbook.Worksheets.Add("Jobs");
|
||||||
var headers = new[] { "ID", "Job Number", "Customer", "Status", "Priority",
|
var headers = new[] { "ID", "Job Number", "Customer", "Status", "Priority",
|
||||||
"Description", "Due Date", "Final Price", "Created At" };
|
"Description", "Project Name", "Due Date", "Final Price", "Created At" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -337,9 +368,10 @@ public class AccountDataExportController : Controller
|
|||||||
ws.Cells[r, 4].Value = j.JobStatus?.DisplayName ?? j.JobStatusId.ToString();
|
ws.Cells[r, 4].Value = j.JobStatus?.DisplayName ?? j.JobStatusId.ToString();
|
||||||
ws.Cells[r, 5].Value = j.JobPriority?.DisplayName ?? j.JobPriorityId.ToString();
|
ws.Cells[r, 5].Value = j.JobPriority?.DisplayName ?? j.JobPriorityId.ToString();
|
||||||
ws.Cells[r, 6].Value = j.Description;
|
ws.Cells[r, 6].Value = j.Description;
|
||||||
ws.Cells[r, 7].Value = j.DueDate?.ToString("yyyy-MM-dd");
|
ws.Cells[r, 7].Value = j.ProjectName;
|
||||||
ws.Cells[r, 8].Value = j.FinalPrice;
|
ws.Cells[r, 8].Value = j.DueDate?.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 9].Value = j.CreatedAt.ToString("yyyy-MM-dd");
|
ws.Cells[r, 9].Value = j.FinalPrice;
|
||||||
|
ws.Cells[r, 10].Value = j.CreatedAt.ToString("yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -353,7 +385,7 @@ public class AccountDataExportController : Controller
|
|||||||
var data = await FetchQuotesAsync(companyId);
|
var data = await FetchQuotesAsync(companyId);
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Quotes");
|
var ws = pkg.Workbook.Worksheets.Add("Quotes");
|
||||||
var headers = new[] { "ID", "Quote Number", "Customer / Prospect", "Status",
|
var headers = new[] { "ID", "Quote Number", "Customer / Prospect", "Status",
|
||||||
"Quote Date", "Expiration Date", "Subtotal", "Tax", "Total" };
|
"Quote Date", "Expiration Date", "Project Name", "Subtotal", "Tax", "Total" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -363,7 +395,8 @@ public class AccountDataExportController : Controller
|
|||||||
ws.Cells[r, 4].Value = q.QuoteStatus?.DisplayName ?? q.QuoteStatusId.ToString();
|
ws.Cells[r, 4].Value = q.QuoteStatus?.DisplayName ?? q.QuoteStatusId.ToString();
|
||||||
ws.Cells[r, 5].Value = q.QuoteDate.ToString("yyyy-MM-dd");
|
ws.Cells[r, 5].Value = q.QuoteDate.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 6].Value = q.ExpirationDate?.ToString("yyyy-MM-dd");
|
ws.Cells[r, 6].Value = q.ExpirationDate?.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 7].Value = q.SubTotal; ws.Cells[r, 8].Value = q.TaxAmount; ws.Cells[r, 9].Value = q.Total;
|
ws.Cells[r, 7].Value = q.ProjectName;
|
||||||
|
ws.Cells[r, 8].Value = q.SubTotal; ws.Cells[r, 9].Value = q.TaxAmount; ws.Cells[r, 10].Value = q.Total;
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -377,7 +410,7 @@ public class AccountDataExportController : Controller
|
|||||||
var data = await FetchInvoicesAsync(companyId);
|
var data = await FetchInvoicesAsync(companyId);
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Invoices");
|
var ws = pkg.Workbook.Worksheets.Add("Invoices");
|
||||||
var headers = new[] { "ID", "Invoice #", "Customer", "Status", "Invoice Date",
|
var headers = new[] { "ID", "Invoice #", "Customer", "Status", "Invoice Date",
|
||||||
"Due Date", "Subtotal", "Tax", "Total", "Amount Paid", "Balance Due" };
|
"Due Date", "Project Name", "Subtotal", "Tax", "Total", "Amount Paid", "Balance Due" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -389,9 +422,10 @@ public class AccountDataExportController : Controller
|
|||||||
ws.Cells[r, 3].Value = cust; ws.Cells[r, 4].Value = inv.Status.ToString();
|
ws.Cells[r, 3].Value = cust; ws.Cells[r, 4].Value = inv.Status.ToString();
|
||||||
ws.Cells[r, 5].Value = inv.InvoiceDate.ToString("yyyy-MM-dd");
|
ws.Cells[r, 5].Value = inv.InvoiceDate.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 6].Value = inv.DueDate?.ToString("yyyy-MM-dd");
|
ws.Cells[r, 6].Value = inv.DueDate?.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 7].Value = inv.SubTotal; ws.Cells[r, 8].Value = inv.TaxAmount;
|
ws.Cells[r, 7].Value = inv.ProjectName;
|
||||||
ws.Cells[r, 9].Value = inv.Total; ws.Cells[r, 10].Value = inv.AmountPaid;
|
ws.Cells[r, 8].Value = inv.SubTotal; ws.Cells[r, 9].Value = inv.TaxAmount;
|
||||||
ws.Cells[r, 11].Value = inv.BalanceDue;
|
ws.Cells[r, 10].Value = inv.Total; ws.Cells[r, 11].Value = inv.AmountPaid;
|
||||||
|
ws.Cells[r, 12].Value = inv.BalanceDue;
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -487,15 +521,30 @@ public class AccountDataExportController : Controller
|
|||||||
{
|
{
|
||||||
var data = await FetchCustomersAsync(companyId);
|
var data = await FetchCustomersAsync(companyId);
|
||||||
var sb = new StringBuilder();
|
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");
|
sb.AppendLine("CompanyName,ContactFirstName,ContactLastName,Email,Phone,MobilePhone,Address,City,State,ZipCode,Country,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,TaxId,IsActive,Notes,LeadSource,ShipToAddress,ShipToCity,ShipToState,ShipToZipCode,ShipToCountry");
|
||||||
foreach (var c in data)
|
foreach (var c in data)
|
||||||
{
|
{
|
||||||
var customerType = c.IsCommercial ? "Commercial" : "Non-Commercial";
|
var customerType = c.IsCommercial ? "Commercial" : "Non-Commercial";
|
||||||
sb.AppendLine($"{CsvEscape(c.CompanyName)},{CsvEscape(c.ContactFirstName)},{CsvEscape(c.ContactLastName)},{CsvEscape(c.Email)},{CsvEscape(c.Phone)},{CsvEscape(c.MobilePhone)},{CsvEscape(c.Address)},{CsvEscape(c.City)},{CsvEscape(c.State)},{CsvEscape(c.ZipCode)},{CsvEscape(c.Country)},{customerType},{CsvEscape(c.PricingTier?.TierName)},{c.CreditLimit},{CsvEscape(c.PaymentTerms)},{c.IsTaxExempt.ToString().ToLower()},{CsvEscape(c.TaxId)},{c.IsActive.ToString().ToLower()},{CsvEscape(c.GeneralNotes)}");
|
sb.AppendLine($"{CsvEscape(c.CompanyName)},{CsvEscape(c.ContactFirstName)},{CsvEscape(c.ContactLastName)},{CsvEscape(c.Email)},{CsvEscape(c.Phone)},{CsvEscape(c.MobilePhone)},{CsvEscape(c.Address)},{CsvEscape(c.City)},{CsvEscape(c.State)},{CsvEscape(c.ZipCode)},{CsvEscape(c.Country)},{customerType},{CsvEscape(c.PricingTier?.TierName)},{c.CreditLimit},{CsvEscape(c.PaymentTerms)},{c.IsTaxExempt.ToString().ToLower()},{CsvEscape(c.TaxId)},{c.IsActive.ToString().ToLower()},{CsvEscape(c.GeneralNotes)},{CsvEscape(c.LeadSource)},{CsvEscape(c.ShipToAddress)},{CsvEscape(c.ShipToCity)},{CsvEscape(c.ShipToState)},{CsvEscape(c.ShipToZipCode)},{CsvEscape(c.ShipToCountry)}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Builds the customer contacts CSV. CustomerEmail is the join key for re-import.</summary>
|
||||||
|
private async Task<string> BuildCustomerContactsCsv(int companyId)
|
||||||
|
{
|
||||||
|
var data = await _db.CustomerContacts.AsNoTracking()
|
||||||
|
.Include(cc => cc.Customer)
|
||||||
|
.Where(cc => cc.CompanyId == companyId && !cc.IsDeleted)
|
||||||
|
.OrderBy(cc => cc.Customer!.CompanyName).ThenBy(cc => cc.LastName).ThenBy(cc => cc.FirstName)
|
||||||
|
.ToListAsync();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("CustomerEmail,FirstName,LastName,Title,ContactRole,Email,Phone,MobilePhone,Notes");
|
||||||
|
foreach (var cc in data)
|
||||||
|
sb.AppendLine($"{CsvEscape(cc.Customer?.Email)},{CsvEscape(cc.FirstName)},{CsvEscape(cc.LastName)},{CsvEscape(cc.Title)},{CsvEscape(cc.ContactRole)},{CsvEscape(cc.Email)},{CsvEscape(cc.Phone)},{CsvEscape(cc.MobilePhone)},{CsvEscape(cc.Notes)}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Column names match <c>JobImportDto</c> exactly so the file can be re-imported.
|
/// 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.
|
/// CustomerEmail is used (not display name) because the importer resolves the customer FK by email.
|
||||||
@@ -504,13 +553,13 @@ public class AccountDataExportController : Controller
|
|||||||
{
|
{
|
||||||
var data = await FetchJobsAsync(companyId);
|
var data = await FetchJobsAsync(companyId);
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,FinalPrice,CustomerPO,SpecialInstructions,Notes");
|
sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,ProjectName,FinalPrice,CustomerPO,SpecialInstructions,Notes");
|
||||||
foreach (var j in data)
|
foreach (var j in data)
|
||||||
{
|
{
|
||||||
var customerName = !string.IsNullOrWhiteSpace(j.Customer?.CompanyName)
|
var customerName = !string.IsNullOrWhiteSpace(j.Customer?.CompanyName)
|
||||||
? j.Customer.CompanyName
|
? j.Customer.CompanyName
|
||||||
: $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
|
: $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
|
||||||
sb.AppendLine($"{CsvEscape(j.JobNumber)},{CsvEscape(j.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(j.JobStatus?.DisplayName)},{CsvEscape(j.JobPriority?.DisplayName)},{j.ScheduledDate?.ToString("yyyy-MM-dd")},{j.DueDate?.ToString("yyyy-MM-dd")},{j.FinalPrice},{CsvEscape(j.CustomerPO)},{CsvEscape(j.SpecialInstructions)},{CsvEscape(j.InternalNotes)}");
|
sb.AppendLine($"{CsvEscape(j.JobNumber)},{CsvEscape(j.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(j.JobStatus?.DisplayName)},{CsvEscape(j.JobPriority?.DisplayName)},{j.ScheduledDate?.ToString("yyyy-MM-dd")},{j.DueDate?.ToString("yyyy-MM-dd")},{CsvEscape(j.ProjectName)},{j.FinalPrice},{CsvEscape(j.CustomerPO)},{CsvEscape(j.SpecialInstructions)},{CsvEscape(j.InternalNotes)}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -520,13 +569,13 @@ public class AccountDataExportController : Controller
|
|||||||
{
|
{
|
||||||
var data = await FetchQuotesAsync(companyId);
|
var data = await FetchQuotesAsync(companyId);
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");
|
sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,ProjectName,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");
|
||||||
foreach (var q in data)
|
foreach (var q in data)
|
||||||
{
|
{
|
||||||
var customerName = !string.IsNullOrWhiteSpace(q.Customer?.CompanyName)
|
var customerName = !string.IsNullOrWhiteSpace(q.Customer?.CompanyName)
|
||||||
? q.Customer.CompanyName
|
? q.Customer.CompanyName
|
||||||
: $"{q.Customer?.ContactFirstName} {q.Customer?.ContactLastName}".Trim();
|
: $"{q.Customer?.ContactFirstName} {q.Customer?.ContactLastName}".Trim();
|
||||||
sb.AppendLine($"{CsvEscape(q.QuoteNumber)},{CsvEscape(q.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(q.ProspectCompanyName)},{CsvEscape(q.ProspectContactName)},{CsvEscape(q.ProspectEmail)},{CsvEscape(q.ProspectPhone)},{CsvEscape(q.QuoteStatus?.DisplayName)},{q.QuoteDate:yyyy-MM-dd},{q.ExpirationDate?.ToString("yyyy-MM-dd")},{q.SubTotal},{q.TaxAmount},{q.Total},{CsvEscape(q.Notes)},{CsvEscape(q.Terms)}");
|
sb.AppendLine($"{CsvEscape(q.QuoteNumber)},{CsvEscape(q.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(q.ProspectCompanyName)},{CsvEscape(q.ProspectContactName)},{CsvEscape(q.ProspectEmail)},{CsvEscape(q.ProspectPhone)},{CsvEscape(q.QuoteStatus?.DisplayName)},{q.QuoteDate:yyyy-MM-dd},{q.ExpirationDate?.ToString("yyyy-MM-dd")},{CsvEscape(q.ProjectName)},{q.SubTotal},{q.TaxAmount},{q.Total},{CsvEscape(q.Notes)},{CsvEscape(q.Terms)}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -539,13 +588,13 @@ public class AccountDataExportController : Controller
|
|||||||
{
|
{
|
||||||
var data = await FetchInvoicesAsync(companyId);
|
var data = await FetchInvoicesAsync(companyId);
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("ID,Invoice #,Customer,Status,Invoice Date,Due Date,Subtotal,Tax,Total,Amount Paid,Balance Due");
|
sb.AppendLine("ID,Invoice #,Customer,Status,Invoice Date,Due Date,Project Name,Subtotal,Tax,Total,Amount Paid,Balance Due");
|
||||||
foreach (var inv in data)
|
foreach (var inv in data)
|
||||||
{
|
{
|
||||||
var cust = inv.Customer != null
|
var cust = inv.Customer != null
|
||||||
? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
|
? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
|
||||||
: $"Customer #{inv.CustomerId}";
|
: $"Customer #{inv.CustomerId}";
|
||||||
sb.AppendLine($"{inv.Id},{CsvEscape(inv.InvoiceNumber)},{CsvEscape(cust)},{inv.Status},{inv.InvoiceDate:yyyy-MM-dd},{inv.DueDate?.ToString("yyyy-MM-dd")},{inv.SubTotal},{inv.TaxAmount},{inv.Total},{inv.AmountPaid},{inv.BalanceDue}");
|
sb.AppendLine($"{inv.Id},{CsvEscape(inv.InvoiceNumber)},{CsvEscape(cust)},{inv.Status},{inv.InvoiceDate:yyyy-MM-dd},{inv.DueDate?.ToString("yyyy-MM-dd")},{CsvEscape(inv.ProjectName)},{inv.SubTotal},{inv.TaxAmount},{inv.Total},{inv.AmountPaid},{inv.BalanceDue}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,10 +115,11 @@ public class DataExportController : Controller
|
|||||||
{
|
{
|
||||||
switch (sheet)
|
switch (sheet)
|
||||||
{
|
{
|
||||||
case "Customers": await AddCustomersSheet(package, companyId, headerColor); break;
|
case "Customers": await AddCustomersSheet(package, companyId, headerColor); break;
|
||||||
case "Jobs": await AddJobsSheet(package, companyId, headerColor); break;
|
case "CustomerContacts": await AddCustomerContactsSheet(package, companyId, headerColor); break;
|
||||||
case "Quotes": await AddQuotesSheet(package, companyId, headerColor); break;
|
case "Jobs": await AddJobsSheet(package, companyId, headerColor); break;
|
||||||
case "Invoices": await AddInvoicesSheet(package, companyId, headerColor); break;
|
case "Quotes": await AddQuotesSheet(package, companyId, headerColor); break;
|
||||||
|
case "Invoices": await AddInvoicesSheet(package, companyId, headerColor); break;
|
||||||
case "Inventory": await AddInventorySheet(package, companyId, headerColor); break;
|
case "Inventory": await AddInventorySheet(package, companyId, headerColor); break;
|
||||||
case "Equipment": await AddEquipmentSheet(package, companyId, headerColor); break;
|
case "Equipment": await AddEquipmentSheet(package, companyId, headerColor); break;
|
||||||
case "Vendors": await AddVendorsSheet(package, companyId, headerColor); break;
|
case "Vendors": await AddVendorsSheet(package, companyId, headerColor); break;
|
||||||
@@ -164,10 +165,11 @@ public class DataExportController : Controller
|
|||||||
{
|
{
|
||||||
switch (sheet)
|
switch (sheet)
|
||||||
{
|
{
|
||||||
case "Customers": WriteCsvEntry(zip, "Customers.csv", await BuildCustomersCsv(companyId)); break;
|
case "Customers": WriteCsvEntry(zip, "Customers.csv", await BuildCustomersCsv(companyId)); break;
|
||||||
case "Jobs": WriteCsvEntry(zip, "Jobs.csv", await BuildJobsCsv(companyId)); break;
|
case "CustomerContacts": WriteCsvEntry(zip, "CustomerContacts.csv", await BuildCustomerContactsCsv(companyId)); break;
|
||||||
case "Quotes": WriteCsvEntry(zip, "Quotes.csv", await BuildQuotesCsv(companyId)); break;
|
case "Jobs": WriteCsvEntry(zip, "Jobs.csv", await BuildJobsCsv(companyId)); break;
|
||||||
case "Invoices": WriteCsvEntry(zip, "Invoices.csv", await BuildInvoicesCsv(companyId)); break;
|
case "Quotes": WriteCsvEntry(zip, "Quotes.csv", await BuildQuotesCsv(companyId)); break;
|
||||||
|
case "Invoices": WriteCsvEntry(zip, "Invoices.csv", await BuildInvoicesCsv(companyId)); break;
|
||||||
case "Inventory": WriteCsvEntry(zip, "Inventory.csv", await BuildInventoryCsv(companyId)); break;
|
case "Inventory": WriteCsvEntry(zip, "Inventory.csv", await BuildInventoryCsv(companyId)); break;
|
||||||
case "Equipment": WriteCsvEntry(zip, "Equipment.csv", await BuildEquipmentCsv(companyId)); break;
|
case "Equipment": WriteCsvEntry(zip, "Equipment.csv", await BuildEquipmentCsv(companyId)); break;
|
||||||
case "Vendors": WriteCsvEntry(zip, "Vendors.csv", await BuildVendorsCsv(companyId)); break;
|
case "Vendors": WriteCsvEntry(zip, "Vendors.csv", await BuildVendorsCsv(companyId)); break;
|
||||||
@@ -240,7 +242,9 @@ public class DataExportController : Controller
|
|||||||
var ws = pkg.Workbook.Worksheets.Add("Customers");
|
var ws = pkg.Workbook.Worksheets.Add("Customers");
|
||||||
var headers = new[] { "ID", "Company Name", "First Name", "Last Name", "Email", "Phone",
|
var headers = new[] { "ID", "Company Name", "First Name", "Last Name", "Email", "Phone",
|
||||||
"Commercial", "City", "State", "Active", "Credit Limit",
|
"Commercial", "City", "State", "Active", "Credit Limit",
|
||||||
"Current Balance", "Created At" };
|
"Current Balance", "Lead Source",
|
||||||
|
"Ship-To Address", "Ship-To City", "Ship-To State", "Ship-To Zip", "Ship-To Country",
|
||||||
|
"Created At" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
|
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
@@ -259,7 +263,13 @@ public class DataExportController : Controller
|
|||||||
ws.Cells[r, 10].Value = c.IsActive ? "Yes" : "No";
|
ws.Cells[r, 10].Value = c.IsActive ? "Yes" : "No";
|
||||||
ws.Cells[r, 11].Value = c.CreditLimit;
|
ws.Cells[r, 11].Value = c.CreditLimit;
|
||||||
ws.Cells[r, 12].Value = c.CurrentBalance;
|
ws.Cells[r, 12].Value = c.CurrentBalance;
|
||||||
ws.Cells[r, 13].Value = c.CreatedAt.ToString("yyyy-MM-dd");
|
ws.Cells[r, 13].Value = c.LeadSource;
|
||||||
|
ws.Cells[r, 14].Value = c.ShipToAddress;
|
||||||
|
ws.Cells[r, 15].Value = c.ShipToCity;
|
||||||
|
ws.Cells[r, 16].Value = c.ShipToState;
|
||||||
|
ws.Cells[r, 17].Value = c.ShipToZipCode;
|
||||||
|
ws.Cells[r, 18].Value = c.ShipToCountry;
|
||||||
|
ws.Cells[r, 19].Value = c.CreatedAt.ToString("yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -282,22 +292,23 @@ public class DataExportController : Controller
|
|||||||
|
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Jobs");
|
var ws = pkg.Workbook.Worksheets.Add("Jobs");
|
||||||
var headers = new[] { "ID", "Job Number", "Customer", "Status", "Priority",
|
var headers = new[] { "ID", "Job Number", "Customer", "Status", "Priority",
|
||||||
"Description", "Due Date", "Final Price", "Created At" };
|
"Description", "Project Name", "Due Date", "Final Price", "Created At" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
|
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
var r = i + 2;
|
var r = i + 2;
|
||||||
var j = data[i];
|
var j = data[i];
|
||||||
ws.Cells[r, 1].Value = j.Id;
|
ws.Cells[r, 1].Value = j.Id;
|
||||||
ws.Cells[r, 2].Value = j.JobNumber;
|
ws.Cells[r, 2].Value = j.JobNumber;
|
||||||
ws.Cells[r, 3].Value = j.Customer?.CompanyName ?? $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
|
ws.Cells[r, 3].Value = j.Customer?.CompanyName ?? $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
|
||||||
ws.Cells[r, 4].Value = j.JobStatus?.DisplayName ?? j.JobStatusId.ToString();
|
ws.Cells[r, 4].Value = j.JobStatus?.DisplayName ?? j.JobStatusId.ToString();
|
||||||
ws.Cells[r, 5].Value = j.JobPriority?.DisplayName ?? j.JobPriorityId.ToString();
|
ws.Cells[r, 5].Value = j.JobPriority?.DisplayName ?? j.JobPriorityId.ToString();
|
||||||
ws.Cells[r, 6].Value = j.Description;
|
ws.Cells[r, 6].Value = j.Description;
|
||||||
ws.Cells[r, 7].Value = j.DueDate?.ToString("yyyy-MM-dd");
|
ws.Cells[r, 7].Value = j.ProjectName;
|
||||||
ws.Cells[r, 8].Value = j.FinalPrice;
|
ws.Cells[r, 8].Value = j.DueDate?.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 9].Value = j.CreatedAt.ToString("yyyy-MM-dd");
|
ws.Cells[r, 9].Value = j.FinalPrice;
|
||||||
|
ws.Cells[r, 10].Value = j.CreatedAt.ToString("yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -318,22 +329,23 @@ public class DataExportController : Controller
|
|||||||
|
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Quotes");
|
var ws = pkg.Workbook.Worksheets.Add("Quotes");
|
||||||
var headers = new[] { "ID", "Quote Number", "Customer / Prospect", "Status",
|
var headers = new[] { "ID", "Quote Number", "Customer / Prospect", "Status",
|
||||||
"Quote Date", "Expiration Date", "Subtotal", "Tax", "Total" };
|
"Quote Date", "Expiration Date", "Project Name", "Subtotal", "Tax", "Total" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
|
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
var r = i + 2;
|
var r = i + 2;
|
||||||
var q = data[i];
|
var q = data[i];
|
||||||
ws.Cells[r, 1].Value = q.Id;
|
ws.Cells[r, 1].Value = q.Id;
|
||||||
ws.Cells[r, 2].Value = q.QuoteNumber;
|
ws.Cells[r, 2].Value = q.QuoteNumber;
|
||||||
ws.Cells[r, 3].Value = string.IsNullOrEmpty(q.ProspectCompanyName) ? $"Customer #{q.CustomerId}" : q.ProspectCompanyName;
|
ws.Cells[r, 3].Value = string.IsNullOrEmpty(q.ProspectCompanyName) ? $"Customer #{q.CustomerId}" : q.ProspectCompanyName;
|
||||||
ws.Cells[r, 4].Value = q.QuoteStatus?.DisplayName ?? q.QuoteStatusId.ToString();
|
ws.Cells[r, 4].Value = q.QuoteStatus?.DisplayName ?? q.QuoteStatusId.ToString();
|
||||||
ws.Cells[r, 5].Value = q.QuoteDate.ToString("yyyy-MM-dd");
|
ws.Cells[r, 5].Value = q.QuoteDate.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 6].Value = q.ExpirationDate?.ToString("yyyy-MM-dd");
|
ws.Cells[r, 6].Value = q.ExpirationDate?.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 7].Value = q.SubTotal;
|
ws.Cells[r, 7].Value = q.ProjectName;
|
||||||
ws.Cells[r, 8].Value = q.TaxAmount;
|
ws.Cells[r, 8].Value = q.SubTotal;
|
||||||
ws.Cells[r, 9].Value = q.Total;
|
ws.Cells[r, 9].Value = q.TaxAmount;
|
||||||
|
ws.Cells[r, 10].Value = q.Total;
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -456,7 +468,7 @@ public class DataExportController : Controller
|
|||||||
|
|
||||||
var ws = pkg.Workbook.Worksheets.Add("Invoices");
|
var ws = pkg.Workbook.Worksheets.Add("Invoices");
|
||||||
var headers = new[] { "ID", "Invoice #", "Customer", "Status", "Invoice Date",
|
var headers = new[] { "ID", "Invoice #", "Customer", "Status", "Invoice Date",
|
||||||
"Due Date", "Subtotal", "Tax", "Total", "Amount Paid", "Balance Due" };
|
"Due Date", "Project Name", "Subtotal", "Tax", "Total", "Amount Paid", "Balance Due" };
|
||||||
WriteHeader(ws, headers, hdr);
|
WriteHeader(ws, headers, hdr);
|
||||||
|
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
@@ -472,11 +484,12 @@ public class DataExportController : Controller
|
|||||||
ws.Cells[r, 4].Value = inv.Status.ToString();
|
ws.Cells[r, 4].Value = inv.Status.ToString();
|
||||||
ws.Cells[r, 5].Value = inv.InvoiceDate.ToString("yyyy-MM-dd");
|
ws.Cells[r, 5].Value = inv.InvoiceDate.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 6].Value = inv.DueDate?.ToString("yyyy-MM-dd");
|
ws.Cells[r, 6].Value = inv.DueDate?.ToString("yyyy-MM-dd");
|
||||||
ws.Cells[r, 7].Value = inv.SubTotal;
|
ws.Cells[r, 7].Value = inv.ProjectName;
|
||||||
ws.Cells[r, 8].Value = inv.TaxAmount;
|
ws.Cells[r, 8].Value = inv.SubTotal;
|
||||||
ws.Cells[r, 9].Value = inv.Total;
|
ws.Cells[r, 9].Value = inv.TaxAmount;
|
||||||
ws.Cells[r, 10].Value = inv.AmountPaid;
|
ws.Cells[r, 10].Value = inv.Total;
|
||||||
ws.Cells[r, 11].Value = inv.BalanceDue;
|
ws.Cells[r, 11].Value = inv.AmountPaid;
|
||||||
|
ws.Cells[r, 12].Value = inv.BalanceDue;
|
||||||
}
|
}
|
||||||
AutoFit(ws, headers.Length);
|
AutoFit(ws, headers.Length);
|
||||||
}
|
}
|
||||||
@@ -530,11 +543,11 @@ public class DataExportController : Controller
|
|||||||
.Include(c => c.PricingTier)
|
.Include(c => c.PricingTier)
|
||||||
.Where(c => c.CompanyId == companyId && !c.IsDeleted).OrderBy(c => c.CompanyName).ToListAsync();
|
.Where(c => c.CompanyId == companyId && !c.IsDeleted).OrderBy(c => c.CompanyName).ToListAsync();
|
||||||
var sb = new StringBuilder();
|
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");
|
sb.AppendLine("CompanyName,ContactFirstName,ContactLastName,Email,Phone,MobilePhone,Address,City,State,ZipCode,Country,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,TaxId,IsActive,Notes,LeadSource,ShipToAddress,ShipToCity,ShipToState,ShipToZipCode,ShipToCountry");
|
||||||
foreach (var c in data)
|
foreach (var c in data)
|
||||||
{
|
{
|
||||||
var customerType = c.IsCommercial ? "Commercial" : "Non-Commercial";
|
var customerType = c.IsCommercial ? "Commercial" : "Non-Commercial";
|
||||||
sb.AppendLine($"{CsvEscape(c.CompanyName)},{CsvEscape(c.ContactFirstName)},{CsvEscape(c.ContactLastName)},{CsvEscape(c.Email)},{CsvEscape(c.Phone)},{CsvEscape(c.MobilePhone)},{CsvEscape(c.Address)},{CsvEscape(c.City)},{CsvEscape(c.State)},{CsvEscape(c.ZipCode)},{CsvEscape(c.Country)},{customerType},{CsvEscape(c.PricingTier?.TierName)},{c.CreditLimit},{CsvEscape(c.PaymentTerms)},{c.IsTaxExempt.ToString().ToLower()},{CsvEscape(c.TaxId)},{c.IsActive.ToString().ToLower()},{CsvEscape(c.GeneralNotes)}");
|
sb.AppendLine($"{CsvEscape(c.CompanyName)},{CsvEscape(c.ContactFirstName)},{CsvEscape(c.ContactLastName)},{CsvEscape(c.Email)},{CsvEscape(c.Phone)},{CsvEscape(c.MobilePhone)},{CsvEscape(c.Address)},{CsvEscape(c.City)},{CsvEscape(c.State)},{CsvEscape(c.ZipCode)},{CsvEscape(c.Country)},{customerType},{CsvEscape(c.PricingTier?.TierName)},{c.CreditLimit},{CsvEscape(c.PaymentTerms)},{c.IsTaxExempt.ToString().ToLower()},{CsvEscape(c.TaxId)},{c.IsActive.ToString().ToLower()},{CsvEscape(c.GeneralNotes)},{CsvEscape(c.LeadSource)},{CsvEscape(c.ShipToAddress)},{CsvEscape(c.ShipToCity)},{CsvEscape(c.ShipToState)},{CsvEscape(c.ShipToZipCode)},{CsvEscape(c.ShipToCountry)}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -552,13 +565,13 @@ public class DataExportController : Controller
|
|||||||
.Include(j => j.Customer).Include(j => j.JobStatus).Include(j => j.JobPriority)
|
.Include(j => j.Customer).Include(j => j.JobStatus).Include(j => j.JobPriority)
|
||||||
.OrderByDescending(j => j.CreatedAt).ToListAsync();
|
.OrderByDescending(j => j.CreatedAt).ToListAsync();
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,FinalPrice,CustomerPO,SpecialInstructions,Notes");
|
sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,ProjectName,FinalPrice,CustomerPO,SpecialInstructions,Notes");
|
||||||
foreach (var j in data)
|
foreach (var j in data)
|
||||||
{
|
{
|
||||||
var customerName = !string.IsNullOrWhiteSpace(j.Customer?.CompanyName)
|
var customerName = !string.IsNullOrWhiteSpace(j.Customer?.CompanyName)
|
||||||
? j.Customer.CompanyName
|
? j.Customer.CompanyName
|
||||||
: $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
|
: $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
|
||||||
sb.AppendLine($"{CsvEscape(j.JobNumber)},{CsvEscape(j.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(j.JobStatus?.DisplayName)},{CsvEscape(j.JobPriority?.DisplayName)},{j.ScheduledDate?.ToString("yyyy-MM-dd")},{j.DueDate?.ToString("yyyy-MM-dd")},{j.FinalPrice},{CsvEscape(j.CustomerPO)},{CsvEscape(j.SpecialInstructions)},{CsvEscape(j.InternalNotes)}");
|
sb.AppendLine($"{CsvEscape(j.JobNumber)},{CsvEscape(j.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(j.JobStatus?.DisplayName)},{CsvEscape(j.JobPriority?.DisplayName)},{j.ScheduledDate?.ToString("yyyy-MM-dd")},{j.DueDate?.ToString("yyyy-MM-dd")},{CsvEscape(j.ProjectName)},{j.FinalPrice},{CsvEscape(j.CustomerPO)},{CsvEscape(j.SpecialInstructions)},{CsvEscape(j.InternalNotes)}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -574,13 +587,13 @@ public class DataExportController : Controller
|
|||||||
.Where(q => q.CompanyId == companyId && !q.IsDeleted)
|
.Where(q => q.CompanyId == companyId && !q.IsDeleted)
|
||||||
.Include(q => q.Customer).Include(q => q.QuoteStatus).OrderByDescending(q => q.QuoteDate).ToListAsync();
|
.Include(q => q.Customer).Include(q => q.QuoteStatus).OrderByDescending(q => q.QuoteDate).ToListAsync();
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");
|
sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,ProjectName,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");
|
||||||
foreach (var q in data)
|
foreach (var q in data)
|
||||||
{
|
{
|
||||||
var customerName = !string.IsNullOrWhiteSpace(q.Customer?.CompanyName)
|
var customerName = !string.IsNullOrWhiteSpace(q.Customer?.CompanyName)
|
||||||
? q.Customer.CompanyName
|
? q.Customer.CompanyName
|
||||||
: $"{q.Customer?.ContactFirstName} {q.Customer?.ContactLastName}".Trim();
|
: $"{q.Customer?.ContactFirstName} {q.Customer?.ContactLastName}".Trim();
|
||||||
sb.AppendLine($"{CsvEscape(q.QuoteNumber)},{CsvEscape(q.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(q.ProspectCompanyName)},{CsvEscape(q.ProspectContactName)},{CsvEscape(q.ProspectEmail)},{CsvEscape(q.ProspectPhone)},{CsvEscape(q.QuoteStatus?.DisplayName)},{q.QuoteDate:yyyy-MM-dd},{q.ExpirationDate?.ToString("yyyy-MM-dd")},{q.SubTotal},{q.TaxAmount},{q.Total},{CsvEscape(q.Notes)},{CsvEscape(q.Terms)}");
|
sb.AppendLine($"{CsvEscape(q.QuoteNumber)},{CsvEscape(q.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(q.ProspectCompanyName)},{CsvEscape(q.ProspectContactName)},{CsvEscape(q.ProspectEmail)},{CsvEscape(q.ProspectPhone)},{CsvEscape(q.QuoteStatus?.DisplayName)},{q.QuoteDate:yyyy-MM-dd},{q.ExpirationDate?.ToString("yyyy-MM-dd")},{CsvEscape(q.ProjectName)},{q.SubTotal},{q.TaxAmount},{q.Total},{CsvEscape(q.Notes)},{CsvEscape(q.Terms)}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
@@ -596,17 +609,68 @@ public class DataExportController : Controller
|
|||||||
.Where(i => i.CompanyId == companyId && !i.IsDeleted)
|
.Where(i => i.CompanyId == companyId && !i.IsDeleted)
|
||||||
.Include(i => i.Customer).OrderByDescending(i => i.InvoiceDate).ToListAsync();
|
.Include(i => i.Customer).OrderByDescending(i => i.InvoiceDate).ToListAsync();
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("ID,Invoice #,Customer,Status,Invoice Date,Due Date,Subtotal,Tax,Total,Amount Paid,Balance Due");
|
sb.AppendLine("ID,Invoice #,Customer,Status,Invoice Date,Due Date,Project Name,Subtotal,Tax,Total,Amount Paid,Balance Due");
|
||||||
foreach (var inv in data)
|
foreach (var inv in data)
|
||||||
{
|
{
|
||||||
var cust = inv.Customer != null
|
var cust = inv.Customer != null
|
||||||
? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
|
? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
|
||||||
: $"Customer #{inv.CustomerId}";
|
: $"Customer #{inv.CustomerId}";
|
||||||
sb.AppendLine($"{inv.Id},{CsvEscape(inv.InvoiceNumber)},{CsvEscape(cust)},{inv.Status},{inv.InvoiceDate:yyyy-MM-dd},{inv.DueDate?.ToString("yyyy-MM-dd")},{inv.SubTotal},{inv.TaxAmount},{inv.Total},{inv.AmountPaid},{inv.BalanceDue}");
|
sb.AppendLine($"{inv.Id},{CsvEscape(inv.InvoiceNumber)},{CsvEscape(cust)},{inv.Status},{inv.InvoiceDate:yyyy-MM-dd},{inv.DueDate?.ToString("yyyy-MM-dd")},{CsvEscape(inv.ProjectName)},{inv.SubTotal},{inv.TaxAmount},{inv.Total},{inv.AmountPaid},{inv.BalanceDue}");
|
||||||
}
|
}
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a CustomerContacts worksheet: one row per additional contact linked to the company's customers.
|
||||||
|
/// CustomerEmail is the join key used by the importer to re-link contacts to their parent customer.
|
||||||
|
/// </summary>
|
||||||
|
private async Task AddCustomerContactsSheet(ExcelPackage pkg, int companyId, Color hdr)
|
||||||
|
{
|
||||||
|
var data = await _db.CustomerContacts.AsNoTracking().IgnoreQueryFilters()
|
||||||
|
.Include(cc => cc.Customer)
|
||||||
|
.Where(cc => cc.CompanyId == companyId && !cc.IsDeleted)
|
||||||
|
.OrderBy(cc => cc.Customer!.CompanyName).ThenBy(cc => cc.LastName).ThenBy(cc => cc.FirstName)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var ws = pkg.Workbook.Worksheets.Add("CustomerContacts");
|
||||||
|
var headers = new[] { "CustomerEmail", "FirstName", "LastName", "Title", "ContactRole", "Email", "Phone", "MobilePhone", "Notes" };
|
||||||
|
WriteHeader(ws, headers, hdr);
|
||||||
|
|
||||||
|
for (int i = 0; i < data.Count; i++)
|
||||||
|
{
|
||||||
|
var r = i + 2;
|
||||||
|
var cc = data[i];
|
||||||
|
ws.Cells[r, 1].Value = cc.Customer?.Email;
|
||||||
|
ws.Cells[r, 2].Value = cc.FirstName;
|
||||||
|
ws.Cells[r, 3].Value = cc.LastName;
|
||||||
|
ws.Cells[r, 4].Value = cc.Title;
|
||||||
|
ws.Cells[r, 5].Value = cc.ContactRole;
|
||||||
|
ws.Cells[r, 6].Value = cc.Email;
|
||||||
|
ws.Cells[r, 7].Value = cc.Phone;
|
||||||
|
ws.Cells[r, 8].Value = cc.MobilePhone;
|
||||||
|
ws.Cells[r, 9].Value = cc.Notes;
|
||||||
|
}
|
||||||
|
AutoFit(ws, headers.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the customer contacts CSV string for the specified company.
|
||||||
|
/// CustomerEmail is the join key used by the importer to re-link contacts to their parent customer.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string> BuildCustomerContactsCsv(int companyId)
|
||||||
|
{
|
||||||
|
var data = await _db.CustomerContacts.AsNoTracking().IgnoreQueryFilters()
|
||||||
|
.Include(cc => cc.Customer)
|
||||||
|
.Where(cc => cc.CompanyId == companyId && !cc.IsDeleted)
|
||||||
|
.OrderBy(cc => cc.Customer!.CompanyName).ThenBy(cc => cc.LastName).ThenBy(cc => cc.FirstName)
|
||||||
|
.ToListAsync();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("CustomerEmail,FirstName,LastName,Title,ContactRole,Email,Phone,MobilePhone,Notes");
|
||||||
|
foreach (var cc in data)
|
||||||
|
sb.AppendLine($"{CsvEscape(cc.Customer?.Email)},{CsvEscape(cc.FirstName)},{CsvEscape(cc.LastName)},{CsvEscape(cc.Title)},{CsvEscape(cc.ContactRole)},{CsvEscape(cc.Email)},{CsvEscape(cc.Phone)},{CsvEscape(cc.MobilePhone)},{CsvEscape(cc.Notes)}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the inventory CSV string for the specified company, ordered alphabetically by name.
|
/// Builds the inventory CSV string for the specified company, ordered alphabetically by name.
|
||||||
/// Column names match <see cref="InventoryItemImportDto"/> exactly so the file can be re-imported.
|
/// Column names match <see cref="InventoryItemImportDto"/> exactly so the file can be re-imported.
|
||||||
|
|||||||
@@ -31,8 +31,9 @@
|
|||||||
<div class="row g-2 mb-4">
|
<div class="row g-2 mb-4">
|
||||||
@foreach (var item in new[]
|
@foreach (var item in new[]
|
||||||
{
|
{
|
||||||
("Customers", "people", "Customers"),
|
("Customers", "people", "Customers"),
|
||||||
("Jobs", "tools", "Jobs"),
|
("CustomerContacts", "person-lines-fill", "Customer Contacts"),
|
||||||
|
("Jobs", "tools", "Jobs"),
|
||||||
("Quotes", "file-earmark-text", "Quotes"),
|
("Quotes", "file-earmark-text", "Quotes"),
|
||||||
("Invoices", "receipt", "Invoices"),
|
("Invoices", "receipt", "Invoices"),
|
||||||
("Inventory", "boxes", "Inventory Items"),
|
("Inventory", "boxes", "Inventory Items"),
|
||||||
|
|||||||
@@ -180,6 +180,13 @@
|
|||||||
<i class="bi bi-people me-1 text-muted"></i>Customers
|
<i class="bi bi-people me-1 text-muted"></i>Customers
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check mb-1">
|
||||||
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
||||||
|
value="CustomerContacts" id="chkCustomerContacts" checked />
|
||||||
|
<label class="form-check-label" for="chkCustomerContacts">
|
||||||
|
<i class="bi bi-person-lines-fill me-1 text-muted"></i>Customer Contacts
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="form-check mb-1">
|
<div class="form-check mb-1">
|
||||||
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
<input class="form-check-input sheet-check" type="checkbox" name="sheets"
|
||||||
value="Jobs" id="chkJobs" checked />
|
value="Jobs" id="chkJobs" checked />
|
||||||
|
|||||||
Reference in New Issue
Block a user